taler-typescript-core

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

commit e22aa38398f2c1d84ca9960757543ad65c34721c
parent 0ebcc1c57c52596966729c68b7fe1c6136da27c8
Author: Florian Dold <florian@dold.me>
Date:   Mon,  1 Jun 2026 15:48:30 +0200

wallet-core: make expiration optional in initiatePeerPushDebit request

Diffstat:
Mpackages/taler-util/src/types-taler-exchange.ts | 16++++++++++++++++
Mpackages/taler-util/src/types-taler-wallet.ts | 6++++--
Mpackages/taler-wallet-core/src/pay-peer-push-debit.ts | 63+++++++++++++++++++++++++++++++++++++++++++++++++++------------
3 files changed, 71 insertions(+), 14 deletions(-)

diff --git a/packages/taler-util/src/types-taler-exchange.ts b/packages/taler-util/src/types-taler-exchange.ts @@ -1303,6 +1303,13 @@ export interface PeerContractTerms { purse_expiration: TalerProtocolTimestamp; } +export interface PartialPeerContractTerms { + amount: AmountString; + summary: string; + icon_id?: string; + purse_expiration?: TalerProtocolTimestamp; +} + export interface EncryptedContract { // Encrypted contract. econtract: string; @@ -1590,6 +1597,15 @@ export const codecForPeerContractTerms = (): Codec<PeerContractTerms> => .property("icon_id", codecOptional(codecForString())) .build("PeerContractTerms"); +export const codecForPartialPeerContractTerms = + (): Codec<PartialPeerContractTerms> => + buildCodecForObject<PartialPeerContractTerms>() + .property("summary", codecForString()) + .property("amount", codecForAmountString()) + .property("purse_expiration", codecOptional(codecForTimestamp)) + .property("icon_id", codecOptional(codecForString())) + .build("PartialPeerContractTerms"); + export interface ExchangeBatchDepositRequest { // The merchant's account details. merchant_payto_uri: string; diff --git a/packages/taler-util/src/types-taler-wallet.ts b/packages/taler-util/src/types-taler-wallet.ts @@ -86,9 +86,11 @@ import { ExchangeAuditor, ExchangeRefundRequest, ExchangeWireAccount, + PartialPeerContractTerms, PeerContractTerms, UnblindedDenominationSignature, codecForExchangeWireAccount, + codecForPartialPeerContractTerms, codecForPeerContractTerms, } from "./types-taler-exchange.js"; import { @@ -3496,7 +3498,7 @@ export interface InitiatePeerPushDebitRequest { */ restrictScope?: ScopeInfo; - partialContractTerms: PeerContractTerms; + partialContractTerms: PartialPeerContractTerms; } export interface InitiatePeerPushDebitResponse { @@ -3510,7 +3512,7 @@ export interface InitiatePeerPushDebitResponse { export const codecForInitiatePeerPushDebitRequest = (): Codec<InitiatePeerPushDebitRequest> => buildCodecForObject<InitiatePeerPushDebitRequest>() - .property("partialContractTerms", codecForPeerContractTerms()) + .property("partialContractTerms", codecForPartialPeerContractTerms()) .property("exchangeBaseUrl", codecOptional(codecForCanonBaseUrl())) .property("restrictScope", codecOptional(codecForScopeInfo())) .build("InitiatePeerPushDebitRequest"); diff --git a/packages/taler-wallet-core/src/pay-peer-push-debit.ts b/packages/taler-wallet-core/src/pay-peer-push-debit.ts @@ -15,6 +15,7 @@ */ import { + AbsoluteTime, Amounts, CheckPeerPushDebitOkResponse, CheckPeerPushDebitRequest, @@ -28,6 +29,7 @@ import { InitiatePeerPushDebitRequest, InitiatePeerPushDebitResponse, Logger, + PeerContractTerms, PurseConflict, RefreshReason, ScopeInfo, @@ -410,14 +412,17 @@ export async function checkPeerPushDebitV2( ); } +const fallbackDefaultPeerPushExpiration = Duration.toTalerProtocolDuration( + Duration.fromSpec({ days: 7 }), +); + async function getDefaultPeerPushExpiration( wex: WalletExecutionContext, exchangeBaseUrl: string, ): Promise<TalerProtocolDuration> { - const d = Duration.toTalerProtocolDuration(Duration.fromSpec({ days: 7 })); return await wex.runLegacyWalletDbTx(async (tx) => { const ex = await getExchangeDetailsInTx(tx, exchangeBaseUrl); - return ex?.defaultPeerPushExpiration ?? d; + return ex?.defaultPeerPushExpiration ?? fallbackDefaultPeerPushExpiration; }); } @@ -1026,8 +1031,23 @@ export async function initiatePeerPushDebit( const instructedAmount = Amounts.parseOrThrow( req.partialContractTerms.amount, ); - const purseExpiration = req.partialContractTerms.purse_expiration; - const contractTerms = { ...req.partialContractTerms }; + const currency = Amounts.currencyOf(instructedAmount); + + if (req.exchangeBaseUrl != null && req.restrictScope != null) { + throw Error( + "initiatePeerPushDebit: exchangeBaseUrl and restrictScope are mutually exclusive", + ); + } + + let restrictScope: ScopeInfo | undefined; + + if (req.exchangeBaseUrl != null) { + restrictScope = { + type: ScopeType.Exchange, + currency, + url: req.exchangeBaseUrl, + }; + } const pursePair = await wex.cryptoApi.createEddsaKeypair({}); const mergePair = await wex.cryptoApi.createEddsaKeypair({}); @@ -1040,17 +1060,14 @@ export async function initiatePeerPushDebit( const contractEncNonce = encodeCrock(getRandomBytes(24)); - const hContractTerms = ContractTermsUtil.hashContractTerms(contractTerms); - await updateWithdrawalDenomsForCurrency(wex, instructedAmount.currency); - let exchangeBaseUrl; + let exchangeBaseUrl: string | undefined; await wex.runLegacyWalletDbTx(async (tx) => { const coinSelRes = await selectPeerCoinsInTx(wex, tx, { instructedAmount, - // Any (single!) exchange that is in scope works. - restrictScope: req.restrictScope, + restrictScope: restrictScope, feesCoveredByCounterparty: false, }); @@ -1083,13 +1100,36 @@ export async function initiatePeerPushDebit( ); logger.trace( `peer debit contract terms amount: ${Amounts.stringify( - contractTerms.amount, + req.partialContractTerms.amount, )}`, ); logger.trace( `peer debit deposit fees: ${Amounts.stringify(sel.totalDepositFees)}`, ); + exchangeBaseUrl = coinSelRes.result.exchangeBaseUrl; + + const ex = await getExchangeDetailsInTx(tx, exchangeBaseUrl); + const myExpiration = + req.partialContractTerms.purse_expiration ?? + AbsoluteTime.toProtocolTimestamp( + AbsoluteTime.addDuration( + AbsoluteTime.now(), + Duration.fromTalerProtocolDuration( + ex?.defaultPeerPushExpiration ?? fallbackDefaultPeerPushExpiration, + ), + ), + ); + + const contractTerms: PeerContractTerms = { + ...req.partialContractTerms, + purse_expiration: myExpiration, + }; + + // We can only compute the contract terms after we've set + // the expiration. + const hContractTerms = ContractTermsUtil.hashContractTerms(contractTerms); + const totalAmount = await getTotalPeerPaymentCostInTx(wex, tx, coins); const ppi: PeerPushDebitRecord = { amount: Amounts.stringify(instructedAmount), @@ -1100,7 +1140,7 @@ export async function initiatePeerPushDebit( exchangeBaseUrl: sel.exchangeBaseUrl, mergePriv: mergePair.priv, mergePub: mergePair.pub, - purseExpiration: timestampProtocolToDb(purseExpiration), + purseExpiration: timestampProtocolToDb(myExpiration), pursePriv: pursePair.priv, pursePub: pursePair.pub, timestampCreated: timestampPreciseToDb(TalerPreciseTimestamp.now()), @@ -1130,7 +1170,6 @@ export async function initiatePeerPushDebit( h: hContractTerms, contractTermsRaw: contractTerms, }); - exchangeBaseUrl = coinSelRes.result.exchangeBaseUrl; const [oldRec, h] = await ctx.getRecordHandle(tx); if (oldRec) { throw Error("record for peer-push-debit already exists");