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:
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),