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:
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");