commit 705c889c7b795ce921f7982d44c899e95e2fd1aa
parent 9529d1b1b0b4f8ae0f45de90de221f6ac9a4a180
Author: Florian Dold <florian@dold.me>
Date: Fri, 25 Apr 2025 13:46:05 +0200
wallet-core: fix regression that prevented p2p tx deletion
The deletion is now also tested in taler-harness.
Fixes https://bugs.taler.net/n/9796
Diffstat:
4 files changed, 171 insertions(+), 104 deletions(-)
diff --git a/packages/taler-harness/src/integrationtests/test-peer-push.ts b/packages/taler-harness/src/integrationtests/test-peer-push.ts
@@ -140,12 +140,17 @@ export async function runPeerPushTest(t: GlobalTestState) {
// FIXME propagate the error correctly
// t.assertTrue(ex1.errorDetail.code === TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE);
- const unknown_purse = await t.assertThrowsTalerErrorAsync(wallet1.call(
- WalletApiOperation.PreparePeerPushCredit,
- { talerUri: "taler+http://pay-push/localhost:8081/MQP1DP1J94ZZWNQS7TRDF1KJZ7V8H74CZF41V90FKXBPN5GNRN6G" }
- ));
+ const unknown_purse = await t.assertThrowsTalerErrorAsync(
+ wallet1.call(WalletApiOperation.PreparePeerPushCredit, {
+ talerUri:
+ "taler+http://pay-push/localhost:8081/MQP1DP1J94ZZWNQS7TRDF1KJZ7V8H74CZF41V90FKXBPN5GNRN6G",
+ }),
+ );
// FIXME this should fail with a proper error code
- t.assertTrue(unknown_purse.errorDetail.code === TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION);
+ t.assertTrue(
+ unknown_purse.errorDetail.code ===
+ TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
+ );
}
t.logStep("P2P push confirm");
@@ -197,21 +202,21 @@ export async function runPeerPushTest(t: GlobalTestState) {
transactionId: tx.transactionId,
txState: {
major: TransactionMajorState.Done,
- }
+ },
}),
Promise.race([
Promise.all([
wallet2.call(WalletApiOperation.TestingWaitTransactionState, {
transactionId: prepare2.transactionId,
txState: {
- major: TransactionMajorState.Done
- }
+ major: TransactionMajorState.Done,
+ },
}),
wallet3.call(WalletApiOperation.TestingWaitTransactionState, {
transactionId: prepare3.transactionId,
txState: {
- major: TransactionMajorState.Aborted
- }
+ major: TransactionMajorState.Aborted,
+ },
}),
]),
Promise.all([
@@ -219,21 +224,21 @@ export async function runPeerPushTest(t: GlobalTestState) {
transactionId: prepare2.transactionId,
txState: {
major: TransactionMajorState.Aborted,
- }
+ },
}),
wallet3.call(WalletApiOperation.TestingWaitTransactionState, {
transactionId: prepare3.transactionId,
txState: {
- major: TransactionMajorState.Done
- }
+ major: TransactionMajorState.Done,
+ },
}),
- ])
+ ]),
]),
wallet4.call(WalletApiOperation.TestingWaitTransactionState, {
transactionId: prepare4.transactionId,
txState: {
major: TransactionMajorState.Aborted,
- }
+ },
}),
]);
@@ -339,22 +344,41 @@ export async function runPeerPushTest(t: GlobalTestState) {
transactionId: tx.transactionId,
txState: {
major: TransactionMajorState.Aborted,
- }
+ },
}),
wallet2.call(WalletApiOperation.TestingWaitTransactionState, {
transactionId: prepare2.transactionId,
txState: {
major: TransactionMajorState.Aborted,
- }
+ },
}),
wallet3.call(WalletApiOperation.TestingWaitTransactionState, {
transactionId: prepare3.transactionId,
txState: {
major: TransactionMajorState.Aborted,
- }
+ },
}),
]);
}
+
+ // Test deleting p2p transaction.
+ // Tests against a regression.
+ t.runSpanAsync("delete-transactions", async () => {
+ const delAll = async (w: WalletClient) => {
+ const txn1 = await w.call(WalletApiOperation.GetTransactionsV2, {
+ includeAll: true,
+ });
+ for (const txn of txn1.transactions) {
+ await w.call(WalletApiOperation.DeleteTransaction, {
+ transactionId: txn.transactionId,
+ });
+ }
+ };
+ await delAll(wallet1);
+ await delAll(wallet2);
+ await delAll(wallet3);
+ await delAll(wallet4);
+ });
}
runPeerPushTest.suites = ["wallet"];
diff --git a/packages/taler-wallet-core/src/pay-peer-pull-credit.ts b/packages/taler-wallet-core/src/pay-peer-pull-credit.ts
@@ -299,7 +299,7 @@ export class PeerPullCreditTransactionContext implements TransactionContext {
"transactionsMeta",
],
},
- this.deleteTransactionInTx
+ this.deleteTransactionInTx.bind(this),
);
for (const notif of res.notifs) {
this.wex.ws.notify(notif);
diff --git a/packages/taler-wallet-core/src/pay-peer-pull-debit.ts b/packages/taler-wallet-core/src/pay-peer-pull-debit.ts
@@ -178,7 +178,7 @@ export class PeerPullDebitTransactionContext implements TransactionContext {
async deleteTransaction(): Promise<void> {
const res = await this.wex.db.runReadWriteTx(
{ storeNames: ["peerPullDebit", "transactionsMeta"] },
- this.deleteTransactionInTx
+ this.deleteTransactionInTx.bind(this),
);
for (const notif of res.notifs) {
this.wex.ws.notify(notif);
diff --git a/packages/taler-wallet-core/src/pay-peer-push-debit.ts b/packages/taler-wallet-core/src/pay-peer-push-debit.ts
@@ -97,7 +97,7 @@ import {
constructTransactionIdentifier,
isUnsuccessfulTransaction,
} from "./transactions.js";
-import { walletExchangeClient, WalletExecutionContext } from "./wallet.js";
+import { WalletExecutionContext, walletExchangeClient } from "./wallet.js";
import { updateWithdrawalDenomsForCurrency } from "./withdraw.js";
const logger = new Logger("pay-peer-push-debit.ts");
@@ -121,8 +121,8 @@ export class PeerPushDebitTransactionContext implements TransactionContext {
}
readonly store = "peerPushDebit";
- readonly recordId = this.pursePub
- readonly recordState = computePeerPushDebitTransactionState
+ readonly recordId = this.pursePub;
+ readonly recordState = computePeerPushDebitTransactionState;
readonly recordMeta = (rec: PeerPushDebitRecord) => ({
transactionId: this.transactionId,
status: rec.status,
@@ -130,7 +130,9 @@ export class PeerPushDebitTransactionContext implements TransactionContext {
currency: Amounts.currencyOf(rec.amount),
exchanges: [rec.exchangeBaseUrl],
});
- updateTransactionMeta = (tx: WalletDbReadWriteTransaction<["peerPushDebit", "transactionsMeta"]>) => recordUpdateMeta(this, tx)
+ updateTransactionMeta = (
+ tx: WalletDbReadWriteTransaction<["peerPushDebit", "transactionsMeta"]>,
+ ) => recordUpdateMeta(this, tx);
/**
* Get the full transaction details for the transaction.
@@ -194,7 +196,7 @@ export class PeerPushDebitTransactionContext implements TransactionContext {
async deleteTransaction(): Promise<void> {
const res = await this.wex.db.runReadWriteTx(
{ storeNames: ["peerPushDebit", "transactionsMeta"] },
- this.deleteTransactionInTx
+ this.deleteTransactionInTx.bind(this),
);
for (const notif of res.notifs) {
this.wex.ws.notify(notif);
@@ -202,9 +204,7 @@ export class PeerPushDebitTransactionContext implements TransactionContext {
}
async deleteTransactionInTx(
- tx: WalletDbReadWriteTransaction<
- ["peerPushDebit", "transactionsMeta"]
- >,
+ tx: WalletDbReadWriteTransaction<["peerPushDebit", "transactionsMeta"]>,
): Promise<{ notifs: WalletNotification[] }> {
return recordDelete(this, tx);
}
@@ -233,8 +233,8 @@ export class PeerPushDebitTransactionContext implements TransactionContext {
default:
assertUnreachable(rec.status);
}
- })
- this.wex.taskScheduler.stopShepherdTask(this.taskId)
+ });
+ this.wex.taskScheduler.stopShepherdTask(this.taskId);
}
async abortTransaction(reason?: TalerErrorDetail): Promise<void> {
@@ -262,7 +262,7 @@ export class PeerPushDebitTransactionContext implements TransactionContext {
default:
assertUnreachable(rec.status);
}
- })
+ });
this.wex.taskScheduler.stopShepherdTask(this.taskId);
this.wex.taskScheduler.startShepherdTask(this.taskId);
}
@@ -291,7 +291,7 @@ export class PeerPushDebitTransactionContext implements TransactionContext {
default:
assertUnreachable(rec.status);
}
- })
+ });
this.wex.taskScheduler.startShepherdTask(this.taskId);
}
@@ -316,7 +316,7 @@ export class PeerPushDebitTransactionContext implements TransactionContext {
default:
assertUnreachable(rec.status);
}
- })
+ });
this.wex.taskScheduler.stopShepherdTask(this.taskId);
this.wex.taskScheduler.startShepherdTask(this.taskId);
}
@@ -489,12 +489,12 @@ async function handlePurseCreationConflict(
coinPubs: sel.coins.map((x) => x.coinPub),
contributions: sel.coins.map((x) => x.contribution),
};
- return TransitionResultType.Transition
+ return TransitionResultType.Transition;
}
default:
- return TransitionResultType.Stay
+ return TransitionResultType.Stay;
}
- })
+ });
return TaskRunResult.progress();
}
@@ -502,7 +502,8 @@ async function processPeerPushDebitCreateReserve(
wex: WalletExecutionContext,
peerPushInitiation: PeerPushDebitRecord,
): Promise<TaskRunResult> {
- const { pursePub, purseExpiration, contractTermsHash, exchangeBaseUrl } = peerPushInitiation;
+ const { pursePub, purseExpiration, contractTermsHash, exchangeBaseUrl } =
+ peerPushInitiation;
const ctx = new PeerPushDebitTransactionContext(wex, pursePub);
logger.trace(`processing ${ctx.transactionId} pending(create-reserve)`);
@@ -543,8 +544,9 @@ async function processPeerPushDebitCreateReserve(
assertUnreachable(coinSelRes);
}
- let transitionDone = false
- await recordTransition(ctx,
+ let transitionDone = false;
+ await recordTransition(
+ ctx,
{
extraStores: [
"coinAvailability",
@@ -579,7 +581,7 @@ async function processPeerPushDebitCreateReserve(
});
transitionDone = true;
- return TransitionResultType.Transition
+ return TransitionResultType.Transition;
},
);
if (transitionDone) {
@@ -649,7 +651,10 @@ async function processPeerPushDebitCreateReserve(
econtract: econtractResp.econtract,
};
- const resp = await exchangeClient.createPurseFromDeposit(peerPushInitiation.pursePub, reqBody);
+ const resp = await exchangeClient.createPurseFromDeposit(
+ peerPushInitiation.pursePub,
+ reqBody,
+ );
switch (resp.case) {
case "ok":
// Possibly on to the next batch.
@@ -659,34 +664,49 @@ async function processPeerPushDebitCreateReserve(
await ctx.failTransaction(resp.detail);
return TaskRunResult.finished();
case HttpStatusCode.Conflict:
- return handlePurseCreationConflict(wex, peerPushInitiation, resp.body);
+ return handlePurseCreationConflict(
+ wex,
+ peerPushInitiation,
+ resp.body,
+ );
case HttpStatusCode.TooEarly:
return TaskRunResult.backoff();
default:
- assertUnreachable(resp)
+ assertUnreachable(resp);
}
} else {
const depositPayload: ExchangePurseDeposits = {
deposits: depositSigsResp.deposits,
};
- const resp = await exchangeClient.depositIntoPurse(peerPushInitiation.pursePub, depositPayload);
+ const resp = await exchangeClient.depositIntoPurse(
+ peerPushInitiation.pursePub,
+ depositPayload,
+ );
switch (resp.case) {
case "ok":
// Possibly on to the next batch.
continue;
case HttpStatusCode.Gone:
// FIXME we need PeerPushDebitStatus.ExpiredDeletePurse
- await recordTransitionStatus(ctx, PeerPushDebitStatus.PendingCreatePurse, PeerPushDebitStatus.AbortingDeletePurse)
- return TaskRunResult.progress()
+ await recordTransitionStatus(
+ ctx,
+ PeerPushDebitStatus.PendingCreatePurse,
+ PeerPushDebitStatus.AbortingDeletePurse,
+ );
+ return TaskRunResult.progress();
case HttpStatusCode.Conflict:
// Handle double-spending
- return handlePurseCreationConflict(wex, peerPushInitiation, resp.body);
+ return handlePurseCreationConflict(
+ wex,
+ peerPushInitiation,
+ resp.body,
+ );
case HttpStatusCode.Forbidden:
case HttpStatusCode.NotFound:
await ctx.failTransaction(resp.detail);
return TaskRunResult.finished();
default:
- assertUnreachable(resp)
+ assertUnreachable(resp);
}
}
}
@@ -695,17 +715,25 @@ async function processPeerPushDebitCreateReserve(
const resp = await exchangeClient.getPurseStatusAtDeposit(pursePub);
switch (resp.case) {
case "ok":
- await recordTransitionStatus(ctx, PeerPushDebitStatus.PendingCreatePurse, PeerPushDebitStatus.PendingReady)
- return TaskRunResult.progress()
+ await recordTransitionStatus(
+ ctx,
+ PeerPushDebitStatus.PendingCreatePurse,
+ PeerPushDebitStatus.PendingReady,
+ );
+ return TaskRunResult.progress();
case HttpStatusCode.Gone:
// FIXME we need PeerPushDebitStatus.ExpiredDeletePurse
- await recordTransitionStatus(ctx, PeerPushDebitStatus.PendingCreatePurse, PeerPushDebitStatus.AbortingDeletePurse)
- return TaskRunResult.progress()
+ await recordTransitionStatus(
+ ctx,
+ PeerPushDebitStatus.PendingCreatePurse,
+ PeerPushDebitStatus.AbortingDeletePurse,
+ );
+ return TaskRunResult.progress();
case HttpStatusCode.NotFound:
await ctx.failTransaction(resp.detail);
return TaskRunResult.finished();
default:
- assertUnreachable(resp)
+ assertUnreachable(resp);
}
}
@@ -715,24 +743,25 @@ async function processPeerPushDebitAbortingDeletePurse(
): Promise<TaskRunResult> {
const { pursePub, pursePriv, exchangeBaseUrl } = peerPushInitiation;
const ctx = new PeerPushDebitTransactionContext(wex, pursePub);
- const exchangeClient = walletExchangeClient(exchangeBaseUrl, wex)
+ const exchangeClient = walletExchangeClient(exchangeBaseUrl, wex);
const sigResp = await wex.cryptoApi.signDeletePurse({
pursePriv,
});
- const resp = await exchangeClient.deletePurse(pursePub, sigResp.sig)
+ const resp = await exchangeClient.deletePurse(pursePub, sigResp.sig);
switch (resp.case) {
case "ok":
case HttpStatusCode.NotFound:
break;
case HttpStatusCode.Conflict:
- throw Error("purse deletion conflict")
+ throw Error("purse deletion conflict");
case HttpStatusCode.Forbidden:
ctx.failTransaction(resp.detail);
- return TaskRunResult.finished()
+ return TaskRunResult.finished();
}
- await recordTransition(ctx,
+ await recordTransition(
+ ctx,
{
extraStores: [
"coinAvailability",
@@ -741,8 +770,9 @@ async function processPeerPushDebitAbortingDeletePurse(
"denominations",
"refreshGroups",
"refreshSessions",
- ]
- }, async (rec, tx) => {
+ ],
+ },
+ async (rec, tx) => {
if (rec.status !== PeerPushDebitStatus.AbortingDeletePurse) {
return TransitionResultType.Stay;
}
@@ -770,8 +800,9 @@ async function processPeerPushDebitAbortingDeletePurse(
ctx.transactionId,
);
rec.abortRefreshGroupId = refresh.refreshGroupId;
- return TransitionResultType.Transition
- })
+ return TransitionResultType.Transition;
+ },
+ );
return TaskRunResult.backoff();
}
@@ -786,7 +817,10 @@ async function processPeerPushDebitReady(
logger.trace("processing peer-push-debit pending(ready)");
const pursePub = peerPushInitiation.pursePub;
const ctx = new PeerPushDebitTransactionContext(wex, pursePub);
- const exchangeClient = walletExchangeClient(peerPushInitiation.exchangeBaseUrl, wex)
+ const exchangeClient = walletExchangeClient(
+ peerPushInitiation.exchangeBaseUrl,
+ wex,
+ );
const resp = await exchangeClient.getPurseStatusAtMerge(pursePub, true);
switch (resp.case) {
@@ -794,55 +828,63 @@ async function processPeerPushDebitReady(
if (!isPurseMerged(resp.body)) {
return TaskRunResult.longpollReturnedPending();
} else {
- await recordTransitionStatus(ctx, PeerPushDebitStatus.PendingReady, PeerPushDebitStatus.Done);
+ await recordTransitionStatus(
+ ctx,
+ PeerPushDebitStatus.PendingReady,
+ PeerPushDebitStatus.Done,
+ );
return TaskRunResult.progress();
}
}
case HttpStatusCode.Gone:
logger.info(`purse ${pursePub} is gone, aborting peer-push-debit`);
- await recordTransition(ctx, {
- extraStores: [
- "coinAvailability",
- "coinHistory",
- "coins",
- "denominations",
- "refreshGroups",
- "refreshSessions",
- ]
- }, async (rec, tx) => {
- if (rec.status !== PeerPushDebitStatus.PendingReady) {
- return TransitionResultType.Stay;
- }
- const currency = Amounts.currencyOf(rec.amount);
- const coinPubs: CoinRefreshRequest[] = [];
-
- if (rec.coinSel) {
- for (let i = 0; i < rec.coinSel.coinPubs.length; i++) {
- coinPubs.push({
- amount: rec.coinSel.contributions[i],
- coinPub: rec.coinSel.coinPubs[i],
- });
+ await recordTransition(
+ ctx,
+ {
+ extraStores: [
+ "coinAvailability",
+ "coinHistory",
+ "coins",
+ "denominations",
+ "refreshGroups",
+ "refreshSessions",
+ ],
+ },
+ async (rec, tx) => {
+ if (rec.status !== PeerPushDebitStatus.PendingReady) {
+ return TransitionResultType.Stay;
}
-
- const refresh = await createRefreshGroup(
- wex,
- tx,
- currency,
- coinPubs,
- RefreshReason.AbortPeerPushDebit,
- ctx.transactionId,
- );
-
- rec.abortRefreshGroupId = refresh.refreshGroupId;
- }
- rec.status = PeerPushDebitStatus.Aborted;
- return TransitionResultType.Transition
- })
+ const currency = Amounts.currencyOf(rec.amount);
+ const coinPubs: CoinRefreshRequest[] = [];
+
+ if (rec.coinSel) {
+ for (let i = 0; i < rec.coinSel.coinPubs.length; i++) {
+ coinPubs.push({
+ amount: rec.coinSel.contributions[i],
+ coinPub: rec.coinSel.coinPubs[i],
+ });
+ }
+
+ const refresh = await createRefreshGroup(
+ wex,
+ tx,
+ currency,
+ coinPubs,
+ RefreshReason.AbortPeerPushDebit,
+ ctx.transactionId,
+ );
+
+ rec.abortRefreshGroupId = refresh.refreshGroupId;
+ }
+ rec.status = PeerPushDebitStatus.Aborted;
+ return TransitionResultType.Transition;
+ },
+ );
return TaskRunResult.backoff();
case HttpStatusCode.NotFound:
throw Error("peer push credit disappeared");
default:
- assertUnreachable(resp)
+ assertUnreachable(resp);
}
}
@@ -911,7 +953,8 @@ export async function initiatePeerPushDebit(
await updateWithdrawalDenomsForCurrency(wex, instructedAmount.currency);
let exchangeBaseUrl;
- await recordCreate(ctx,
+ await recordCreate(
+ ctx,
{
extraStores: [
"coinAvailability",
@@ -925,7 +968,7 @@ export async function initiatePeerPushDebit(
"refreshSessions",
"globalCurrencyExchanges",
"globalCurrencyAuditors",
- ]
+ ],
},
async (tx) => {
const coinSelRes = await selectPeerCoinsInTx(wex, tx, {