taler-typescript-core

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

commit 6943d692a549fa4a14684292be84393616bf2e0b
parent 45a0bbb0118051f70b95f6b8d49fd934c0eda2d1
Author: Florian Dold <dold@taler.net>
Date:   Mon, 28 Sep 2026 17:40:04 +0200

wallet-core: restore short peer-pull payment URIs

The `GET /purses/$PURSE_PUB/merge` request currently doesn't return the
target amount for invoices, and this can cause confusion when the amount
differs from the contract terms.

This commit reverts a (slightly misguided) attempt to fix this by
including signed information about the amount in the peer-pull URI.

Issue: https://bugs.taler.net/n/11828

Diffstat:
Mpackages/taler-harness/src/integrationtests/test-kyc-peer-pull.ts | 3++-
Mpackages/taler-harness/src/integrationtests/test-peer-pull.ts | 5+++++
Mpackages/taler-util/src/taleruri.test.ts | 45+++++++++++++++++++++------------------------
Mpackages/taler-util/src/taleruri.ts | 61++-----------------------------------------------------------
Mpackages/taler-wallet-core/src/db/indexeddb/schema.ts | 3---
Mpackages/taler-wallet-core/src/db/indexeddb/transaction.ts | 2--
Mpackages/taler-wallet-core/src/db/records.ts | 4----
Mpackages/taler-wallet-core/src/db/sqlite/transaction.ts | 14+++-----------
Mpackages/taler-wallet-core/src/db/testing/conformance-cases.ts | 6------
Mpackages/taler-wallet-core/src/pay-peer-pull-credit.ts | 17++++-------------
Mpackages/taler-wallet-core/src/pay-peer-pull-debit.ts | 25+++----------------------
11 files changed, 40 insertions(+), 145 deletions(-)

