commit 3b11b38eaf6ae1f00a44681fea8739ad0b4f56b2
parent 92ca0147caab915327f9a678991530616cc3395c
Author: Florian Dold <dold@taler.net>
Date: Sat, 5 Sep 2026 16:37:20 +0200
wallet-core: keep pull initiation recoverable while offline
Once an invoice is persisted, return its transaction ID even if the
immediate purse creation attempt fails. Start background retries and
respect the network availability hint before attempting creation.
Assert offline initiation remains pending and exposes no unconfirmed URI.
Shorten the expiry test time jump so only purses expire, avoiding unrelated
denomination-loss warnings.
Validation: 875 core tests and peer-abort-balance, peer-pull,
peer-pull-debit-purse-gone and peer-repair all pass.
Issue: https://bugs.taler.net/n/7903
Diffstat:
2 files changed, 29 insertions(+), 6 deletions(-)
diff --git a/packages/taler-harness/src/integrationtests/test-peer-pull.ts b/packages/taler-harness/src/integrationtests/test-peer-pull.ts
@@ -33,10 +33,6 @@ import {
} from "../harness/environments.js";
import { GlobalTestState, WalletClient } from "../harness/harness.js";
-const purse_expiration = AbsoluteTime.toProtocolTimestamp(
- AbsoluteTime.addDuration(AbsoluteTime.now(), Duration.fromSpec({ days: 2 })),
-);
-
/**
* States a wallet ends up in when it loses the race for an invoice: the purse
* is gone by the time it deposits, which aborts the payment, and a deposit
@@ -55,6 +51,12 @@ const stLostRace = [
* Run a test for basic peer-pull payments.
*/
export async function runPeerPullTest(t: GlobalTestState) {
+ const purse_expiration = AbsoluteTime.toProtocolTimestamp(
+ AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ Duration.fromSpec({ minutes: 2 }),
+ ),
+ );
t.allowLog({
file: "wallet-wallet-stderr.log",
level: "WARNING",
@@ -476,6 +478,14 @@ export async function runPeerPullTest(t: GlobalTestState) {
},
},
);
+ // A failed immediate creation attempt must still return the saved
+ // transaction, but cannot expose a URI without an exchange confirmation.
+ t.assertTrue(initiate.talerUri === undefined);
+ const pending = await wallet1.call(WalletApiOperation.GetTransactionById, {
+ transactionId: initiate.transactionId,
+ });
+ t.assertDeepEqual(pending.txState.major, TransactionMajorState.Pending);
+ t.assertDeepEqual(pending.txState.minor, TransactionMinorState.CreatePurse);
await wallet1.call(WalletApiOperation.AbortTransaction, {
transactionId: initiate.transactionId,
});
@@ -524,6 +534,7 @@ export async function runPeerPullTest(t: GlobalTestState) {
},
},
);
+ t.assertTrue(expiresBeforeCreate.talerUri === undefined);
const suspendedBeforeCreate = await wallet1.call(
WalletApiOperation.InitiatePeerPullCredit,
{
@@ -547,8 +558,9 @@ export async function runPeerPullTest(t: GlobalTestState) {
timeout: { seconds: 10 },
});
+ // Expire the purses without also expiring the wallet's denominations.
const timetravelOffsetMs = Duration.toMilliseconds(
- Duration.fromSpec({ days: 5 }),
+ Duration.fromSpec({ minutes: 5 }),
);
await exchange.stop();
diff --git a/packages/taler-wallet-core/src/pay-peer-pull-credit.ts b/packages/taler-wallet-core/src/pay-peer-pull-credit.ts
@@ -53,6 +53,7 @@ import {
checkDbInvariant,
checkProtocolInvariant,
encodeCrock,
+ getErrorDetailFromException,
getRandomBytes,
j2s,
} from "@gnu-taler/taler-util";
@@ -1689,7 +1690,17 @@ async function internalInitiatePeerPullPayment(
tx.getPeerPullCredit(pursePair.pub),
);
checkDbInvariant(!!initialRecord, "new peer pull credit is missing");
- await processPeerPullCreditCreatePurse(wex, initialRecord);
+ if (wex.ws.networkAvailable) {
+ try {
+ await processPeerPullCreditCreatePurse(wex, initialRecord);
+ } catch (e) {
+ // The transaction is already persisted. Return its ID so the caller can
+ // follow or abort it; the shepherd will record errors and retry creation.
+ logger.info(
+ `Deferring initial pull purse creation: ${j2s(getErrorDetailFromException(e))}`,
+ );
+ }
+ }
wex.taskScheduler.startShepherdTask(ctx.taskId);
const currentRecord = await wex.runWalletDbTx((tx) =>
tx.getPeerPullCredit(pursePair.pub),