taler-typescript-core

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

commit b32819d2d026822074c410be98ca97c7cd0226fa
parent 2fb5aa6300834b5908630750c4c3d715a0fdb80c
Author: Florian Dold <dold@taler.net>
Date:   Thu, 20 Aug 2026 19:06:41 +0200

wallet-core: enforce advertised transaction actions

Diffstat:
Mpackages/taler-wallet-core/src/transactions.test.ts | 96++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mpackages/taler-wallet-core/src/transactions.ts | 17+++++++++++++++++
Mpackages/taler-wallet-core/src/wallet-api-types.ts | 1+
3 files changed, 113 insertions(+), 1 deletion(-)

diff --git a/packages/taler-wallet-core/src/transactions.test.ts b/packages/taler-wallet-core/src/transactions.test.ts @@ -13,14 +13,29 @@ You should have received a copy of the GNU General Public License along with GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { TransactionType } from "@gnu-taler/taler-util"; +import { + TalerError, + TalerErrorCode, + TalerPreciseTimestamp, + TalerProtocolTimestamp, + TransactionType, +} from "@gnu-taler/taler-util"; import assert from "node:assert"; import { test } from "node:test"; import { constructTransactionIdentifier, + deleteTransaction, ParsedTransactionIdentifier, parseTransactionIdentifier, } from "./transactions.js"; +import { + PeerPushDebitStatus, + timestampPreciseToDb, + timestampProtocolToDb, + WalletPeerPushDebit, +} from "./db-common.js"; +import { WalletDbTransaction } from "./dbtx.js"; +import { WalletExecutionContext } from "./wallet.js"; const allIdentifiers: ParsedTransactionIdentifier[] = [ { tag: TransactionType.Deposit, depositGroupId: "dg" }, @@ -65,3 +80,82 @@ test("a malformed transaction identifier is rejected", (t) => { ); assert.strictEqual(parseTransactionIdentifier(""), undefined); }); + +test("deleting a transaction is rejected when delete is not advertised", async () => { + const purseExpiration = TalerProtocolTimestamp.fromSeconds(2_000_000_000); + const contractTermsHash = "contract-terms-hash"; + let record: WalletPeerPushDebit | undefined = { + exchangeBaseUrl: "https://exchange.example/", + amount: "TESTKUDOS:1", + totalCost: "TESTKUDOS:1", + contractTermsHash, + pursePub: "purse-pub", + pursePriv: "purse-priv", + mergePub: "merge-pub", + mergePriv: "merge-priv", + contractPriv: "contract-priv", + contractPub: "contract-pub", + contractEncNonce: "contract-enc-nonce", + purseExpiration: timestampProtocolToDb(purseExpiration), + timestampCreated: timestampPreciseToDb( + TalerPreciseTimestamp.fromSeconds(1_000_000_000), + ), + status: PeerPushDebitStatus.PendingCreatePurse, + }; + const tx = { + async getPeerPushDebit(): Promise<WalletPeerPushDebit | undefined> { + return record; + }, + async getOperationRetry(): Promise<undefined> { + return undefined; + }, + async getContractTerms() { + return { + h: contractTermsHash, + contractTermsRaw: { + purse_expiration: purseExpiration, + summary: "peer payment", + }, + }; + }, + async getExchange(): Promise<undefined> { + return undefined; + }, + async deletePeerPushDebit(): Promise<void> { + record = undefined; + }, + async deleteTransactionMeta(): Promise<void> {}, + notify(): void {}, + } as unknown as WalletDbTransaction; + let stopped = false; + const wex = { + async runWalletDbTx<T>( + f: (tx: WalletDbTransaction) => Promise<T>, + ): Promise<T> { + return await f(tx); + }, + taskScheduler: { + stopShepherdTask(): void { + stopped = true; + }, + }, + } as unknown as WalletExecutionContext; + const transactionId = constructTransactionIdentifier({ + tag: TransactionType.PeerPushDebit, + pursePub: "purse-pub", + }); + + await assert.rejects( + deleteTransaction(wex, transactionId), + (error: unknown) => { + assert.ok(error instanceof TalerError); + assert.strictEqual( + error.errorDetail.code, + TalerErrorCode.WALLET_REQUEST_TRANSACTION_STATE_UNSUPPORTED, + ); + return true; + }, + ); + assert.ok(record, "the transaction record must be retained"); + assert.strictEqual(stopped, false, "its recovery task must keep running"); +}); diff --git a/packages/taler-wallet-core/src/transactions.ts b/packages/taler-wallet-core/src/transactions.ts @@ -32,6 +32,7 @@ import { TalerError, TalerErrorCode, Transaction, + TransactionAction, TransactionByIdRequest, TransactionIdStr, TransactionMajorState, @@ -1038,6 +1039,22 @@ export async function deleteTransaction( transactionId: string, ): Promise<void> { const ctx = await getContextForTransaction(wex, transactionId); + const transaction = await wex.runWalletDbTx(async (tx) => + ctx.lookupFullTransaction(tx), + ); + if (!transaction) { + throw makeTransactionNotFoundError(transactionId); + } + if (!transaction.txActions.includes(TransactionAction.Delete)) { + throw TalerError.fromDetail( + TalerErrorCode.WALLET_REQUEST_TRANSACTION_STATE_UNSUPPORTED, + { + txState: transaction.txState, + debugStateNum: transaction.stId, + }, + `transaction ${transactionId} cannot be deleted in its current state`, + ); + } await ctx.userDeleteTransaction(); if (ctx.taskId) { wex.taskScheduler.stopShepherdTask(ctx.taskId); diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts @@ -1752,6 +1752,7 @@ export const walletApiExpectedErrors = { ], [WalletApiOperation.DeleteTransaction]: [ TalerErrorCode.WALLET_TRANSACTION_NOT_FOUND, + TalerErrorCode.WALLET_REQUEST_TRANSACTION_STATE_UNSUPPORTED, ], [WalletApiOperation.RetryTransaction]: [ TalerErrorCode.WALLET_TRANSACTION_NOT_FOUND,