commit c077f765817da803b4e5db58e6c79dad12f82873
parent b8c8a914fc31918fa8e8ed40359bf168476a946a
Author: Iván Ávalos <avalos@disroot.org>
Date: Fri, 5 Dec 2025 19:14:55 +0100
wallet-core: add exchangeMasterPubMismatch flag and split balanceReceiverAcceptable
Diffstat:
7 files changed, 169 insertions(+), 8 deletions(-)
diff --git a/packages/taler-util/src/types-taler-wallet.ts b/packages/taler-util/src/types-taler-wallet.ts
@@ -898,11 +898,28 @@ export interface PaymentInsufficientBalanceDetails {
balanceAgeAcceptable: AmountString;
/**
- * Balance of type "merchant-acceptable" (see balance.ts for definition).
+ * Balance of type "receiver-acceptable" (see balance.ts for definition).
+ *
+ * @deprecated (2025-12-05) use balanceReceiver[...]Acceptable instead.
*/
balanceReceiverAcceptable: AmountString;
/**
+ * Balance of type "receiver-exchange-url-acceptable" (see balance.ts for definition).
+ */
+ balanceReceiverExchangeUrlAcceptable: AmountString;
+
+ /**
+ * Balance of type "receiver-exchange-pub-acceptable" (see balance.ts for definition).
+ */
+ balanceReceiverExchangePubAcceptable: AmountString;
+
+ /**
+ * Balance of type "receiver-auditor-url-acceptable" (see balance.ts for definition).
+ */
+ balanceReceiverAuditorUrlAcceptable: AmountString;
+
+ /**
* Balance of type "merchant-depositable" (see balance.ts for definition).
*/
balanceReceiverDepositable: AmountString;
@@ -921,11 +938,25 @@ export interface PaymentInsufficientBalanceDetails {
balanceMaterial: AmountString;
balanceExchangeDepositable: AmountString;
balanceAgeAcceptable: AmountString;
+
+ /**
+ * @deprecated (2025-12-05) use balanceReceiver[...]Acceptable instead.
+ */
balanceReceiverAcceptable: AmountString;
+
+ balanceReceiverExchangeUrlAcceptable: AmountString;
+ balanceReceiverExchangePubAcceptable: AmountString;
+ balanceReceiverAuditorUrlAcceptable: AmountString;
balanceReceiverDepositable: AmountString;
maxEffectiveSpendAmount: AmountString;
/**
+ * The exchange master public key configured by the merchant
+ * backend differs from the one of the coins stored in the wallet.
+ */
+ exchangeMasterPubMismatch: boolean;
+
+ /**
* Exchange doesn't have global fees configured for the relevant year,
* p2p payments aren't possible.
*
@@ -994,10 +1025,14 @@ export const codecForPayMerchantInsufficientBalanceDetails =
.property("balanceAvailable", codecForAmountString())
.property("balanceMaterial", codecForAmountString())
.property("balanceReceiverAcceptable", codecForAmountString())
+ .property("balanceReceiverExchangeUrlAcceptable", codecForAmountString())
+ .property("balanceReceiverExchangePubAcceptable", codecForAmountString())
+ .property("balanceReceiverAuditorUrlAcceptable", codecForAmountString())
.property("balanceReceiverDepositable", codecForAmountString())
.property("balanceExchangeDepositable", codecForAmountString())
.property("perExchange", codecForAny())
.property("maxEffectiveSpendAmount", codecForAmountString())
+ .deprecatedProperty("balanceReceiverAcceptable")
.build("PayMerchantInsufficientBalanceDetails");
export const codecForPreparePayResultInsufficientBalance =
@@ -1645,6 +1680,8 @@ export interface DenominationInfo {
stampExpireDeposit: TalerProtocolTimestamp;
exchangeBaseUrl: string;
+
+ exchangeMasterPub: string;
}
export type DenomOperation = "deposit" | "withdraw" | "refresh" | "refund";
diff --git a/packages/taler-wallet-core/src/balance.ts b/packages/taler-wallet-core/src/balance.ts
@@ -39,10 +39,20 @@
* - "age-acceptable": Subset of the material balance that can be spent
* with age restrictions applied.
*
- * - "merchant-acceptable": Subset of the material balance that can be spent with a particular
+ * - "receiver-acceptable": Subset of the material balance that can be spent with a particular
* merchant (restricted via min age, exchange, auditor, wire_method).
+ * Deprecated in favor of more specific "receiver-*-acceptable"" balance types.
*
- * - "merchant-depositable": Subset of the merchant-acceptable balance that the merchant
+ * - "receiver-exchange-url-acceptable": Subset of the material balance that can be spent
+ * with a particular merchant based on the exchange URLs it accepts.
+ *
+ * - "receiver-exchange-pub-acceptable": Subset of the material balance that can be spent
+ * with a particular merchant based on the exchange public keys it accepts.
+ *
+ * - "receiver-auditor-url-acceptable": Subset of the material balance that can be spent
+ * with a particular merchant based on the auditor URLs it accepts.
+ *
+ * - "receiver-depositable": Subset of the merchant-acceptable balance that the merchant
* can accept via their supported wire methods.
*/
@@ -837,10 +847,27 @@ export interface PaymentBalanceDetails {
/**
* Balance of type "receiver-acceptable" (see balance.ts for definition).
+ *
+ * @deprecated (2025-12-05) use balanceReceiver[...]Acceptable instead.
*/
balanceReceiverAcceptable: AmountJson;
/**
+ * Balance of type "receiver-exchange-url-acceptable" (see balance.ts for definition).
+ */
+ balanceReceiverExchangeUrlAcceptable: AmountJson;
+
+ /**
+ * Balance of type "receiver-exchange-pub-acceptable" (see balance.ts for definition).
+ */
+ balanceReceiverExchangePubAcceptable: AmountJson;
+
+ /**
+ * Balance of type "receiver-auditor-url-acceptable" (see balance.ts for definition).
+ */
+ balanceReceiverAuditorUrlAcceptable: AmountJson;
+
+ /**
* Balance of type "receiver-depositable" (see balance.ts for definition).
*/
balanceReceiverDepositable: AmountJson;
@@ -901,6 +928,9 @@ export async function getPaymentBalanceDetailsInTx(
balanceMaterial: Amounts.zeroOfCurrency(req.currency),
balanceAgeAcceptable: Amounts.zeroOfCurrency(req.currency),
balanceReceiverAcceptable: Amounts.zeroOfCurrency(req.currency),
+ balanceReceiverExchangeUrlAcceptable: Amounts.zeroOfCurrency(req.currency),
+ balanceReceiverExchangePubAcceptable: Amounts.zeroOfCurrency(req.currency),
+ balanceReceiverAuditorUrlAcceptable: Amounts.zeroOfCurrency(req.currency),
balanceReceiverDepositable: Amounts.zeroOfCurrency(req.currency),
maxMerchantEffectiveDepositAmount: Amounts.zeroOfCurrency(req.currency),
balanceExchangeDepositable: Amounts.zeroOfCurrency(req.currency),
@@ -970,27 +1000,41 @@ export async function getPaymentBalanceDetailsInTx(
let ageOkay = ca.maxAge === 0 || ca.maxAge > req.minAge;
- let merchantExchangeAcceptable = false;
+ let merchantExchangePubAcceptable = false;
+ let merchantExchangeUrlAcceptable = false;
+ let merchantExchangeAuditorAcceptable = false;
if (!req.restrictReceiverExchanges) {
- merchantExchangeAcceptable = true;
+ merchantExchangePubAcceptable = true;
+ merchantExchangeUrlAcceptable = true;
+ merchantExchangeAuditorAcceptable = true;
} else {
for (const ex of req.restrictReceiverExchanges.exchanges) {
+ if (ex.exchangePub === ca.exchangeMasterPub) {
+ merchantExchangePubAcceptable = true;
+ break;
+ }
+ }
+
+ for (const ex of req.restrictReceiverExchanges.exchanges) {
if (ex.exchangeBaseUrl === ca.exchangeBaseUrl) {
- merchantExchangeAcceptable = true;
+ merchantExchangeUrlAcceptable = true;
break;
}
}
+
for (const acceptedAuditor of req.restrictReceiverExchanges.auditors) {
for (const exchangeAuditor of wireDetails.auditors) {
if (acceptedAuditor.auditorBaseUrl === exchangeAuditor.auditor_url) {
- merchantExchangeAcceptable = true;
+ merchantExchangeAuditorAcceptable = true;
break;
}
}
}
}
+ const merchantExchangeAcceptable =
+ merchantExchangeUrlAcceptable || merchantExchangeAuditorAcceptable;
const merchantExchangeDepositable = merchantExchangeAcceptable && wireOkay;
d.balanceAvailable = Amounts.add(d.balanceAvailable, coinAmount).amount;
@@ -1011,6 +1055,24 @@ export async function getPaymentBalanceDetailsInTx(
coinAmount,
).amount;
}
+ if (merchantExchangeUrlAcceptable) {
+ d.balanceReceiverExchangeUrlAcceptable = Amounts.add(
+ d.balanceReceiverExchangeUrlAcceptable,
+ coinAmount,
+ ).amount;
+ }
+ if (merchantExchangePubAcceptable) {
+ d.balanceReceiverExchangePubAcceptable = Amounts.add(
+ d.balanceReceiverExchangePubAcceptable,
+ coinAmount,
+ ).amount;
+ }
+ if (merchantExchangeAuditorAcceptable) {
+ d.balanceReceiverAuditorUrlAcceptable = Amounts.add(
+ d.balanceReceiverAuditorUrlAcceptable,
+ coinAmount,
+ ).amount;
+ }
}
}
diff --git a/packages/taler-wallet-core/src/coinSelection.test.ts b/packages/taler-wallet-core/src/coinSelection.test.ts
@@ -60,6 +60,7 @@ test("p2p: should select the coin", (t) => {
numAvailable: 5,
depositFee: "LOCAL:0.1" as AmountString,
fromExchange: "http://exchange.localhost/",
+ fromMasterPub: "123",
},
]),
tally,
@@ -95,6 +96,7 @@ test("p2p: should select 3 coins", (t) => {
numAvailable: 5,
depositFee: "LOCAL:0.1" as AmountString,
fromExchange: "http://exchange.localhost/",
+ fromMasterPub: "123",
},
]),
tally,
@@ -129,6 +131,7 @@ test("p2p: can't select since the instructed amount is too high", (t) => {
numAvailable: 5,
depositFee: "LOCAL:0.1" as AmountString,
fromExchange: "http://exchange.localhost/",
+ fromMasterPub: "123",
},
]),
tally,
@@ -160,6 +163,7 @@ test("pay: select one coin to pay with fee", (t) => {
numAvailable: 5,
depositFee: "LOCAL:0.1" as AmountString,
fromExchange: "http://exchange.localhost/",
+ fromMasterPub: "123",
},
]),
tally,
@@ -191,6 +195,7 @@ function createCandidates(
depositFee: AmountString;
numAvailable: number;
fromExchange: string;
+ fromMasterPub: string;
}[],
): AvailableCoinsOfDenom[] {
return ar.map((r, idx) => {
@@ -211,6 +216,7 @@ function createCandidates(
stampExpireWithdraw: inTheDistantFuture,
stampStart: inThePast,
exchangeBaseUrl: r.fromExchange,
+ exchangeMasterPub: r.fromMasterPub,
numAvailable: r.numAvailable,
maxAge: 32,
};
@@ -317,6 +323,7 @@ function defaultFeeConfig(
feeRefund: `KUDOS:0.01`,
feeWithdraw: `KUDOS:0.01`,
exchangeBaseUrl: "2",
+ exchangeMasterPub: "123",
maxAge: 0,
numAvailable: totalAvailable,
stampExpireDeposit: TalerProtocolTimestamp.never(),
@@ -471,12 +478,14 @@ test("overpay when remaining < depositFee", (t) => {
numAvailable: 1,
depositFee: "LOCAL:0.2" as AmountString,
fromExchange: "http://exchange.localhost/",
+ fromMasterPub: "123",
},
{
amount: "LOCAL:1" as AmountString,
numAvailable: 1,
depositFee: "LOCAL:0.2" as AmountString,
fromExchange: "http://exchange.localhost/",
+ fromMasterPub: "123",
},
]),
tally,
diff --git a/packages/taler-wallet-core/src/coinSelection.ts b/packages/taler-wallet-core/src/coinSelection.ts
@@ -630,6 +630,15 @@ export async function reportInsufficientBalanceDetails(
balanceReceiverAcceptable: Amounts.stringify(
exchDet.balanceReceiverAcceptable,
),
+ balanceReceiverExchangeUrlAcceptable: Amounts.stringify(
+ exchDet.balanceReceiverExchangeUrlAcceptable,
+ ),
+ balanceReceiverExchangePubAcceptable: Amounts.stringify(
+ exchDet.balanceReceiverExchangePubAcceptable,
+ ),
+ balanceReceiverAuditorUrlAcceptable: Amounts.stringify(
+ exchDet.balanceReceiverAuditorUrlAcceptable,
+ ),
balanceReceiverDepositable: Amounts.stringify(
exchDet.balanceReceiverDepositable,
),
@@ -637,6 +646,9 @@ export async function reportInsufficientBalanceDetails(
exchDet.maxMerchantEffectiveDepositAmount,
),
missingGlobalFees,
+ exchangeMasterPubMismatch: Amounts.cmp(
+ exchDet.balanceReceiverExchangeUrlAcceptable,
+ exchDet.balanceReceiverExchangePubAcceptable) != 0,
causeHint: !!wex.ws.devExperimentState.merchantDepositInsufficient
? InsufficientBalanceHint.MerchantDepositInsufficient
: getHint(req, exchDet),
@@ -655,6 +667,15 @@ export async function reportInsufficientBalanceDetails(
balanceReceiverAcceptable: Amounts.stringify(
details.balanceReceiverAcceptable,
),
+ balanceReceiverExchangeUrlAcceptable: Amounts.stringify(
+ details.balanceReceiverExchangeUrlAcceptable,
+ ),
+ balanceReceiverExchangePubAcceptable: Amounts.stringify(
+ details.balanceReceiverExchangePubAcceptable,
+ ),
+ balanceReceiverAuditorUrlAcceptable: Amounts.stringify(
+ details.balanceReceiverAuditorUrlAcceptable,
+ ),
balanceExchangeDepositable: Amounts.stringify(
details.balanceExchangeDepositable,
),
diff --git a/packages/taler-wallet-core/src/common.ts b/packages/taler-wallet-core/src/common.ts
@@ -151,6 +151,7 @@ export async function makeCoinAvailable(
currency: denom.currency,
denomPubHash: denom.denomPubHash,
exchangeBaseUrl: denom.exchangeBaseUrl,
+ exchangeMasterPub: denom.exchangeMasterPub,
freshCoinCount: 0,
visibleCoinCount: 0,
};
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts
@@ -171,7 +171,7 @@ export const CURRENT_DB_CONFIG_KEY = "currentMainDbName";
* backwards-compatible way or object stores and indices
* are added.
*/
-export const WALLET_DB_MINOR_VERSION = 24;
+export const WALLET_DB_MINOR_VERSION = 25;
declare const symDbProtocolTimestamp: unique symbol;
@@ -595,6 +595,7 @@ export namespace DenominationRecord {
stampStart: timestampProtocolFromDb(d.stampStart),
value: Amounts.stringify(d.value),
exchangeBaseUrl: d.exchangeBaseUrl,
+ exchangeMasterPub: d.exchangeMasterPub,
};
}
}
@@ -2597,6 +2598,7 @@ export interface CoinAvailabilityRecord {
value: AmountString;
denomPubHash: string;
exchangeBaseUrl: string;
+ exchangeMasterPub?: string;
/**
* Age restriction on the coin, or 0 for no age restriction (or
@@ -3833,6 +3835,13 @@ export const walletDbFixups: FixupDescription[] = [
fn: fixupTokenFamilyHash,
name: "fixupTokenFamilyHash",
},
+ // Removing this would cause merchant acceptable
+ // amount to be calculaed based on exchangeBaseUrl
+ // instead of masterPublicKey for old coins.
+ {
+ fn: fixupCoinAvailabilityExchangePub,
+ name: "fixupCoinAvailabilityExchangePub",
+ }
];
/**
@@ -3958,6 +3967,27 @@ async function fixupTokenFamilyHash(
}
}
+async function fixupCoinAvailabilityExchangePub(
+ tx: WalletDbAllStoresReadWriteTransaction,
+): Promise<void> {
+ const cars = await tx.coinAvailability.getAll();
+ const exchanges: Record<string, ExchangeDetailsRecord | undefined> = {};
+ for (const car of cars) {
+ if (car.exchangeMasterPub === undefined) {
+ if (exchanges[car.exchangeBaseUrl] === undefined) {
+ exchanges[car.exchangeBaseUrl] = await tx.exchangeDetails.indexes
+ .byExchangeBaseUrl.get(car.exchangeBaseUrl);
+ }
+
+ const exchange = exchanges[car.exchangeBaseUrl];
+ if (exchange !== undefined) {
+ car.exchangeMasterPub = exchange.masterPublicKey;
+ await tx.coinAvailability.put(car);
+ }
+ }
+ }
+}
+
export async function applyFixups(
db: DbAccess<typeof WalletStoresV1>,
): Promise<number> {
diff --git a/packages/taler-wallet-core/src/refresh.ts b/packages/taler-wallet-core/src/refresh.ts
@@ -505,6 +505,7 @@ async function getCoinAvailabilityForDenom(
currency: Amounts.currencyOf(denom.value),
denomPubHash: denom.denomPubHash,
exchangeBaseUrl: denom.exchangeBaseUrl,
+ exchangeMasterPub: denom.exchangeMasterPub,
freshCoinCount: 0,
visibleCoinCount: 0,
};