taler-typescript-core

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

commit 9d5ef78709bb5f2f554cc10ed6c25a6d7912a803
parent 1c253dcfe51895ec5516c76cc7e904d154360659
Author: Florian Dold <florian@dold.me>
Date:   Wed, 30 Jul 2025 20:24:32 +0200

wallet-core: properly add exchanges to pay transaction

Diffstat:
Mpackages/taler-wallet-core/src/dev-experiments.ts | 30++++++++++++++++++++++++++++++
Mpackages/taler-wallet-core/src/exchanges.ts | 1+
Mpackages/taler-wallet-core/src/pay-merchant.ts | 64++++++++++++++++++++++++++++++++++++++++++----------------------
3 files changed, 73 insertions(+), 22 deletions(-)

diff --git a/packages/taler-wallet-core/src/dev-experiments.ts b/packages/taler-wallet-core/src/dev-experiments.ts @@ -49,6 +49,7 @@ import { } from "./db.js"; import { DenomLossTransactionContext } from "./exchanges.js"; import { RefreshTransactionContext } from "./refresh.js"; +import { rematerializeTransactions } from "./transactions.js"; import { DevExperimentState, WalletExecutionContext } from "./wallet.js"; const logger = new Logger("dev-experiments.ts"); @@ -162,6 +163,35 @@ export async function applyDevExperiment( }); return; } + case "rebuild-transactions": { + await wex.db.runAllStoresReadWriteTx({}, async (tx) => { + // Re-build / fix up info about exchanges for purchase transactions. + await tx.purchases.iter().forEachAsync(async (rec) => { + let exchangeSet = new Set<string>(); + if (!rec.exchanges || rec.exchanges.length === 0) { + if (rec.payInfo?.payCoinSelection) { + const coinPubs = rec.payInfo.payCoinSelection.coinPubs; + + for (const pub of coinPubs) { + const coin = await tx.coins.get(pub); + if (!coin) { + logger.warn( + `coin ${coinPubs} not found, unable to compute full scope`, + ); + continue; + } + exchangeSet.add(coin.exchangeBaseUrl); + } + } + rec.exchanges = [...exchangeSet]; + rec.exchanges.sort(); + await tx.purchases.put(rec); + } + }); + await rematerializeTransactions(wex, tx); + }); + return; + } case "stop-fakeprotover": { const baseUrl = parsedUri.query?.get("base_url"); if (!baseUrl) { diff --git a/packages/taler-wallet-core/src/exchanges.ts b/packages/taler-wallet-core/src/exchanges.ts @@ -335,6 +335,7 @@ export async function getScopeForAllCoins( for (const pub of coinPubs) { const coin = await tx.coins.get(pub); if (!coin) { + logger.warn(`coin ${coinPubs} not found, unable to compute full scope`); continue; } exchangeSet.add(coin.exchangeBaseUrl); diff --git a/packages/taler-wallet-core/src/pay-merchant.ts b/packages/taler-wallet-core/src/pay-merchant.ts @@ -313,12 +313,10 @@ export class PayMerchantTransactionContext implements TransactionContext { scopes = []; amountEffective = Amounts.stringify(zero); } else { - scopes = await getScopeForAllCoins( - tx, - !purchaseRec.payInfo.payCoinSelection - ? [] - : purchaseRec.payInfo.payCoinSelection.coinPubs, - ); + const coinList = !purchaseRec.payInfo.payCoinSelection + ? [] + : purchaseRec.payInfo.payCoinSelection.coinPubs; + scopes = await getScopeForAllCoins(tx, coinList); amountEffective = isUnsuccessfulTransaction(txState) ? Amounts.stringify(zero) : Amounts.stringify(purchaseRec.payInfo.totalPayCost); @@ -1218,9 +1216,12 @@ async function processDownloadProposal( if (contractData.choices.length === 1) { currency = Amounts.currencyOf(contractData.choices[0].amount); } else if (contractData.choices.length > 1) { - const firstCurrency = Amounts.currencyOf(contractData.choices[0].amount); - const allSame = contractData.choices.every((c) => - Amounts.currencyOf(c.amount) === firstCurrency); + const firstCurrency = Amounts.currencyOf( + contractData.choices[0].amount, + ); + const allSame = contractData.choices.every( + (c) => Amounts.currencyOf(c.amount) === firstCurrency, + ); if (allSame) { currency = firstCurrency; } @@ -1296,7 +1297,9 @@ async function generateSlate( "can't process slates without secretSeed", ); - logger.trace(`generating slate (${choiceIndex}, ${outputIndex}) for purchase ${purchase.proposalId}`); + logger.trace( + `generating slate (${choiceIndex}, ${outputIndex}) for purchase ${purchase.proposalId}`, + ); let slate = await wex.db.runReadOnlyTx( { storeNames: ["slates"] }, @@ -2858,7 +2861,9 @@ export async function confirmPay( ); p.choiceIndex = choiceIndex; - if (p.download && choiceIndex !== undefined && + if ( + p.download && + choiceIndex !== undefined && contractData.version === MerchantContractVersion.V1 ) { const amount = contractData.choices[choiceIndex].amount; @@ -2875,7 +2880,7 @@ export async function confirmPay( if (selectTokensResult?.type === "success") { const tokens = selectTokensResult.tokens; p.payInfo.payTokenSelection = { - tokenPubs: tokens.map(t => t.tokenUsePub), + tokenPubs: tokens.map((t) => t.tokenUsePub), }; } if (selectCoinsResult.type === "success") { @@ -2886,6 +2891,12 @@ export async function confirmPay( coinPubs: selectCoinsResult.coinSel.coins.map((x) => x.coinPub), }; p.payInfo.payCoinSelectionUid = encodeCrock(getRandomBytes(16)); + p.exchanges = [ + ...new Set( + selectCoinsResult.coinSel.coins.map((x) => x.exchangeBaseUrl), + ), + ]; + p.exchanges.sort(); } p.lastSessionId = sessionId; p.timestampAccept = timestampPreciseToDb(TalerPreciseTimestamp.now()); @@ -3393,13 +3404,18 @@ async function processPurchasePay( } if (tokenSigs) { - await wex.db.runReadWriteTx({ - storeNames: ["purchases"], - }, async (tx) => { - if (!purchase.payInfo) { return; } - purchase.payInfo.slateTokenSigs = tokenSigs; - tx.purchases.put(purchase); - }); + await wex.db.runReadWriteTx( + { + storeNames: ["purchases"], + }, + async (tx) => { + if (!purchase.payInfo) { + return; + } + purchase.payInfo.slateTokenSigs = tokenSigs; + tx.purchases.put(purchase); + }, + ); } // store token outputs @@ -3457,7 +3473,9 @@ export async function validateAndStoreToken( blindedEv: SignedTokenEnvelope, ): Promise<void> { const { tokenIssuePub, tokenIssuePubHash, tokenUsePub, blindingKey } = slate; - logger.trace(`validating token ${tokenIssuePubHash} for purchase ${slate.purchaseId}`); + logger.trace( + `validating token ${tokenIssuePubHash} for purchase ${slate.purchaseId}`, + ); const tokenIssueSig = await wex.cryptoApi.unblindTokenIssueSignature({ slate: { tokenIssuePub, @@ -3476,7 +3494,7 @@ export async function validateAndStoreToken( hm: tokenUsePub, pk: tokenIssuePub.rsa_pub, sig: tokenIssueSig.rsa_signature, - }) + }); if (!valid) { logger.error("token issue signature invalid"); @@ -3484,7 +3502,9 @@ export async function validateAndStoreToken( throw Error("token issue signature invalid"); } - logger.trace(`token ${tokenIssuePubHash} for purchase ${slate.purchaseId} is valid, will be stored`); + logger.trace( + `token ${tokenIssuePubHash} for purchase ${slate.purchaseId} is valid, will be stored`, + ); const token: TokenRecord = { tokenIssueSig,