taler-typescript-core

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

commit ecacdafcf6746189d8395786b684e662f053de07
parent cfa8d17b80d4e5bdb87f02e95cccff1f3003a700
Author: Iván Ávalos <avalos@disroot.org>
Date:   Sun,  6 Jul 2025 16:17:56 +0200

wallet-core: tokens code cleanup + tokenIssuePubHash -> slug

Diffstat:
Mpackages/taler-util/src/types-taler-wallet.ts | 2+-
Mpackages/taler-wallet-core/src/pay-merchant.ts | 14++++++++------
Mpackages/taler-wallet-core/src/tokenSelection.ts | 82++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
3 files changed, 55 insertions(+), 43 deletions(-)

diff --git a/packages/taler-util/src/types-taler-wallet.ts b/packages/taler-util/src/types-taler-wallet.ts @@ -913,7 +913,7 @@ export interface PaymentTokenAvailabilityDetails { tokensUntrusted: number; perTokenFamily: { - [tokenIssuePubHash: string]: { + [slug: string]: { requested: number; available: number; unexpected: number; diff --git a/packages/taler-wallet-core/src/pay-merchant.ts b/packages/taler-wallet-core/src/pay-merchant.ts @@ -2792,7 +2792,7 @@ export async function confirmPay( return; } - let selectTokensResult: SelectPayTokensResult | undefined = undefined; + let selectTokensResult: SelectPayTokensResult | undefined; if (contractData.version === MerchantContractVersion.V1) { selectTokensResult = await selectPayTokensInTx(tx, { @@ -2872,11 +2872,11 @@ export async function confirmPay( p.payInfo = { totalPayCost: Amounts.stringify(payCostInfo), }; - let tokenPubs: string[] | undefined = undefined; if (selectTokensResult?.type === "success") { const tokens = selectTokensResult.tokens; - tokenPubs = tokens.map((t) => t.tokenUsePub); - p.payInfo.payTokenSelection = { tokenPubs }; + p.payInfo.payTokenSelection = { + tokenPubs: tokens.map(t => t.tokenUsePub), + }; } if (selectCoinsResult.type === "success") { p.payInfo.payCoinSelection = { @@ -2892,9 +2892,9 @@ export async function confirmPay( p.purchaseStatus = PurchaseStatus.PendingPaying; await tx.purchases.put(p); await ctx.updateTransactionMeta(tx); - if (tokenPubs) { + if (p.payInfo.payTokenSelection) { await spendTokens(tx, { - tokenPubs, + tokenPubs: p.payInfo.payTokenSelection.tokenPubs, transactionId: ctx.transactionId, }); } @@ -3402,6 +3402,7 @@ async function processPurchasePay( }); } + // store token outputs if (slates && tokenSigs) { for (let i = 0; i < slates.length; i++) { const slate = slates[i]; @@ -3410,6 +3411,7 @@ async function processPurchasePay( } } + // cleanup token inputs if (payInfo.payTokenSelection?.tokenPubs) { await cleanupUsedTokens(wex, payInfo.payTokenSelection.tokenPubs); } diff --git a/packages/taler-wallet-core/src/tokenSelection.ts b/packages/taler-wallet-core/src/tokenSelection.ts @@ -176,33 +176,43 @@ export async function selectPayTokensInTx( throw Error(`proposal ${req.proposalId} could not be found`); } - const inputs = req.contractTerms.choices[req.choiceIndex].inputs; - const inputTokens: {[tokenIssuePubHash: string]: TokenRecord[]} = {}; - const inputCounts: {[tokenIssuePubHash: string]: number} = {}; var tokensRequested = 0; + const inputTokens: {[slug: string]: { + records: TokenRecord[], + requested: number, + }} = {}; - 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; + const inputs = req.contractTerms.choices[req.choiceIndex].inputs; + const tokenIssuePubs: string[] = []; + + for (const slug in req.contractTerms.token_families) { + const requested = inputs + .filter(i => i.type === MerchantContractInputType.Token) + .filter(i => i.token_family_slug === slug) + .reduce((a, b) => a + (b.count ?? 1), 0); + if (requested > 0) { + tokensRequested += requested; + inputTokens[slug] = {records: [], requested}; + const keys = req.contractTerms.token_families[slug].keys; for (const key of keys) { const keyHash = encodeCrock(hashTokenIssuePub(key)); - const t = await tx.tokens.indexes.byTokenIssuePubHash.getAll(keyHash); - if (inputTokens[keyHash] === undefined) { - logger.trace(`found total of ${t.length} tokens for token family ${keyHash}`); - inputCounts[keyHash] = count; - inputTokens[keyHash] = t; + if (!tokenIssuePubs.includes(keyHash)) { + tokenIssuePubs.push(keyHash); + const t = await tx.tokens.indexes.byTokenIssuePubHash.getAll(keyHash); + inputTokens[slug].records.push(...t); } } + + logger.trace( + `found total of ${inputTokens[slug].records.length} records for token family ${slug}, ` + + `out of ${requested} requested` + ); } } return selectTokenCandidates( inputTokens, - inputCounts, tokensRequested, proposal.merchantBaseUrl, ); @@ -226,8 +236,10 @@ export async function selectPayTokens( } export function selectTokenCandidates( - inputTokens: {[tokenIssuePubHash: string]: TokenRecord[]}, - inputCounts: {[tokenIssuePubHash: string]: number}, + inputTokens: {[slug: string]: { + records: TokenRecord[], + requested: number, + }}, tokensRequested: number, merchantBaseUrl: string, ): SelectPayTokensResult { @@ -240,14 +252,13 @@ export function selectTokenCandidates( }; var insufficient = false; - const records: TokenRecord[] = []; + const tokens: TokenRecord[] = []; - for (const keyHash in inputTokens) { - const tokens = inputTokens[keyHash]; - const count = inputCounts[keyHash]; + for (const slug in inputTokens) { + const {records, requested} = inputTokens[slug]; - details.perTokenFamily[keyHash] = { - requested: count, + details.perTokenFamily[slug] = { + requested, available: 0, unexpected: 0, untrusted: 0, @@ -259,7 +270,7 @@ export function selectTokenCandidates( // - filter out tokens with errors // - sort ascending by expiration date // - choose the first n tokens in the list - const usable = tokens.filter(tok => + const usable = records.filter(tok => !tok.transactionId && AbsoluteTime.isBetween( AbsoluteTime.now(), AbsoluteTime.fromProtocolTimestamp(timestampProtocolFromDb(tok.validAfter)), @@ -272,42 +283,41 @@ export function selectTokenCandidates( ); 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; + details.perTokenFamily[slug].unexpected += 1; return true; // usable case TokenMerchantVerificationResult.Untrusted: - details.tokensUntrusted += 1; - details.perTokenFamily[keyHash].untrusted += 1; + details.perTokenFamily[slug].untrusted += 1; return false; // non-usable default: assertUnreachable(res); } }).sort((a, b) => a.validBefore - b.validBefore); - details.tokensAvailable += details.perTokenFamily[keyHash].available; + details.perTokenFamily[slug].available = usable.length; + details.tokensAvailable += details.perTokenFamily[slug].available; + details.tokensUnexpected += details.perTokenFamily[slug].unexpected; + details.tokensUntrusted += details.perTokenFamily[slug].untrusted; - if (usable.length < count) { + if (usable.length < requested) { insufficient = true; continue; } - records.push(...usable.slice(0, count)); + tokens.push(...usable.slice(0, requested)); } if (insufficient) { return { type: "failure", - details: details, + details, }; } return { type: "success", - tokens: records, - details: details, + tokens, + details, }; }