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