taler-typescript-core

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

commit d2bc91139c49ed59799889252e4670695105bb50
parent 4581f2e9ad365ef95424a3ff8b2f81b139d8fe68
Author: Florian Dold <florian@dold.me>
Date:   Sun, 23 Feb 2025 17:12:58 +0100

wallet-core: allow purging transactions when deleting exchange, refactor tx deletion

Diffstat:
Mpackages/taler-util/src/types-taler-wallet.ts | 11+++++++++++
Mpackages/taler-wallet-core/src/db.ts | 90++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
Mpackages/taler-wallet-core/src/deposits.ts | 50++++++++++++++++++++++++++++++++++----------------
Mpackages/taler-wallet-core/src/exchanges.ts | 166++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Mpackages/taler-wallet-core/src/pay-merchant.ts | 107+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
Mpackages/taler-wallet-core/src/pay-peer-pull-credit.ts | 70++++++++++++++++++++++++++++++++++++++++++++++------------------------
Mpackages/taler-wallet-core/src/pay-peer-pull-debit.ts | 45++++++++++++++++++++++++++++++++++-----------
Mpackages/taler-wallet-core/src/pay-peer-push-credit.ts | 71+++++++++++++++++++++++++++++++++++++++++++++++------------------------
Mpackages/taler-wallet-core/src/pay-peer-push-debit.ts | 43++++++++++++++++++++++++++++++++++---------
Mpackages/taler-wallet-core/src/recoup.ts | 35+++++++++++++++++++++++++++++++++--
Mpackages/taler-wallet-core/src/refresh.ts | 50+++++++++++++++++++++++++++++++++++++++-----------
Mpackages/taler-wallet-core/src/shepherd.ts | 1-
Mpackages/taler-wallet-core/src/withdraw.ts | 57++++++++++++++++++++++++++++++++++++++++++---------------
13 files changed, 603 insertions(+), 193 deletions(-)