diff --git a/packages/taler-harness/src/integrationtests/test-kyc-peer-pull.ts b/packages/taler-harness/src/integrationtests/test-kyc-peer-pull.ts @@ -108,7 +108,7 @@ export async function runKycPeerPullTest(t: GlobalTestState) { }, }); - // Purse creation after KYC adds the exchange commitment to the invoice URI. + // The invoice can be paid after KYC has allowed purse creation to finish. const readyPull = await walletClient.call( WalletApiOperation.GetTransactionById, { @@ -117,6 +117,7 @@ export async function runKycPeerPullTest(t: GlobalTestState) { ); t.assertTrue(readyPull.type === TransactionType.PeerPullCredit); t.assertTrue(!!readyPull.talerUri); + t.assertTrue(!readyPull.talerUri.includes("?")); const prepRes = await w0.walletClient.call( WalletApiOperation.PreparePeerPullDebit, { diff --git a/packages/taler-harness/src/integrationtests/test-peer-pull.ts b/packages/taler-harness/src/integrationtests/test-peer-pull.ts @@ -125,6 +125,11 @@ export async function runPeerPullTest(t: GlobalTestState) { transactionId: initiate.transactionId, }); t.assertDeepEqual(tx.type, TransactionType.PeerPullCredit); + t.assertTrue(!!tx.talerUri); + t.assertTrue(!tx.talerUri.includes("?")); + if (initiate.talerUri !== undefined) { + t.assertDeepEqual(initiate.talerUri, tx.talerUri); + } return tx; } diff --git a/packages/taler-util/src/taleruri.test.ts b/packages/taler-util/src/taleruri.test.ts @@ -423,30 +423,27 @@ const VALID_MAILBOX_KEY = "1".repeat(52) as EddsaPublicKeyString; ); }); - test("pull URI round-trips the exchange purse commitment", () => { - const purseCreateProof = { - exchangePub: "1".repeat(52), - exchangeSig: "2".repeat(103), - exchangeTimestamp: { t_s: 123 }, - totalDeposited: "KUDOS:0" as AmountString, - }; - const url = TalerUris.stringify({ - type: TalerUriAction.PayPull, - exchangeBaseUrl: "https://foo.example.com/" as HostPortPath, - contractPriv: VALID_PRIVATE_KEY, - purseCreateProof, - }); - const parsed = Result.orUndefined( - TalerUris.parseRestricted(url, TalerUriAction.PayPull), - ); - assert.deepStrictEqual(parsed?.purseCreateProof, purseCreateProof); - }); - - test("pull URI rejects a partial purse commitment", () => { - const url = `taler://pay-pull/exch.example.com/${VALID_PRIVATE_KEY}?exchange_pub=${"1".repeat(52)}`; - assert.ok( - Result.isError(TalerUris.parseRestricted(url, TalerUriAction.PayPull)), - ); + test("pull URI ignores former proof parameters and serializes a short URI", () => { + const shortUri = `taler://pay-pull/exch.example.com/${VALID_PRIVATE_KEY}`; + for (const query of [ + `exchange_pub=${"1".repeat(52)}&exchange_sig=${"2".repeat(103)}&exchange_timestamp=123&total_deposited=KUDOS%3A0`, + `exchange_pub=${"1".repeat(52)}`, + "exchange_pub=invalid&exchange_sig=invalid&exchange_timestamp=invalid&total_deposited=invalid", + ]) { + const parsed = Result.orUndefined( + TalerUris.parseRestricted( + `${shortUri}?${query}`, + TalerUriAction.PayPull, + ), + ); + assert.ok(parsed); + assert.deepStrictEqual(parsed, { + type: TalerUriAction.PayPull, + exchangeBaseUrl: "https://exch.example.com/", + contractPriv: VALID_PRIVATE_KEY, + }); + assert.strictEqual(TalerUris.stringify(parsed), shortUri); + } }); /** diff --git a/packages/taler-util/src/taleruri.ts b/packages/taler-util/src/taleruri.ts @@ -33,9 +33,7 @@ import { AmountString, EddsaPrivateKeyString, EddsaPublicKeyString, - EddsaSignatureString, } from "./types-taler-common.js"; -import { TalerProtocolTimestamp } from "./time.js"; import { URL, URLSearchParams } from "./url.js"; export enum TalerUriAction { @@ -296,19 +294,7 @@ export namespace TalerUris { }); return result; } - case TalerUriAction.PayPull: { - const proof = p.purseCreateProof; - if (proof) { - result.push(["exchange_pub", proof.exchangePub]); - result.push(["exchange_sig", proof.exchangeSig]); - result.push([ - "exchange_timestamp", - String(proof.exchangeTimestamp.t_s), - ]); - result.push(["total_deposited", proof.totalDeposited]); - } - return result; - } + case TalerUriAction.PayPull: case TalerUriAction.Refund: case TalerUriAction.PayPush: case TalerUriAction.Restore: @@ -757,7 +743,7 @@ function parsePayPull( scheme: "http" | "https", uriType: TalerUriAction.PayPull, cs: string[], - params: Record<string, string>, + _params: Record<string, string>, opts: TalerUris.PaytoParseOptions = {}, ): TalerUris.ParseResult { // check number of segments @@ -792,45 +778,10 @@ function parsePayPull( }); } - const proofFields = [ - params["exchange_pub"], - params["exchange_sig"], - params["exchange_timestamp"], - params["total_deposited"], - ]; - const proofFieldCount = proofFields.filter((x) => x !== undefined).length; - if ( - !opts.ignoreComponentError && - proofFieldCount !== 0 && - (proofFieldCount !== proofFields.length || - !isFixedSizeCrock(params["exchange_pub"], 32) || - !isFixedSizeCrock(params["exchange_sig"], 64) || - !Number.isSafeInteger(Number(params["exchange_timestamp"])) || - Number(params["exchange_timestamp"]) < 0 || - !Amounts.parse(params["total_deposited"])) - ) { - return Result.errorWithDetail(TalerUriParseError.INVALID_PARAMETER, { - uriType, - name: "purse creation proof", - }); - } - const purseCreateProof = - proofFieldCount === proofFields.length - ? { - exchangePub: params["exchange_pub"] as EddsaPublicKeyString, - exchangeSig: params["exchange_sig"] as EddsaSignatureString, - exchangeTimestamp: { - t_s: Number(params["exchange_timestamp"]), - }, - totalDeposited: params["total_deposited"] as AmountString, - } - : undefined; - return Result.of({ type: TalerUriAction.PayPull, exchangeBaseUrl: exchange ?? (cs[0] as HostPortPath), contractPriv: contractPriv as EddsaPrivateKeyString, - purseCreateProof, }); } @@ -1262,14 +1213,6 @@ export interface TalerPayPullUri { type: TalerUriAction.PayPull; exchangeBaseUrl: HostPortPath; contractPriv: EddsaPrivateKeyString; - - /** Exchange-authenticated purse metadata, required before paying. */ - purseCreateProof?: { - totalDeposited: AmountString; - exchangeTimestamp: TalerProtocolTimestamp; - exchangeSig: EddsaSignatureString; - exchangePub: EddsaPublicKeyString; - }; } export interface TalerDevExperimentUri { diff --git a/packages/taler-wallet-core/src/db/indexeddb/schema.ts b/packages/taler-wallet-core/src/db/indexeddb/schema.ts @@ -26,7 +26,6 @@ import { HashCodeString, MailboxConfiguration, MailboxMessageRecord, - PurseCreateSuccessResponse, ScopeInfo, TalerErrorDetail, } from "@gnu-taler/taler-util"; @@ -310,8 +309,6 @@ export interface PeerPullCreditRecord { mergeReserveRowId: number; - purseCreateProof?: PurseCreateSuccessResponse; - /** * Status of the peer pull payment initiation. */ diff --git a/packages/taler-wallet-core/src/db/indexeddb/transaction.ts b/packages/taler-wallet-core/src/db/indexeddb/transaction.ts @@ -1691,7 +1691,6 @@ export class IdbWalletTransaction implements WalletDbTransaction { contractEncNonce: r.contractEncNonce, mergeTimestamp: r.mergeTimestamp, mergeReserveRowId: r.mergeReserveRowId, - purseCreateProof: r.purseCreateProof, status: r.status, kycPaytoHash: r.kycPaytoHash, kycAccessToken: r.kycAccessToken, @@ -1725,7 +1724,6 @@ export class IdbWalletTransaction implements WalletDbTransaction { contractEncNonce: rec.contractEncNonce, mergeTimestamp: rec.mergeTimestamp, mergeReserveRowId: rec.mergeReserveRowId, - purseCreateProof: rec.purseCreateProof, status: rec.status, kycPaytoHash: rec.kycPaytoHash, kycAccessToken: rec.kycAccessToken, diff --git a/packages/taler-wallet-core/src/db/records.ts b/packages/taler-wallet-core/src/db/records.ts @@ -59,7 +59,6 @@ import { hash, stringToBytes, canonicalJson, - PurseCreateSuccessResponse, } from "@gnu-taler/taler-util"; import { DbPreciseTimestamp, @@ -2619,9 +2618,6 @@ export interface WalletPeerPullCredit { mergeReserveRowId: number; - /** Exchange proof binding this invoice's purse metadata. */ - purseCreateProof?: PurseCreateSuccessResponse; - /** * Status of the peer pull payment initiation. */ diff --git a/packages/taler-wallet-core/src/db/sqlite/transaction.ts b/packages/taler-wallet-core/src/db/sqlite/transaction.ts @@ -3441,9 +3441,6 @@ export class SqliteWalletTransaction implements WalletDbTransaction { contractEncNonce: dbToCrock(row.contract_enc_nonce), mergeTimestamp: dbTimestamp(row.merge_timestamp), mergeReserveRowId: num(row.merge_reserve_row_id), - ...(row.purse_create_proof != null - ? { purseCreateProof: dbToJson(row.purse_create_proof) } - : undefined), status: num(row.status), withdrawalGroupId: optStr(row.withdrawal_group_id), ...(row.kyc_payto_hash != null @@ -3495,11 +3492,11 @@ export class SqliteWalletTransaction implements WalletDbTransaction { merge_reserve_row_id, status, kyc_payto_hash, kyc_access_token, kyc_last_check_status, kyc_last_check_code, kyc_last_rule_gen, kyc_last_aml_review, kyc_last_deny, abort_reason, fail_reason, - withdrawal_group_id, purse_create_proof + withdrawal_group_id ) VALUES ($transactionAmounts, $pub, $url, $amt, $eae, $ppriv, $cth, $mpub, $mpriv, $cpub, $cpriv, $nonce, $mts, $mrri, $status, $kph, $kat, $klcs, $klcc, - $klrg, $klar, $kld, $abort, $fail, $wgid, $pcp + $klrg, $klar, $kld, $abort, $fail, $wgid ) ON CONFLICT(purse_pub) DO UPDATE SET transaction_amounts = excluded.transaction_amounts, @@ -3525,8 +3522,7 @@ export class SqliteWalletTransaction implements WalletDbTransaction { kyc_last_deny = excluded.kyc_last_deny, abort_reason = excluded.abort_reason, fail_reason = excluded.fail_reason, - withdrawal_group_id = excluded.withdrawal_group_id, - purse_create_proof = excluded.purse_create_proof`, + withdrawal_group_id = excluded.withdrawal_group_id`, { transactionAmounts: rec.transactionAmounts === undefined @@ -3556,10 +3552,6 @@ export class SqliteWalletTransaction implements WalletDbTransaction { abort: rec.abortReason === undefined ? null : jsonToDb(rec.abortReason), fail: rec.failReason === undefined ? null : jsonToDb(rec.failReason), wgid: rec.withdrawalGroupId ?? null, - pcp: - rec.purseCreateProof === undefined - ? null - : jsonToDb(rec.purseCreateProof), }, ); } diff --git a/packages/taler-wallet-core/src/db/testing/conformance-cases.ts b/packages/taler-wallet-core/src/db/testing/conformance-cases.ts @@ -729,12 +729,6 @@ function makePeerPullCredit(pursePub: string): WalletPeerPullCredit { contractEncNonce: ck("nonce"), mergeTimestamp: tsPrecise(1000), mergeReserveRowId: 1, - purseCreateProof: { - total_deposited: amt("TESTKUDOS:0"), - exchange_timestamp: { t_s: 1001 }, - exchange_pub: ck("purse-proof-pub"), - exchange_sig: ckOfSize("purse-proof-sig", 64), - }, status: PeerPullPaymentCreditStatus.PendingCreatePurse, withdrawalGroupId: undefined, }; diff --git a/packages/taler-wallet-core/src/pay-peer-pull-credit.ts b/packages/taler-wallet-core/src/pay-peer-pull-credit.ts @@ -127,19 +127,10 @@ import { const logger = new Logger("pay-peer-pull-credit.ts"); function makePeerPullPaymentUri(rec: WalletPeerPullCredit): string { - const proof = rec.purseCreateProof; return TalerUris.stringify({ type: TalerUriAction.PayPull, exchangeBaseUrl: rec.exchangeBaseUrl as HostPortPath, contractPriv: rec.contractPriv, - purseCreateProof: proof - ? { - totalDeposited: proof.total_deposited, - exchangeTimestamp: proof.exchange_timestamp, - exchangeSig: proof.exchange_sig, - exchangePub: proof.exchange_pub, - } - : undefined, }); } @@ -1273,7 +1264,6 @@ async function processPeerPullCreditCreatePurse( return; } const nextStatus = statusAfterPullCreditCreate(rec.status); - rec.purseCreateProof = resp.body; rec.status = nextStatus; await h.update( rec, @@ -1737,9 +1727,10 @@ async function internalInitiatePeerPullPayment( ); return { - talerUri: currentRecord?.purseCreateProof - ? makePeerPullPaymentUri(currentRecord) - : undefined, + talerUri: + currentRecord?.status === PeerPullPaymentCreditStatus.PendingReady + ? makePeerPullPaymentUri(currentRecord) + : undefined, transactionId: ctx.transactionId, }; } diff --git a/packages/taler-wallet-core/src/pay-peer-pull-debit.ts b/packages/taler-wallet-core/src/pay-peer-pull-debit.ts @@ -118,7 +118,6 @@ import { import { WalletExecutionContext, walletExchangeClient } from "./wallet.js"; import { WalletDbTransaction } from "./db/transaction.js"; import { - requireValidExchangePurseCreateConfirmation, requireValidExchangePurseDepositConfirmation, requireValidExchangePurseStatus, } from "./exchange-signatures.js"; @@ -2015,27 +2014,9 @@ async function internalPreparePeerPullDebit( } const contractTermsHash = ContractTermsUtil.hashContractTerms(contractTerms); - const purseCreateProof = uri.purseCreateProof; - if (!purseCreateProof) { - throw TalerError.fromDetail( - TalerErrorCode.WALLET_CONTRACT_TERMS_UNSUPPORTED, - {}, - "the pull-payment invoice lacks an exchange-authenticated purse commitment", - ); - } - await requireValidExchangePurseCreateConfirmation(wex, { - exchangeBaseUrl, - pursePub, - contractTermsHash, - purseValueAfterFees: contractTerms.amount, - purseExpiration: contractTerms.purse_expiration, - response: { - total_deposited: purseCreateProof.totalDeposited, - exchange_timestamp: purseCreateProof.exchangeTimestamp, - exchange_sig: purseCreateProof.exchangeSig, - exchange_pub: purseCreateProof.exchangePub, - }, - }); + + // FIXME: Authenticate the target and contract hash before spending once the + // exchange supplies signed purse metadata on GET. See issue #11828. const resp = await runWithProgressRetries(wex, () => exchangeClient.getPurseStatusAtMerge(pursePub),