diff --git a/packages/taler-util/src/walletTypes.ts b/packages/taler-util/src/walletTypes.ts index 657e6568c..006140070 100644 --- a/packages/taler-util/src/walletTypes.ts +++ b/packages/taler-util/src/walletTypes.ts @@ -945,4 +945,8 @@ export interface WalletCurrencyInfo { exchangeMasterPub: string; exchangeBaseUrl: string; }[]; +} + +export interface DeleteTransactionRequest { + transactionId: string; } \ No newline at end of file diff --git a/packages/taler-wallet-cli/src/index.ts b/packages/taler-wallet-cli/src/index.ts index 8a9e28d32..59b8d5c60 100644 --- a/packages/taler-wallet-cli/src/index.ts +++ b/packages/taler-wallet-cli/src/index.ts @@ -284,6 +284,21 @@ walletCli }); }); +walletCli + .subcommand("deleteTransaction", "delete-transaction", { + help: "Permanently delete a transaction from the transaction list.", + }) + .requiredArgument("transactionId", clk.STRING, { + help: "Identifier of the transaction to delete", + }) + .action(async (args) => { + await withWallet(args, async (wallet) => { + await wallet.deleteTransaction({ + transactionId: args.deleteTransaction.transactionId, + }); + }); + }); + walletCli .subcommand("handleUri", "handle-uri", { help: "Handle a taler:// URI.", @@ -609,13 +624,13 @@ const currenciesCli = walletCli.subcommand("currencies", "currencies", { }); currenciesCli - .subcommand("show", "show", { help: "Show currencies."}) + .subcommand("show", "show", { help: "Show currencies." }) .action(async (args) => { await withWallet(args, async (wallet) => { const currencies = await wallet.getCurrencies(); console.log(JSON.stringify(currencies, undefined, 2)); }); - }) + }); const reservesCli = advancedCli.subcommand("reserves", "reserves", { help: "Manage reserves.", diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index 52fe5c3dc..609f43ea1 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -25,7 +25,6 @@ import { MerchantInfo, Product, RefreshReason, - ReserveTransaction, TalerErrorDetails, Timestamp, } from "@gnu-taler/taler-util"; @@ -1783,7 +1782,7 @@ class AuditorTrustStore extends Store<"auditorTrust", AuditorTrustRecord> { class ExchangeTrustStore extends Store<"exchangeTrust", ExchangeTrustRecord> { constructor() { super("exchangeTrust", { - keyPath: ["currency", "exchangeBaseUrl", "exchangePub"], + keyPath: ["currency", "exchangeBaseUrl", "exchangeMasterPub"], }); } exchangeMasterPubIndex = new Index< @@ -1791,7 +1790,7 @@ class ExchangeTrustStore extends Store<"exchangeTrust", ExchangeTrustRecord> { "exchangeMasterPubIndex", string, ExchangeTrustRecord - >(this, "exchangeMasterPubIndex", "exchangePub"); + >(this, "exchangeMasterPubIndex", "exchangeMasterPub"); uidIndex = new Index< "exchangeTrust", "uidIndex", @@ -1810,6 +1809,12 @@ class ReservesStore extends Store<"reserves", ReserveRecord> { constructor() { super("reserves", { keyPath: "reservePub" }); } + byInitialWithdrawalGroupId = new Index< + "reserves", + "initialWithdrawalGroupIdIndex", + string, + ReserveRecord + >(this, "initialWithdrawalGroupIdIndex", "initialWithdrawalGroupId"); } class TipsStore extends Store<"tips", TipRecord> { diff --git a/packages/taler-wallet-core/src/operations/transactions.ts b/packages/taler-wallet-core/src/operations/transactions.ts index dcd3ddbd9..48d0ffec5 100644 --- a/packages/taler-wallet-core/src/operations/transactions.ts +++ b/packages/taler-wallet-core/src/operations/transactions.ts @@ -24,6 +24,7 @@ import { RefundState, ReserveRecordStatus, AbortStatus, + ReserveRecord, } from "../db.js"; import { AmountJson, Amounts, timestampCmp } from "@gnu-taler/taler-util"; import { @@ -42,7 +43,7 @@ import { getFundingPaytoUris } from "./reserves"; * Create an event ID from the type and the primary key for the event. */ function makeEventId(type: TransactionType, ...args: string[]): string { - return type + ";" + args.map((x) => encodeURIComponent(x)).join(";"); + return type + ":" + args.map((x) => encodeURIComponent(x)).join(":"); } function shouldSkipCurrency( @@ -365,3 +366,55 @@ export async function getTransactions( return { transactions: [...txNotPending, ...txPending] }; } + +export enum TombstoneTag { + WithdrawalGroup = "withdrawal-group", + Reserve = "reserve", +} + +/** + * Permanentely delete a transaction based on the transaction ID. + */ +export async function deleteTransaction( + ws: InternalWalletState, + transactionId: string, +): Promise { + const [type, ...rest] = transactionId.split(":"); + + if (type === TransactionType.Withdrawal) { + const withdrawalGroupId = rest[0]; + ws.db.runWithWriteTransaction( + [Stores.withdrawalGroups, Stores.reserves, Stores.tombstones], + async (tx) => { + const withdrawalGroupRecord = await tx.get( + Stores.withdrawalGroups, + withdrawalGroupId, + ); + if (withdrawalGroupRecord) { + await tx.delete(Stores.withdrawalGroups, withdrawalGroupId); + await tx.put(Stores.tombstones, { + id: TombstoneTag.WithdrawalGroup + ":" + withdrawalGroupId, + }); + return; + } + const reserveRecord: ReserveRecord | undefined = await tx.getIndexed( + Stores.reserves.byInitialWithdrawalGroupId, + withdrawalGroupId, + ); + if (reserveRecord && !reserveRecord.initialWithdrawalStarted) { + const reservePub = reserveRecord.reservePub; + await tx.delete(Stores.reserves, reservePub); + await tx.put(Stores.tombstones, { + id: TombstoneTag.Reserve + ":" + reservePub, + }); + } + }, + ); + } else if (type === TransactionType.Refund) { + // To delete refund transactions, the whole + // purchase should be deleted. + throw Error("refunds cannot be deleted"); + } else { + throw Error(`can't delete a '${type}' transaction`); + } +} diff --git a/packages/taler-wallet-core/src/util/query.ts b/packages/taler-wallet-core/src/util/query.ts index 4c37a7254..1ef75a420 100644 --- a/packages/taler-wallet-core/src/util/query.ts +++ b/packages/taler-wallet-core/src/util/query.ts @@ -33,6 +33,7 @@ import { IDBVersionChangeEvent, Event, IDBCursor, + IDBKeyPath, } from "@gnu-taler/idb-bridge"; import { Logger } from "./logging"; diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index 192b54926..c380786ab 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -25,6 +25,7 @@ import { BackupRecovery, codecForAny, + DeleteTransactionRequest, TalerErrorCode, WalletCurrencyInfo, } from "@gnu-taler/taler-util"; @@ -92,7 +93,7 @@ import { withdrawTestBalance, } from "./operations/testing"; import { acceptTip, prepareTip, processTip } from "./operations/tip"; -import { getTransactions } from "./operations/transactions"; +import { deleteTransaction, getTransactions } from "./operations/transactions"; import { getExchangeWithdrawalInfo, getWithdrawalDetailsForUri, @@ -578,6 +579,10 @@ export class Wallet { return getWithdrawalDetailsForUri(this.ws, talerWithdrawUri); } + async deleteTransaction(req: DeleteTransactionRequest): Promise { + return deleteTransaction(this.ws, req.transactionId); + } + /** * Update or add exchange DB entry by fetching the /keys and /wire information. * Optionally link the reserve entry to the new or existing