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:
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,