diff --git a/packages/taler-util/src/types-taler-wallet.ts b/packages/taler-util/src/types-taler-wallet.ts @@ -1898,13 +1898,24 @@ export interface GetExchangeResourcesResponse { export interface DeleteExchangeRequest { exchangeBaseUrl: string; + + /** + * Delete the exchange even if it's in use. + */ purge?: boolean; + + /** + * Also purge *all* transactions that involve the exchange, + * even ones that also involve other exchanges. + */ + purgeTransactions?: boolean; } export const codecForDeleteExchangeRequest = (): Codec<DeleteExchangeRequest> => buildCodecForObject<DeleteExchangeRequest>() .property("exchangeBaseUrl", codecForCanonBaseUrl()) .property("purge", codecOptional(codecForBoolean())) + .property("purgeTransactions", codecOptional(codecForBoolean())) .build("DeleteExchangeRequest"); export interface ForceExchangeUpdateRequest { diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts @@ -157,7 +157,7 @@ export const CURRENT_DB_CONFIG_KEY = "currentMainDbName"; * backwards-compatible way or object stores and indices * are added. */ -export const WALLET_DB_MINOR_VERSION = 14; +export const WALLET_DB_MINOR_VERSION = 15; declare const symDbProtocolTimestamp: unique symbol; @@ -1077,7 +1077,7 @@ export interface RefreshGroupRecord { * Refund requests that might still be necessary * before the refresh can work. */ - refundRequests: { [n: number]: ExchangeRefundRequest}; + refundRequests: { [n: number]: ExchangeRefundRequest }; timestampCreated: DbPreciseTimestamp; @@ -1321,6 +1321,13 @@ export interface PurchaseRecord { payInfo: PurchasePayInfo | undefined; /** + * Exchanges involved in this purchase. + * Used as a multiEntry index to find all purchases for + * an exchange. + */ + exchanges?: string[]; + + /** * Pending removals from pay coin selection. * * Used when a the pay coin selection needs to be changed @@ -2253,10 +2260,14 @@ export enum ReserveRecordStatus { * other records to reference the reserve key pair via a small row ID. * * In the future, we might also store KYC info about a reserve here. + * + * FIXME: Should reference exchange. */ export interface ReserveRecord { rowId?: number; + reservePub: string; + reservePriv: string; status?: ReserveRecordStatus; @@ -2435,6 +2446,11 @@ export interface RefundItemRecord { status: RefundItemStatus; + /** + * Mandatory since DB minor version 15. + */ + proposalId?: string; + refundGroupId: string; /** @@ -2748,7 +2764,11 @@ export const WalletStoresV1 = { describeContents<RefreshSessionRecord>({ keyPath: ["refreshGroupId", "coinIndex"], }), - {}, + { + byRefreshGroupId: describeIndex("byRefreshGroupId", "refreshGroupId", { + versionAdded: 15, + }), + }, ), recoupGroups: describeStore( "recoupGroups", @@ -2759,6 +2779,9 @@ export const WalletStoresV1 = { byStatus: describeIndex("byStatus", "operationStatus", { versionAdded: 6, }), + byExchangeBaseUrl: describeIndex("byExchangeBaseUrl", "exchangeBaseUrl", { + versionAdded: 15, + }), }, ), purchases: describeStore( @@ -2774,19 +2797,9 @@ export const WalletStoresV1 = { "merchantBaseUrl", "orderId", ]), - }, - ), - // Just a tombstone at this point. - rewards: describeStore( - "rewards", - describeContents<any>({ keyPath: "walletRewardId" }), - { - byMerchantTipIdAndBaseUrl: describeIndex("byMerchantRewardIdAndBaseUrl", [ - "merchantRewardId", - "merchantBaseUrl", - ]), - byStatus: describeIndex("byStatus", "status", { - versionAdded: 8, + byExchange: describeIndex("byExchange", "exchanges", { + versionAdded: 15, + multiEntry: true, }), }, ), @@ -2934,13 +2947,6 @@ export const WalletStoresV1 = { byStatus: describeIndex("byStatus", "status"), }, ), - _obsolete_bankAccounts: describeStore( - "bankAccounts", - describeContents<any>({ - keyPath: "uri", - }), - {}, - ), bankAccountsV2: describeStore( "bankAccountsV2", describeContents<BankAccountsRecord>({ @@ -2960,13 +2966,6 @@ export const WalletStoresV1 = { }), {}, ), - userAttention: describeStore( - "userAttention", - describeContents<UserAttentionRecord>({ - keyPath: ["entityId", "info.type"], - }), - {}, - ), refundGroups: describeStore( "refundGroups", describeContents<RefundGroupRecord>({ @@ -2999,7 +2998,9 @@ export const WalletStoresV1 = { }), {}, ), - // Obsolete store, not used anymore + // + // Obsolete stores, not used anymore + // _obsolete_transactions: describeStoreV2({ recordCodec: passthroughCodec<unknown>(), storeName: "transactions", @@ -3015,6 +3016,33 @@ export const WalletStoresV1 = { }), }, }), + _obsolete_bankAccounts: describeStore( + "bankAccounts", + describeContents<any>({ + keyPath: "uri", + }), + {}, + ), + _obsolete_rewards: describeStore( + "rewards", + describeContents<any>({ keyPath: "walletRewardId" }), + { + byMerchantTipIdAndBaseUrl: describeIndex("byMerchantRewardIdAndBaseUrl", [ + "merchantRewardId", + "merchantBaseUrl", + ]), + byStatus: describeIndex("byStatus", "status", { + versionAdded: 8, + }), + }, + ), + userAttention: describeStore( + "userAttention", + describeContents<UserAttentionRecord>({ + keyPath: ["entityId", "info.type"], + }), + {}, + ), }; export type WalletDbStoresArr = Array<StoreNames<typeof WalletStoresV1>>; diff --git a/packages/taler-wallet-core/src/deposits.ts b/packages/taler-wallet-core/src/deposits.ts @@ -42,6 +42,7 @@ import { KycAuthTransferInfo, Logger, MerchantContractTermsV0, + NotificationType, RefreshReason, ScopeInfo, SelectedProspectiveCoin, @@ -59,6 +60,7 @@ import { TransactionState, TransactionType, URL, + WalletNotification, assertUnreachable, canonicalJson, checkDbInvariant, @@ -86,7 +88,6 @@ import { PendingTaskType, TaskIdStr, TaskRunResult, - TombstoneTag, TransactionContext, constructTaskIdentifier, runWithClientCancellation, @@ -306,24 +307,41 @@ export class DepositTransactionContext implements TransactionContext { } async deleteTransaction(): Promise<void> { - const depositGroupId = this.depositGroupId; - const ws = this.wex; - // FIXME: We should check first if we are in a final state - // where deletion is allowed. - await ws.db.runReadWriteTx( - { storeNames: ["depositGroups", "tombstones", "transactionsMeta"] }, + const res = await this.wex.db.runReadWriteTx( + { + storeNames: ["depositGroups", "tombstones", "transactionsMeta"], + }, async (tx) => { - const tipRecord = await tx.depositGroups.get(depositGroupId); - if (tipRecord) { - await tx.depositGroups.delete(depositGroupId); - await tx.tombstones.put({ - id: TombstoneTag.DeleteDepositGroup + ":" + depositGroupId, - }); - await this.updateTransactionMeta(tx); - } + return this.deleteTransactionInTx(tx); }, ); - return; + for (const notif of res.notifs) { + this.wex.ws.notify(notif); + } + } + + async deleteTransactionInTx( + tx: WalletDbReadWriteTransaction< + ["depositGroups", "tombstones", "transactionsMeta"] + >, + ): Promise<{ notifs: WalletNotification[] }> { + const notifs: WalletNotification[] = []; + const rec = await tx.depositGroups.get(this.depositGroupId); + if (!rec) { + return { notifs }; + } + const oldTxState = computeDepositTransactionStatus(rec); + await tx.depositGroups.delete(rec.depositGroupId); + await this.updateTransactionMeta(tx); + notifs.push({ + type: NotificationType.TransactionStateTransition, + transactionId: this.transactionId, + oldTxState, + newTxState: { + major: TransactionMajorState.Deleted, + }, + }); + return { notifs }; } async suspendTransaction(): Promise<void> { diff --git a/packages/taler-wallet-core/src/exchanges.ts b/packages/taler-wallet-core/src/exchanges.ts @@ -167,9 +167,16 @@ import { selectBestForOverlappingDenominations, selectMinimumFee, } from "./denominations.js"; +import { DepositTransactionContext } from "./deposits.js"; +import { + PayMerchantTransactionContext, + RefundTransactionContext, +} from "./pay-merchant.js"; +import { PeerPullCreditTransactionContext } from "./pay-peer-pull-credit.js"; +import { PeerPullDebitTransactionContext } from "./pay-peer-pull-debit.js"; import { DbReadOnlyTransaction } from "./query.js"; -import { createRecoupGroup } from "./recoup.js"; -import { createRefreshGroup } from "./refresh.js"; +import { RecoupTransactionContext, createRecoupGroup } from "./recoup.js"; +import { RefreshTransactionContext, createRefreshGroup } from "./refresh.js"; import { BalanceEffect, constructTransactionIdentifier, @@ -178,6 +185,9 @@ import { } from "./transactions.js"; import { WALLET_EXCHANGE_PROTOCOL_VERSION } from "./versions.js"; import { InternalWalletState, WalletExecutionContext } from "./wallet.js"; +import { WithdrawTransactionContext } from "./withdraw.js"; +import { PeerPushCreditTransactionContext } from "./pay-peer-push-credit.js"; +import { PeerPushDebitTransactionContext } from "./pay-peer-push-debit.js"; const logger = new Logger("exchanges.ts"); @@ -2846,8 +2856,11 @@ async function internalGetExchangeResources( async function purgeExchange( wex: WalletExecutionContext, tx: WalletDbAllStoresReadWriteTransaction, - exchangeBaseUrl: string, -): Promise<void> { + exchangeRec: ExchangeEntryRecord, + purgeTransactions?: boolean, +): Promise<{ notifs: WalletNotification[] }> { + const exchangeBaseUrl = exchangeRec.baseUrl; + const notifs: WalletNotification[] = []; const detRecs = await tx.exchangeDetails.indexes.byExchangeBaseUrl.getAll(); // Remove all exchange detail records for that exchange for (const r of detRecs) { @@ -2865,8 +2878,16 @@ async function purgeExchange( await tx.exchangeSignKeys.delete([r.rowId, rec.signkeyPub]); } } - // FIXME: Also remove records related to transactions? + + const oldExchangeState = getExchangeState(exchangeRec); + await tx.exchanges.delete(exchangeBaseUrl); + notifs.push({ + type: NotificationType.ExchangeStateTransition, + oldExchangeState, + newExchangeState: undefined, + exchangeBaseUrl, + }); { const coinAvailabilityRecs = @@ -2886,6 +2907,7 @@ async function purgeExchange( const coinRecs = await tx.coins.indexes.byBaseUrl.getAll(exchangeBaseUrl); for (const rec of coinRecs) { await tx.coins.delete(rec.coinPub); + await tx.coinHistory.delete(rec.coinPub); } } @@ -2897,23 +2919,125 @@ async function purgeExchange( } } + // Always delete withdrawals, even if no explicit + // transaction deletion was requested. { const withdrawalGroupRecs = await tx.withdrawalGroups.indexes.byExchangeBaseUrl.getAll( exchangeBaseUrl, ); for (const wg of withdrawalGroupRecs) { - await tx.withdrawalGroups.delete(wg.withdrawalGroupId); - const planchets = await tx.planchets.indexes.byGroup.getAll( - wg.withdrawalGroupId, - ); - for (const p of planchets) { - await tx.planchets.delete(p.coinPub); + const ctx = new WithdrawTransactionContext(wex, wg.withdrawalGroupId); + const res = await ctx.deleteTransactionInTx(tx); + notifs.push(...res.notifs); + } + } + + if (purgeTransactions) { + // Remove from refreshGroups and refreshSessions + { + await tx.refreshGroups.iter().forEachAsync(async (rg) => { + if (rg.infoPerExchange && rg.infoPerExchange[exchangeBaseUrl] != null) { + const ctx = new RefreshTransactionContext(wex, rg.refreshGroupId); + const res = await ctx.deleteTransactionInTx(tx); + notifs.push(...res.notifs); + } + }); + } + // Remove from recoupGroups + { + const recoupGroups = + await tx.recoupGroups.indexes.byExchangeBaseUrl.getAll(exchangeBaseUrl); + for (const rg of recoupGroups) { + const ctx = new RecoupTransactionContext(wex, rg.recoupGroupId); + const res = await ctx.deleteTransactionInTx(tx); + notifs.push(...res.notifs); + } + } + // Remove from deposits + { + // FIXME: Index would be useful here + await tx.depositGroups.iter().forEachAsync(async (dg) => { + if (dg.infoPerExchange && dg.infoPerExchange[exchangeBaseUrl]) { + const ctx = new DepositTransactionContext(wex, dg.depositGroupId); + const res = await ctx.deleteTransactionInTx(tx); + notifs.push(...res.notifs); + } + }); + } + // Remove from purchases, refundGroups, refundItems + { + const purchases = + await tx.purchases.indexes.byExchange.getAll(exchangeBaseUrl); + for (const purch of purchases) { + const refunds = await tx.refundGroups.indexes.byProposalId.getAll( + purch.proposalId, + ); + for (const r of refunds) { + const refundCtx = new RefundTransactionContext(wex, r.refundGroupId); + const res = await refundCtx.deleteTransactionInTx(tx); + notifs.push(...res.notifs); + } + const payCtx = new PayMerchantTransactionContext(wex, purch.proposalId); + const res = await payCtx.deleteTransactionInTx(tx); + notifs.push(...res.notifs); } } + // Remove from peerPullCredit + { + await tx.peerPullCredit.iter().forEachAsync(async (rec) => { + if (rec.exchangeBaseUrl === exchangeBaseUrl) { + const ctx = new PeerPullCreditTransactionContext(wex, rec.pursePub); + const res = await ctx.deleteTransactionInTx(tx); + notifs.push(...res.notifs); + } + }); + } + // Remove from peerPullDebit + { + await tx.peerPullDebit.iter().forEachAsync(async (rec) => { + if (rec.exchangeBaseUrl === exchangeBaseUrl) { + const ctx = new PeerPullDebitTransactionContext( + wex, + rec.peerPullDebitId, + ); + const res = await ctx.deleteTransactionInTx(tx); + notifs.push(...res.notifs); + } + }); + } + // Remove from peerPushCredit + { + await tx.peerPushCredit.iter().forEachAsync(async (rec) => { + if (rec.exchangeBaseUrl === exchangeBaseUrl) { + const ctx = new PeerPushCreditTransactionContext( + wex, + rec.peerPushCreditId, + ); + const res = await ctx.deleteTransactionInTx(tx); + notifs.push(...res.notifs); + } + }); + } + // Remove from peerPushDebit + { + await tx.peerPushDebit.iter().forEachAsync(async (rec) => { + if (rec.exchangeBaseUrl === exchangeBaseUrl) { + const ctx = new PeerPushDebitTransactionContext( + wex, + rec.pursePub, + ); + const res = await ctx.deleteTransactionInTx(tx); + notifs.push(...res.notifs); + } + }); + } } + // FIXME: Is this even necessary? Each deletion should already do it. await rematerializeTransactions(wex, tx); + + return { notifs }; } export async function deleteExchange( @@ -2922,28 +3046,26 @@ export async function deleteExchange( ): Promise<void> { let inUse: boolean = false; const exchangeBaseUrl = req.exchangeBaseUrl; - const notif = await wex.db.runAllStoresReadWriteTx({}, async (tx) => { + const notifs = await wex.db.runAllStoresReadWriteTx({}, async (tx) => { const exchangeRec = await tx.exchanges.get(exchangeBaseUrl); if (!exchangeRec) { // Nothing to delete! logger.info("no exchange found to delete"); return; } - const oldExchangeState = getExchangeState(exchangeRec); const res = await internalGetExchangeResources(wex, tx, exchangeBaseUrl); if (res.hasResources && !req.purge) { inUse = true; return; } - await purgeExchange(wex, tx, exchangeBaseUrl); + const purgeRes = await purgeExchange( + wex, + tx, + exchangeRec, + req.purgeTransactions, + ); wex.ws.exchangeCache.clear(); - - return { - type: NotificationType.ExchangeStateTransition, - oldExchangeState, - newExchangeState: undefined, - exchangeBaseUrl, - } satisfies WalletNotification; + return purgeRes.notifs; }); if (inUse) { @@ -2952,7 +3074,7 @@ export async function deleteExchange( hint: "Exchange in use.", }); } - if (notif) { + for (const notif of notifs ?? []) { wex.ws.notify(notif); } } diff --git a/packages/taler-wallet-core/src/pay-merchant.ts b/packages/taler-wallet-core/src/pay-merchant.ts @@ -96,6 +96,7 @@ import { TransactionType, URL, WalletContractData, + WalletNotification, } from "@gnu-taler/taler-util"; import { getHttpResponseErrorDetails, @@ -122,7 +123,6 @@ import { TaskIdStr, TaskRunResult, TaskRunResultType, - TombstoneTag, TransactionContext, TransitionResultType, } from "./common.js"; @@ -214,8 +214,7 @@ export class PayMerchantTransactionContext implements TransactionContext { status: purchaseRec.purchaseStatus, timestamp: purchaseRec.timestamp, currency: purchaseRec.download?.currency, - // FIXME! - exchanges: [], + exchanges: purchaseRec.exchanges ?? [], }); } @@ -391,24 +390,41 @@ export class PayMerchantTransactionContext implements TransactionContext { } async deleteTransaction(): Promise<void> { - const { wex: ws, proposalId } = this; - await ws.db.runReadWriteTx( - { storeNames: ["purchases", "tombstones", "transactionsMeta"] }, + const res = await this.wex.db.runReadWriteTx( + { + storeNames: ["purchases", "tombstones", "transactionsMeta"], + }, async (tx) => { - let found = false; - const purchase = await tx.purchases.get(proposalId); - if (purchase) { - found = true; - await tx.purchases.delete(proposalId); - await this.updateTransactionMeta(tx); - } - if (found) { - await tx.tombstones.put({ - id: TombstoneTag.DeletePayment + ":" + proposalId, - }); - } + return this.deleteTransactionInTx(tx); }, ); + for (const notif of res.notifs) { + this.wex.ws.notify(notif); + } + } + + async deleteTransactionInTx( + tx: WalletDbReadWriteTransaction< + ["purchases", "tombstones", "transactionsMeta"] + >, + ): Promise<{ notifs: WalletNotification[] }> { + const notifs: WalletNotification[] = []; + const rec = await tx.purchases.get(this.proposalId); + if (!rec) { + return { notifs }; + } + const oldTxState = computePayMerchantTransactionState(rec); + await tx.purchases.delete(rec.proposalId); + await this.updateTransactionMeta(tx); + notifs.push({ + type: NotificationType.TransactionStateTransition, + transactionId: this.transactionId, + oldTxState, + newTxState: { + major: TransactionMajorState.Deleted, + }, + }); + return { notifs }; } async suspendTransaction(): Promise<void> { @@ -671,18 +687,47 @@ export class RefundTransactionContext implements TransactionContext { async deleteTransaction(): Promise<void> { const { wex, refundGroupId, transactionId } = this; - await wex.db.runReadWriteTx( - { storeNames: ["refundGroups", "tombstones", "transactionsMeta"] }, + + const res = await wex.db.runReadWriteTx( + { + storeNames: [ + "refundGroups", + "refundItems", + "tombstones", + "transactionsMeta", + ], + }, async (tx) => { - const refundRecord = await tx.refundGroups.get(refundGroupId); - if (!refundRecord) { - return; - } - await tx.refundGroups.delete(refundGroupId); - await this.updateTransactionMeta(tx); - await tx.tombstones.put({ id: transactionId }); + return await this.deleteTransactionInTx(tx); }, ); + for (const notif of res.notifs) { + this.wex.ws.notify(notif); + } + } + + async deleteTransactionInTx( + tx: WalletDbReadWriteTransaction< + ["refundGroups", "refundItems", "tombstones", "transactionsMeta"] + >, + ): Promise<{ notifs: WalletNotification[] }> { + const notifs: WalletNotification[] = []; + const refundRecord = await tx.refundGroups.get(this.refundGroupId); + if (!refundRecord) { + return { notifs }; + } + await tx.refundGroups.delete(this.refundGroupId); + await this.updateTransactionMeta(tx); + await tx.tombstones.put({ id: this.transactionId }); + const items = await tx.refundItems.indexes.byRefundGroupId.getAll([ + refundRecord.refundGroupId, + ]); + for (const item of items) { + if (item.id != null) { + await tx.refundItems.delete(item.id); + } + } + return { notifs }; } suspendTransaction(): Promise<void> { @@ -1508,6 +1553,8 @@ async function handleInsufficientFunds( coinPubs: res.coinSel.coins.map((x) => x.coinPub), }; payInfo.payCoinSelectionUid = encodeCrock(getRandomBytes(32)); + p.exchanges = [...new Set(res.coinSel.coins.map((x) => x.exchangeBaseUrl))]; + p.exchanges.sort(); await tx.purchases.put(p); await ctx.updateTransactionMeta(tx); await spendCoins(wex, tx, { @@ -2275,6 +2322,12 @@ export async function confirmPay( ), coinPubs: selectCoinsResult.coinSel.coins.map((x) => x.coinPub), }; + p.exchanges = [ + ...new Set( + selectCoinsResult.coinSel.coins.map((x) => x.exchangeBaseUrl), + ), + ]; + p.exchanges.sort(); p.payInfo.payCoinSelectionUid = encodeCrock(getRandomBytes(16)); } p.lastSessionId = sessionId; diff --git a/packages/taler-wallet-core/src/pay-peer-pull-credit.ts b/packages/taler-wallet-core/src/pay-peer-pull-credit.ts @@ -45,6 +45,7 @@ import { TransactionState, TransactionType, WalletAccountMergeFlags, + WalletNotification, assertUnreachable, checkDbInvariant, codecForAccountKycStatus, @@ -66,7 +67,6 @@ import { TaskIdStr, TaskIdentifiers, TaskRunResult, - TombstoneTag, TransactionContext, TransitionResult, TransitionResultType, @@ -110,6 +110,7 @@ import { } from "./transactions.js"; import { WalletExecutionContext } from "./wallet.js"; import { + WithdrawTransactionContext, getExchangeWithdrawalInfo, internalCreateWithdrawalGroup, waitWithdrawalFinal, @@ -372,41 +373,62 @@ export class PeerPullCreditTransactionContext implements TransactionContext { } async deleteTransaction(): Promise<void> { - const { wex: ws, pursePub } = this; - await ws.db.runReadWriteTx( + const res = await this.wex.db.runReadWriteTx( { storeNames: [ "withdrawalGroups", "peerPullCredit", + "planchets", "tombstones", "transactionsMeta", ], }, async (tx) => { - const pullIni = await tx.peerPullCredit.get(pursePub); - if (!pullIni) { - return; - } - if (pullIni.withdrawalGroupId) { - const withdrawalGroupId = pullIni.withdrawalGroupId; - const withdrawalGroupRecord = - await tx.withdrawalGroups.get(withdrawalGroupId); - if (withdrawalGroupRecord) { - await tx.withdrawalGroups.delete(withdrawalGroupId); - await tx.tombstones.put({ - id: TombstoneTag.DeleteWithdrawalGroup + ":" + withdrawalGroupId, - }); - } - } - await tx.peerPullCredit.delete(pursePub); - await this.updateTransactionMeta(tx); - await tx.tombstones.put({ - id: TombstoneTag.DeletePeerPullCredit + ":" + pursePub, - }); + return this.deleteTransactionInTx(tx); }, ); + for (const notif of res.notifs) { + this.wex.ws.notify(notif); + } + } - return; + async deleteTransactionInTx( + tx: WalletDbReadWriteTransaction< + [ + "withdrawalGroups", + "peerPullCredit", + "planchets", + "tombstones", + "transactionsMeta", + ] + >, + ): Promise<{ notifs: WalletNotification[] }> { + const notifs: WalletNotification[] = []; + const rec = await tx.peerPullCredit.get(this.pursePub); + if (!rec) { + return { notifs }; + } + const oldTxState = computePeerPullCreditTransactionState(rec); + if (rec.withdrawalGroupId) { + const withdrawalGroupId = rec.withdrawalGroupId; + const withdrawalCtx = new WithdrawTransactionContext( + this.wex, + withdrawalGroupId, + ); + const res = await withdrawalCtx.deleteTransactionInTx(tx); + notifs.push(...res.notifs); + } + await tx.peerPullCredit.delete(rec.pursePub); + await this.updateTransactionMeta(tx); + notifs.push({ + type: NotificationType.TransactionStateTransition, + transactionId: this.transactionId, + oldTxState, + newTxState: { + major: TransactionMajorState.Deleted, + }, + }); + return { notifs }; } async suspendTransaction(): Promise<void> { diff --git a/packages/taler-wallet-core/src/pay-peer-pull-debit.ts b/packages/taler-wallet-core/src/pay-peer-pull-debit.ts @@ -32,6 +32,7 @@ import { ExchangePurseDeposits, HttpStatusCode, Logger, + NotificationType, ObservabilityEventType, PeerContractTerms, PreparePeerPullDebitRequest, @@ -50,6 +51,7 @@ import { TransactionMinorState, TransactionState, TransactionType, + WalletNotification, assertUnreachable, checkDbInvariant, checkLogicInvariant, @@ -194,20 +196,41 @@ export class PeerPullDebitTransactionContext implements TransactionContext { } async deleteTransaction(): Promise<void> { - const transactionId = this.transactionId; - const ws = this.wex; - const peerPullDebitId = this.peerPullDebitId; - await ws.db.runReadWriteTx( - { storeNames: ["peerPullDebit", "tombstones", "transactionsMeta"] }, + const res = await this.wex.db.runReadWriteTx( + { + storeNames: ["peerPullDebit", "tombstones", "transactionsMeta"], + }, async (tx) => { - const debit = await tx.peerPullDebit.get(peerPullDebitId); - if (debit) { - await tx.peerPullDebit.delete(peerPullDebitId); - await this.updateTransactionMeta(tx); - await tx.tombstones.put({ id: transactionId }); - } + return this.deleteTransactionInTx(tx); }, ); + for (const notif of res.notifs) { + this.wex.ws.notify(notif); + } + } + + async deleteTransactionInTx( + tx: WalletDbReadWriteTransaction< + ["peerPullDebit", "tombstones", "transactionsMeta"] + >, + ): Promise<{ notifs: WalletNotification[] }> { + const notifs: WalletNotification[] = []; + const rec = await tx.peerPullDebit.get(this.peerPullDebitId); + if (!rec) { + return { notifs }; + } + const oldTxState = computePeerPullDebitTransactionState(rec); + await tx.peerPullDebit.delete(rec.peerPullDebitId); + await this.updateTransactionMeta(tx); + notifs.push({ + type: NotificationType.TransactionStateTransition, + transactionId: this.transactionId, + oldTxState, + newTxState: { + major: TransactionMajorState.Deleted, + }, + }); + return { notifs }; } async suspendTransaction(): Promise<void> { diff --git a/packages/taler-wallet-core/src/pay-peer-push-credit.ts b/packages/taler-wallet-core/src/pay-peer-push-credit.ts @@ -39,6 +39,7 @@ import { TransactionState, TransactionType, WalletAccountMergeFlags, + WalletNotification, assertUnreachable, checkDbInvariant, codecForAccountKycStatus, @@ -63,7 +64,6 @@ import { TaskIdStr, TaskIdentifiers, TaskRunResult, - TombstoneTag, TransactionContext, TransitionResult, TransitionResultType, @@ -111,6 +111,7 @@ import { import { WalletExecutionContext } from "./wallet.js"; import { PerformCreateWithdrawalGroupResult, + WithdrawTransactionContext, getExchangeWithdrawalInfo, internalPerformCreateWithdrawalGroup, internalPrepareCreateWithdrawalGroup, @@ -348,40 +349,62 @@ export class PeerPushCreditTransactionContext implements TransactionContext { } async deleteTransaction(): Promise<void> { - const { wex, peerPushCreditId } = this; - await wex.db.runReadWriteTx( + const res = await this.wex.db.runReadWriteTx( { storeNames: [ "withdrawalGroups", + "planchets", "peerPushCredit", "tombstones", "transactionsMeta", ], }, async (tx) => { - const pushInc = await tx.peerPushCredit.get(peerPushCreditId); - if (!pushInc) { - return; - } - if (pushInc.withdrawalGroupId) { - const withdrawalGroupId = pushInc.withdrawalGroupId; - const withdrawalGroupRecord = - await tx.withdrawalGroups.get(withdrawalGroupId); - if (withdrawalGroupRecord) { - await tx.withdrawalGroups.delete(withdrawalGroupId); - await tx.tombstones.put({ - id: TombstoneTag.DeleteWithdrawalGroup + ":" + withdrawalGroupId, - }); - } - } - await tx.peerPushCredit.delete(peerPushCreditId); - await this.updateTransactionMeta(tx); - await tx.tombstones.put({ - id: TombstoneTag.DeletePeerPushCredit + ":" + peerPushCreditId, - }); + return this.deleteTransactionInTx(tx); }, ); - return; + for (const notif of res.notifs) { + this.wex.ws.notify(notif); + } + } + + async deleteTransactionInTx( + tx: WalletDbReadWriteTransaction< + [ + "withdrawalGroups", + "planchets", + "peerPushCredit", + "tombstones", + "transactionsMeta", + ] + >, + ): Promise<{ notifs: WalletNotification[] }> { + const notifs: WalletNotification[] = []; + const rec = await tx.peerPushCredit.get(this.peerPushCreditId); + if (!rec) { + return { notifs }; + } + const oldTxState = computePeerPushCreditTransactionState(rec); + if (rec.withdrawalGroupId) { + const withdrawalGroupId = rec.withdrawalGroupId; + const withdrawalCtx = new WithdrawTransactionContext( + this.wex, + withdrawalGroupId, + ); + const res = await withdrawalCtx.deleteTransactionInTx(tx); + notifs.push(...res.notifs); + } + await tx.peerPushCredit.delete(rec.peerPushCreditId); + await this.updateTransactionMeta(tx); + notifs.push({ + type: NotificationType.TransactionStateTransition, + transactionId: this.transactionId, + oldTxState, + newTxState: { + major: TransactionMajorState.Deleted, + }, + }); + return { notifs }; } async suspendTransaction(): Promise<void> { diff --git a/packages/taler-wallet-core/src/pay-peer-push-debit.ts b/packages/taler-wallet-core/src/pay-peer-push-debit.ts @@ -26,6 +26,7 @@ import { InitiatePeerPushDebitRequest, InitiatePeerPushDebitResponse, Logger, + NotificationType, RefreshReason, ScopeInfo, ScopeType, @@ -43,6 +44,7 @@ import { TransactionMinorState, TransactionState, TransactionType, + WalletNotification, assertUnreachable, checkDbInvariant, encodeCrock, @@ -197,18 +199,41 @@ export class PeerPushDebitTransactionContext implements TransactionContext { } async deleteTransaction(): Promise<void> { - const { wex, pursePub, transactionId } = this; - await wex.db.runReadWriteTx( - { storeNames: ["peerPushDebit", "tombstones", "transactionsMeta"] }, + const res = await this.wex.db.runReadWriteTx( + { + storeNames: ["peerPushDebit", "tombstones", "transactionsMeta"], + }, async (tx) => { - const debit = await tx.peerPushDebit.get(pursePub); - if (debit) { - await tx.peerPushDebit.delete(pursePub); - await this.updateTransactionMeta(tx); - await tx.tombstones.put({ id: transactionId }); - } + return this.deleteTransactionInTx(tx); }, ); + for (const notif of res.notifs) { + this.wex.ws.notify(notif); + } + } + + async deleteTransactionInTx( + tx: WalletDbReadWriteTransaction< + ["peerPushDebit", "tombstones", "transactionsMeta"] + >, + ): Promise<{ notifs: WalletNotification[] }> { + const notifs: WalletNotification[] = []; + const rec = await tx.peerPushDebit.get(this.pursePub); + if (!rec) { + return { notifs }; + } + const oldTxState = computePeerPushDebitTransactionState(rec); + await tx.peerPushDebit.delete(rec.pursePub); + await this.updateTransactionMeta(tx); + notifs.push({ + type: NotificationType.TransactionStateTransition, + transactionId: this.transactionId, + oldTxState, + newTxState: { + major: TransactionMajorState.Deleted, + }, + }); + return { notifs }; } async suspendTransaction(): Promise<void> { diff --git a/packages/taler-wallet-core/src/recoup.ts b/packages/taler-wallet-core/src/recoup.ts @@ -34,6 +34,7 @@ import { TransactionIdStr, TransactionType, URL, + WalletNotification, checkDbInvariant, codecForRecoupConfirmation, codecForReserveStatus, @@ -532,8 +533,38 @@ export class RecoupTransactionContext implements TransactionContext { throw new Error("Method not implemented."); } - deleteTransaction(): Promise<void> { - throw new Error("Method not implemented."); + async deleteTransaction(): Promise<void> { + const res = await this.wex.db.runReadWriteTx( + { + storeNames: [ + "recoupGroups", + "tombstones", + "exchanges", + "transactionsMeta", + ], + }, + async (tx) => { + return this.deleteTransactionInTx(tx); + }, + ); + for (const notif of res.notifs) { + this.wex.ws.notify(notif); + } + } + + async deleteTransactionInTx( + tx: WalletDbReadWriteTransaction< + ["recoupGroups", "tombstones", "exchanges", "transactionsMeta"] + >, + ): Promise<{ notifs: WalletNotification[] }> { + const notifs: WalletNotification[] = []; + const rec = await tx.recoupGroups.get(this.recoupGroupId); + if (!rec) { + return { notifs }; + } + await tx.recoupGroups.delete(this.recoupGroupId); + await this.updateTransactionMeta(tx); + return { notifs }; } lookupFullTransaction( diff --git a/packages/taler-wallet-core/src/refresh.ts b/packages/taler-wallet-core/src/refresh.ts @@ -81,7 +81,6 @@ import { TaskIdStr, TaskRunResult, TaskRunResultType, - TombstoneTag, TransactionContext, TransitionResult, TransitionResultType, @@ -291,20 +290,49 @@ export class RefreshTransactionContext implements TransactionContext { } async deleteTransaction(): Promise<void> { - await this.transition( + const res = await this.wex.db.runReadWriteTx( { - extraStores: ["tombstones"], + storeNames: ["refreshGroups", "refreshSessions", "tombstones"], }, - async (rec, tx) => { - if (!rec) { - return TransitionResult.stay(); - } - await tx.tombstones.put({ - id: TombstoneTag.DeleteRefreshGroup + ":" + this.refreshGroupId, - }); - return TransitionResult.delete(); + async (tx) => { + return this.deleteTransactionInTx(tx); }, ); + for (const notif of res.notifs) { + this.wex.ws.notify(notif); + } + } + + async deleteTransactionInTx( + tx: WalletDbReadWriteTransaction< + ["refreshGroups", "refreshSessions", "tombstones"] + >, + ): Promise<{ notifs: WalletNotification[] }> { + const notifs: WalletNotification[] = []; + const rg = await tx.refreshGroups.get(this.refreshGroupId); + if (!rg) { + logger.warn( + `unable to delete transaction ${this.transactionId}, not found`, + ); + return { notifs }; + } + const oldTxState = computeRefreshTransactionState(rg); + const sessions = await tx.refreshSessions.indexes.byRefreshGroupId.getAll( + rg.refreshGroupId, + ); + for (const s of sessions) { + await tx.refreshSessions.delete([s.refreshGroupId, s.coinIndex]); + } + await tx.refreshGroups.delete(rg.refreshGroupId); + notifs.push({ + type: NotificationType.TransactionStateTransition, + transactionId: this.transactionId, + oldTxState, + newTxState: { + major: TransactionMajorState.Deleted, + }, + }); + return { notifs }; } async suspendTransaction(): Promise<void> { diff --git a/packages/taler-wallet-core/src/shepherd.ts b/packages/taler-wallet-core/src/shepherd.ts @@ -743,7 +743,6 @@ async function getTransactionState( "peerPullDebit", "peerPushDebit", "peerPushCredit", - "rewards", "refreshGroups", "denomLossEvents", ] diff --git a/packages/taler-wallet-core/src/withdraw.ts b/packages/taler-wallet-core/src/withdraw.ts @@ -117,7 +117,6 @@ import { TaskIdStr, TaskRunResult, TaskRunResultType, - TombstoneTag, TransactionContext, TransitionResult, TransitionResultType, @@ -560,24 +559,52 @@ export class WithdrawTransactionContext implements TransactionContext { } async deleteTransaction(): Promise<void> { - await this.transition( + const res = await this.wex.db.runReadWriteTx( { - extraStores: ["tombstones"], - transactionLabel: "delete-transaction-withdraw", + storeNames: [ + "withdrawalGroups", + "planchets", + "tombstones", + "transactionsMeta", + ], }, - async (rec, tx) => { - if (!rec) { - return TransitionResult.stay(); - } - if (rec) { - await tx.tombstones.put({ - id: - TombstoneTag.DeleteWithdrawalGroup + ":" + rec.withdrawalGroupId, - }); - } - return TransitionResult.delete(); + async (tx) => { + return this.deleteTransactionInTx(tx); }, ); + for (const notif of res.notifs) { + this.wex.ws.notify(notif); + } + } + + async deleteTransactionInTx( + tx: WalletDbReadWriteTransaction< + ["withdrawalGroups", "planchets", "tombstones", "transactionsMeta"] + >, + ): Promise<{ notifs: WalletNotification[] }> { + const notifs: WalletNotification[] = []; + const rec = await tx.withdrawalGroups.get(this.withdrawalGroupId); + if (!rec) { + return { notifs }; + } + const oldTxState = computeWithdrawalTransactionStatus(rec); + await tx.withdrawalGroups.delete(rec.withdrawalGroupId); + const planchets = await tx.planchets.indexes.byGroup.getAll( + rec.withdrawalGroupId, + ); + for (const p of planchets) { + await tx.planchets.delete(p.coinPub); + } + await this.updateTransactionMeta(tx); + notifs.push({ + type: NotificationType.TransactionStateTransition, + transactionId: this.transactionId, + oldTxState, + newTxState: { + major: TransactionMajorState.Deleted, + }, + }); + return { notifs }; } async suspendTransaction(): Promise<void> {