taler-typescript-core

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

commit 1fa6532137d4c4666baf0def8f5b8dfa88cae99b
parent d7ab425239226b798a95dbf6742fe4b4a2e30e47
Author: Florian Dold <florian@dold.me>
Date:   Tue,  7 Jul 2026 16:39:01 +0200

wallet-core: support output tokenss with count>1

Diffstat:
Mpackages/taler-harness/src/integrationtests/test-wallet-tokens.ts | 86+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Mpackages/taler-util/src/types-taler-wallet.ts | 1+
Mpackages/taler-wallet-core/src/crypto/cryptoImplementation.ts | 3++-
Mpackages/taler-wallet-core/src/db.ts | 18+++++++++++++++++-
Mpackages/taler-wallet-core/src/pay-merchant.ts | 54++++++++++++++++++++++++++++++++++--------------------
5 files changed, 136 insertions(+), 26 deletions(-)

diff --git a/packages/taler-harness/src/integrationtests/test-wallet-tokens.ts b/packages/taler-harness/src/integrationtests/test-wallet-tokens.ts @@ -21,6 +21,7 @@ import { AbsoluteTime, ChoiceSelectionDetailType, Duration, + j2s, Order, OrderInputType, OrderOutputType, @@ -30,6 +31,7 @@ import { TalerProtocolTimestamp, TokenAvailabilityHint, TokenFamilyKind, + TransactionMajorState, } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { defaultCoinConfig } from "../harness/denomStructures.js"; @@ -52,8 +54,7 @@ export async function runWalletTokensTest(t: GlobalTestState) { defaultCoinConfig.map((x) => x("TESTKUDOS")), { walletConfig: { - features: { - }, + features: {}, }, }, ); @@ -117,7 +118,7 @@ export async function runWalletTokensTest(t: GlobalTestState) { }), ); - let orderJsonDiscount: Order = { + const orderJsonDiscount: Order = { version: 1, summary: "Test order", timestamp: TalerProtocolTimestamp.now(), @@ -148,6 +149,18 @@ export async function runWalletTokensTest(t: GlobalTestState) { ], outputs: [], }, + // Multiple outputs + { + amount: "TESTKUDOS:3", + inputs: [], + outputs: [ + { + count: 5, + type: OrderOutputType.Token, + token_family_slug: "test_discount", + }, + ], + }, ], }; @@ -279,7 +292,9 @@ export async function runWalletTokensTest(t: GlobalTestState) { }, ); - t.assertTrue(choicesRes.defaultChoiceIndex === 1); + console.log(j2s(choicesRes)); + + t.assertDeepEqual(choicesRes.defaultChoiceIndex, 1); t.assertTrue(choicesRes.automaticExecution === false); t.assertTrue(choicesRes.automaticExecutableIndex === undefined); @@ -574,6 +589,69 @@ export async function runWalletTokensTest(t: GlobalTestState) { await merchantApi.getOrderDetails(merchantAdminAccessToken, orderId), ); } + + // Multiple inputs/outputs for discount tokens. + { + const orderResp = succeedOrThrow( + await merchantApi.createOrder(merchantAdminAccessToken, { + order: orderJsonDiscount, + }), + ); + + let orderStatus = succeedOrThrow( + await merchantApi.getOrderDetails( + merchantAdminAccessToken, + orderResp.order_id, + ), + ); + + t.assertTrue(orderStatus.order_status === "unpaid"); + + const talerPayUri = orderStatus.taler_pay_uri; + + const preparePayResult = await walletClient.call( + WalletApiOperation.PreparePayForUriV2, + { + talerPayUri, + }, + ); + + await walletClient.call(WalletApiOperation.TestingWaitTransactionState, { + transactionId: preparePayResult.transactionId, + txState: { + major: TransactionMajorState.Dialog, + minor: "*", + }, + }); + + const tokBefore = await walletClient.call( + WalletApiOperation.ListDiscounts, + {}, + ); + + console.log(`tokens before:`, j2s(tokBefore)); + + await walletClient.call(WalletApiOperation.ConfirmPay, { + transactionId: preparePayResult.transactionId, + choiceIndex: 2, + }); + + await walletClient.call(WalletApiOperation.TestingWaitTransactionState, { + transactionId: preparePayResult.transactionId, + txState: { + major: TransactionMajorState.Done, + }, + }); + + const tokAfter = await walletClient.call( + WalletApiOperation.ListDiscounts, + {}, + ); + + console.log(`tokens after:`, j2s(tokAfter)); + + t.assertDeepEqual(tokAfter.discounts[0].tokensAvailable, 5); + } } runWalletTokensTest.suites = ["merchant", "wallet"]; diff --git a/packages/taler-util/src/types-taler-wallet.ts b/packages/taler-util/src/types-taler-wallet.ts @@ -1294,6 +1294,7 @@ export interface SlateCreationRequest { secretSeed: string; choiceIndex: number; outputIndex: number; + repeatIndex: number; tokenIssuePub: TokenIssuePublicKey; genTokenUseSig: boolean; contractTerms: MerchantContractTermsV1; diff --git a/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts b/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts @@ -1031,10 +1031,11 @@ export const nativeCryptoR: TalerCryptoInterfaceR = { // token generation const info = stringToBytes("taler-payment-token-derivation"); - const saltArrBuf = new ArrayBuffer(4); + const saltArrBuf = new ArrayBuffer(8); const salt = new Uint8Array(saltArrBuf); const saltDataView = new DataView(saltArrBuf); saltDataView.setUint32(0, req.outputIndex); + saltDataView.setUint32(4, req.repeatIndex); const secretSeedDec = decodeCrock(req.secretSeed); const out = kdf(64, secretSeedDec, salt, info); const tokenPriv = out.slice(0, 32); diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts @@ -172,7 +172,7 @@ export const CURRENT_DB_CONFIG_KEY = "currentMainDbName"; * backwards-compatible way or object stores and indices * are added. */ -export const WALLET_DB_MINOR_VERSION = 28; +export const WALLET_DB_MINOR_VERSION = 29; declare const symDbProtocolTimestamp: unique symbol; @@ -1090,6 +1090,15 @@ export interface TokenRecord extends TokenFamilyInfo { outputIndex?: number; /** + * For token outputs with a count>1, this stores + * the index of this token within the same output + * index. + * + * If missing, assumed to be 0. + */ + repeatIndex?: number; + + /** * URL of the merchant issuing the token. */ merchantBaseUrl: string; @@ -3124,6 +3133,13 @@ export const WalletIndexedDbStoresV1 = { versionAdded: 17, }, ), + byPurchaseIdAndChoiceIndexAndOutputIndexAndRepeatIndex: describeIndex( + "byPurchaseIdAndChoiceIndexAndOutputIndexAndRepeatIndex", + ["purchaseId", "choiceIndex", "outputIndex", "repeatIndex"], + { + versionAdded: 29, + }, + ), }, ), reserves: describeStore( diff --git a/packages/taler-wallet-core/src/pay-merchant.ts b/packages/taler-wallet-core/src/pay-merchant.ts @@ -1254,6 +1254,7 @@ async function generateSlate( contractTermsRaw: any, choiceIndex: number, outputIndex: number, + repeatIndex: number, ): Promise<void> { checkDbInvariant( purchase.secretSeed !== undefined, @@ -1261,12 +1262,12 @@ async function generateSlate( ); logger.trace( - `generating slate (${choiceIndex}, ${outputIndex}) for purchase ${purchase.proposalId}`, + `generating slate (${choiceIndex}, ${outputIndex}, ${repeatIndex}) for purchase ${purchase.proposalId}`, ); let slate = await wex.runLegacyWalletDbTx(async (tx) => { - return await tx.slates.indexes.byPurchaseIdAndChoiceIndexAndOutputIndex.get( - [purchase.proposalId, choiceIndex, outputIndex], + return await tx.slates.indexes.byPurchaseIdAndChoiceIndexAndOutputIndexAndRepeatIndex.get( + [purchase.proposalId, choiceIndex, outputIndex, repeatIndex], ); }); @@ -1290,9 +1291,10 @@ async function generateSlate( genTokenUseSig: true, contractTerms: contractData, contractTermsHash: ContractTermsUtil.hashContractTerms(contractTermsRaw), + repeatIndex, }); - let newSlate: SlateRecord = { + const newSlate: SlateRecord = { purchaseId: purchase.proposalId, choiceIndex: choiceIndex, outputIndex: outputIndex, @@ -1312,17 +1314,16 @@ async function generateSlate( tokenEv: r.tokenEv, tokenEvHash: r.tokenEvHash, blindingKey: r.blindingKey, + repeatIndex, }; newSlate.tokenFamilyHash = TokenRecord.hashInfo(newSlate); await wex.runLegacyWalletDbTx(async (tx) => { const s = - await tx.slates.indexes.byPurchaseIdAndChoiceIndexAndOutputIndex.get([ - purchase.proposalId, - choiceIndex, - outputIndex, - ]); + await tx.slates.indexes.byPurchaseIdAndChoiceIndexAndOutputIndexAndRepeatIndex.get( + [purchase.proposalId, choiceIndex, outputIndex, repeatIndex], + ); if (s) { return; } @@ -2973,14 +2974,19 @@ export async function confirmPay( const tok = choice.outputs[j]; switch (tok.type) { case MerchantContractOutputType.Token: { - await generateSlate( - wex, - proposal, - contractTerms, - d.contractTermsRaw, - choiceIndex, - j, - ); + const tokCount = tok.count ?? 1; + for (let repeatIndex = 0; repeatIndex < tokCount; repeatIndex++) { + await generateSlate( + wex, + proposal, + contractTerms, + d.contractTermsRaw, + choiceIndex, + j, + repeatIndex, + ); + } + break; } case MerchantContractOutputType.TaxReceipt: @@ -3253,12 +3259,20 @@ async function processPurchasePay( slates = []; wallet_data = { choice_index: index, tokens_evs: [] }; await wex.runLegacyWalletDbTx(async (tx) => { - ( + const allSlateRes = await tx.slates.getAll(); + logger.info(`all slates: ${j2s(allSlateRes)}`); + const slateRes = await tx.slates.indexes.byPurchaseIdAndChoiceIndex.getAll([ purchase.proposalId, index, - ]) - ).forEach((s) => { + ]); + logger.info( + `got ${slateRes.length} slates from DB, req=${[ + purchase.proposalId, + index, + ]}`, + ); + slateRes.forEach((s) => { slates?.push(s); wallet_data?.tokens_evs.push(s.tokenEv); });