taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

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:
Mpackages/taler-util/src/types-taler-wallet.ts | 39++++++++++++++++++++++++++++++++++++++-
Mpackages/taler-wallet-core/src/balance.ts | 74++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Mpackages/taler-wallet-core/src/coinSelection.test.ts | 9+++++++++
Mpackages/taler-wallet-core/src/coinSelection.ts | 21+++++++++++++++++++++
Mpackages/taler-wallet-core/src/common.ts | 1+
Mpackages/taler-wallet-core/src/db.ts | 32+++++++++++++++++++++++++++++++-
Mpackages/taler-wallet-core/src/refresh.ts | 1+
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, };