taler-typescript-core

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

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:
Mpackages/taler-harness/src/integrationtests/test-peer-push.ts | 60++++++++++++++++++++++++++++++++++++++++++------------------
Mpackages/taler-wallet-core/src/pay-peer-pull-credit.ts | 2+-
Mpackages/taler-wallet-core/src/pay-peer-pull-debit.ts | 2+-
Mpackages/taler-wallet-core/src/pay-peer-push-debit.ts | 211+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
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, {