commit be373f904cbbd83e309181b2ec1379926da40e01
parent cb66e904aa520cc71da99ba0aa2906a935151a9f
Author: Florian Dold <dold@taler.net>
Date: Thu, 20 Aug 2026 19:06:43 +0200
wallet-core: select slate outputs from the payment choice
Diffstat:
2 files changed, 76 insertions(+), 25 deletions(-)
diff --git a/packages/taler-wallet-core/src/crypto/cryptoImplementation.test.ts b/packages/taler-wallet-core/src/crypto/cryptoImplementation.test.ts
@@ -22,12 +22,15 @@ import {
createEddsaKeyPair,
decodeCrock,
durationRoundedToBuffer,
+ DenomKeyType,
eddsaSign,
encodeCrock,
EddsaPublicKeyString,
EddsaSignatureString,
ExchangeSignKeyJson,
GlobalFees,
+ MerchantContractOutputType,
+ MerchantContractTermsV1,
TalerProtocolDuration,
TalerProtocolTimestamp,
TalerSignaturePurpose,
@@ -35,7 +38,10 @@ import {
} from "@gnu-taler/taler-util";
import assert from "node:assert";
import { test } from "node:test";
-import { nativeCryptoR } from "./cryptoImplementation.js";
+import {
+ collectSlateTokenEnvelopes,
+ nativeCryptoR,
+} from "./cryptoImplementation.js";
const master = createEddsaKeyPair();
const masterPub = encodeCrock(master.eddsaPub);
@@ -77,6 +83,44 @@ const validOver = {
key: encodeCrock(signKey.eddsaPub),
};
+test("slate creation reads outputs from the selected choice", () => {
+ const contractTerms = {
+ choices: [
+ {
+ outputs: [
+ {
+ type: MerchantContractOutputType.Token,
+ token_family_slug: "first",
+ },
+ ],
+ },
+ {
+ outputs: [
+ {
+ type: MerchantContractOutputType.Token,
+ token_family_slug: "second",
+ },
+ ],
+ },
+ ],
+ token_families: {
+ first: {
+ keys: [{ cipher: DenomKeyType.Rsa, rsa_pub: "first-key" }],
+ },
+ second: {
+ keys: [{ cipher: DenomKeyType.Rsa, rsa_pub: "second-key" }],
+ },
+ },
+ } as unknown as MerchantContractTermsV1;
+
+ assert.deepStrictEqual(collectSlateTokenEnvelopes(contractTerms, 1), [
+ {
+ cipher: DenomKeyType.Rsa,
+ rsa_blinded_planchet: "second-key",
+ },
+ ]);
+});
+
test("a correctly signed exchange signing key is accepted", async () => {
const res = await nativeCryptoR.isValidSignKey(nativeCryptoR, {
masterPub,
diff --git a/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts b/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts
@@ -83,6 +83,7 @@ import {
Logger,
MakeSyncSignatureRequest,
MerchantContractOutputType,
+ MerchantContractTermsV1,
PayWalletData,
PlanchetCreationRequest,
PlanchetUnblindInfo,
@@ -828,6 +829,32 @@ export interface DenominationValidationRequest {
masterSig: string;
}
+export function collectSlateTokenEnvelopes(
+ contractTerms: MerchantContractTermsV1,
+ choiceIndex: number,
+): TokenEnvelope[] {
+ const choice = contractTerms.choices[choiceIndex];
+ const tokenEvs: TokenEnvelope[] = [];
+ for (const output of choice.outputs) {
+ if (output.type !== MerchantContractOutputType.Token) {
+ continue;
+ }
+ const family = contractTerms.token_families[output.token_family_slug];
+ tokenEvs.push(
+ ...family.keys.map((key) => {
+ if (key.cipher !== DenomKeyType.Rsa) {
+ throw Error(`unsupported cipher (${key.cipher})`);
+ }
+ return {
+ cipher: DenomKeyType.Rsa,
+ rsa_blinded_planchet: key.rsa_pub,
+ } as TokenEnvelope;
+ }),
+ );
+ }
+ return tokenEvs;
+}
+
export interface PaymentSignatureValidationRequest {
sig: string;
contractHash: string;
@@ -1233,30 +1260,10 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
const tokenIssuePubHash = hashTokenIssuePub(req.tokenIssuePub);
const evHash = hashTokenEv(tokenEv, encodeCrock(tokenIssuePubHash));
- const choice = req.contractTerms.choices[req.outputIndex];
- const tokenEvs: TokenEnvelope[] = [];
- for (const output of choice.outputs) {
- if (output.type !== MerchantContractOutputType.Token) {
- continue;
- }
-
- const slug = output.token_family_slug;
- const family = req.contractTerms.token_families[slug];
- tokenEvs.push(
- ...family.keys.map((key) => {
- let tokenEv: TokenEnvelope;
- if (key.cipher === DenomKeyType.Rsa) {
- tokenEv = {
- cipher: DenomKeyType.Rsa,
- rsa_blinded_planchet: key.rsa_pub,
- };
- } else {
- throw Error(`unsupported cipher (${req.tokenIssuePub.cipher})`);
- }
- return tokenEv;
- }),
- );
- }
+ const tokenEvs = collectSlateTokenEnvelopes(
+ req.contractTerms,
+ req.choiceIndex,
+ );
// wallet data object with envelopes
const tokenWalletData: PayWalletData = {