commit bad14bd70143aaa680bde62fa97e5f941e6284a2
parent 6c81a0791cf3f2e068cf53e52f7dd88c21d8ee0f
Author: Iván Ávalos <avalos@disroot.org>
Date: Mon, 30 Jun 2025 19:09:18 +0200
wallet-core: fixes and improvements to token selection
Diffstat:
2 files changed, 71 insertions(+), 81 deletions(-)
diff --git a/packages/taler-wallet-core/src/pay-merchant.ts b/packages/taler-wallet-core/src/pay-merchant.ts
@@ -3358,6 +3358,7 @@ async function processPurchasePay(
const slatesLen = slates?.length ?? 0;
const sigsLen = merchantResp.token_sigs?.length ?? 0;
+ logger.trace(`received ${sigsLen} token signatures from merchant`);
if (slatesLen !== sigsLen) {
throw Error(
`merchant returned mismatching number of token signatures (${slatesLen} vs ${sigsLen})`,
@@ -3370,8 +3371,8 @@ async function processPurchasePay(
}
}
- if (purchase.choiceIndex) {
- await cleanupUsedTokens(wex, purchase.proposalId, purchase.choiceIndex);
+ if (payInfo.payTokenSelection?.tokenPubs) {
+ await cleanupUsedTokens(wex, payInfo.payTokenSelection.tokenPubs);
}
await storeFirstPaySuccess(wex, proposalId, sessionId, merchantResp);
@@ -3436,6 +3437,8 @@ export async function validateAndStoreToken(
throw Error("token issue signature invalid");
}
+ logger.trace(`token ${tokenIssuePubHash} for purchase ${slate.purchaseId} is valid, will be stored`);
+
const token: TokenRecord = {
tokenIssueSig,
...slate,
@@ -3514,21 +3517,15 @@ export async function generateTokenSigs(
export async function cleanupUsedTokens(
wex: WalletExecutionContext,
- proposalId: string,
- choiceIndex: number,
+ tokenPubs: string[],
): Promise<void> {
await wex.db.runReadWriteTx(
{
storeNames: ["tokens"],
},
async (tx) => {
- const tokenPubs =
- await tx.tokens.indexes.byPurchaseIdAndChoiceIndex.getAllKeys([
- proposalId,
- choiceIndex,
- ]);
-
for (const pub of tokenPubs) {
+ logger.trace(`cleaning up used token ${pub}`);
tx.tokens.delete(pub);
}
},
diff --git a/packages/taler-wallet-core/src/tokenSelection.ts b/packages/taler-wallet-core/src/tokenSelection.ts
@@ -20,11 +20,9 @@ import {
hashTokenIssuePub,
j2s,
Logger,
- MerchantContractInput,
MerchantContractInputType,
MerchantContractTermsV1,
MerchantContractTokenDetails,
- MerchantContractTokenFamily,
MerchantContractTokenKind,
PaymentTokenAvailabilityDetails,
} from "@gnu-taler/taler-util";
@@ -179,30 +177,33 @@ export async function selectPayTokensInTx(
}
const inputs = req.contractTerms.choices[req.choiceIndex].inputs;
- const tokenFamilies = req.contractTerms.token_families;
+ const inputTokens: {[tokenIssuePubHash: string]: TokenRecord[]} = {};
+ const inputCounts: {[tokenIssuePubHash: string]: number} = {};
+ var tokensRequested = 0;
- const inputTokens: {[slug: string]: TokenRecord[]} = {};
for (const input of inputs) {
if (input.type == MerchantContractInputType.Token) {
const slug = input.token_family_slug;
const keys = req.contractTerms.token_families[slug].keys;
+ const count = input.count ?? 1;
+ tokensRequested += count;
for (const key of keys) {
const keyHash = encodeCrock(hashTokenIssuePub(key));
const t = await tx.tokens.indexes.byTokenIssuePubHash.getAll(keyHash);
- if (inputTokens[slug]) {
- inputTokens[slug].push(...t);
- } else {
- inputTokens[slug] = t;
+ if (inputTokens[keyHash] === undefined) {
+ logger.trace(`found total of ${t.length} tokens for token family ${keyHash}`);
+ inputCounts[keyHash] = count;
+ inputTokens[keyHash] = t;
}
}
}
}
return selectTokenCandidates(
- inputs,
- tokenFamilies,
inputTokens,
+ inputCounts,
+ tokensRequested,
proposal.merchantBaseUrl,
);
}
@@ -225,13 +226,13 @@ export async function selectPayTokens(
}
export function selectTokenCandidates(
- inputs: MerchantContractInput[],
- tokenFamilies: {[slug: string]: MerchantContractTokenFamily},
- inputTokens: {[slug: string]: TokenRecord[]},
+ inputTokens: {[tokenIssuePubHash: string]: TokenRecord[]},
+ inputCounts: {[tokenIssuePubHash: string]: number},
+ tokensRequested: number,
merchantBaseUrl: string,
): SelectPayTokensResult {
const details: PaymentTokenAvailabilityDetails = {
- tokensRequested: 0,
+ tokensRequested,
tokensAvailable: 0,
tokensUnexpected: 0,
tokensUntrusted: 0,
@@ -241,68 +242,60 @@ export function selectTokenCandidates(
var insufficient = false;
const records: TokenRecord[] = [];
- for (const input of inputs) {
- if (input.type == MerchantContractInputType.Token) {
- const count = input.count ?? 1;
- const slug = input.token_family_slug;
- const tokens = inputTokens[slug];
- details.tokensRequested += count;
+ for (const keyHash in inputTokens) {
+ const tokens = inputTokens[keyHash];
+ const count = inputCounts[keyHash];
- for (const key of tokenFamilies[slug].keys) {
- const keyHash = encodeCrock(hashTokenIssuePub(key));
- if (details.perTokenFamily[keyHash] === undefined) {
- details.perTokenFamily[keyHash] = {
- requested: count,
- available: 0,
- unexpected: 0,
- untrusted: 0,
- }
+ details.perTokenFamily[keyHash] = {
+ requested: count,
+ available: 0,
+ unexpected: 0,
+ untrusted: 0,
+ };
+
+ // Selection algorithm:
+ // - filter out spent tokens (i.e. no transactionId)
+ // - filter out expired/not-yet-valid tokens
+ // - filter out tokens with errors
+ // - sort ascending by expiration date
+ // - choose the first n tokens in the list
+ const usable = tokens.filter(tok =>
+ !tok.transactionId && AbsoluteTime.isBetween(
+ AbsoluteTime.now(),
+ AbsoluteTime.fromProtocolTimestamp(timestampProtocolFromDb(tok.validAfter)),
+ AbsoluteTime.fromProtocolTimestamp(timestampProtocolFromDb(tok.validBefore)),
+ )).filter(tok => {
+ const res = verifyTokenMerchant(
+ merchantBaseUrl,
+ tok.merchantBaseUrl,
+ tok.extraData,
+ );
+ switch (res) {
+ case TokenMerchantVerificationResult.Automatic:
+ details.perTokenFamily[keyHash].available += 1;
+ return true; // usable
+ case TokenMerchantVerificationResult.Unexpected:
+ details.tokensUnexpected += 1;
+ details.perTokenFamily[keyHash].unexpected += 1;
+ details.perTokenFamily[keyHash].available += 1;
+ return true; // usable
+ case TokenMerchantVerificationResult.Untrusted:
+ details.tokensUntrusted += 1;
+ details.perTokenFamily[keyHash].untrusted += 1;
+ return false; // non-usable
+ default:
+ assertUnreachable(res);
}
- }
+ }).sort((a, b) => a.validBefore - b.validBefore);
- // Selection algorithm:
- // - filter out spent tokens (i.e. no transactionId)
- // - filter out expired/not-yet-valid tokens
- // - filter out tokens with errors
- // - sort ascending by expiration date
- // - choose the first n tokens in the list
- const usable = tokens.filter(tok =>
- !tok.transactionId && AbsoluteTime.isBetween(
- AbsoluteTime.now(),
- AbsoluteTime.fromProtocolTimestamp(timestampProtocolFromDb(tok.validAfter)),
- AbsoluteTime.fromProtocolTimestamp(timestampProtocolFromDb(tok.validBefore)),
- )).filter(tok => {
- const res = verifyTokenMerchant(
- merchantBaseUrl,
- tok.merchantBaseUrl,
- tok.extraData,
- );
- switch (res) {
- case TokenMerchantVerificationResult.Automatic:
- details.perTokenFamily[tok.tokenIssuePubHash].available += 1;
- return true; // usable
- case TokenMerchantVerificationResult.Unexpected:
- details.tokensUnexpected += 1;
- details.perTokenFamily[tok.tokenIssuePubHash].unexpected += 1;
- details.perTokenFamily[tok.tokenIssuePubHash].available += 1;
- return true; // usable
- case TokenMerchantVerificationResult.Untrusted:
- details.tokensUntrusted += 1;
- details.perTokenFamily[tok.tokenIssuePubHash].untrusted += 1;
- return false; // non-usable
- default:
- assertUnreachable(res);
- }
- }).sort((a, b) => a.validBefore - b.validBefore);
-
- if (usable.length < count) {
- insufficient = true;
- continue;
- }
+ details.tokensAvailable += details.perTokenFamily[keyHash].available;
- details.tokensAvailable += count;
- records.push(...usable.slice(0, count));
+ if (usable.length < count) {
+ insufficient = true;
+ continue;
}
+
+ records.push(...usable.slice(0, count));
}
if (insufficient) {