diff options
Diffstat (limited to 'packages/taler-wallet-core')
11 files changed, 216 insertions, 103 deletions
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index 07e0f4b0a..772061fb9 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -42,6 +42,7 @@ import { import { RetryInfo } from "./util/retries.js"; import { PayCoinSelection } from "./util/coinSelection.js"; import { Event, IDBDatabase } from "@gnu-taler/idb-bridge"; +import { PendingTaskInfo } from "./pending-types.js"; /** * Name of the Taler database. This is effectively the major @@ -153,6 +154,8 @@ export interface ReserveRecord { */ timestampCreated: Timestamp; + operationStatus: OperationStatus; + /** * Time when the information about this reserve was posted to the bank. * @@ -914,10 +917,19 @@ export enum RefreshCoinStatus { Frozen = "frozen", } +export enum OperationStatus { + Finished = "finished", + Pending = "pending", +} + export interface RefreshGroupRecord { + operationStatus: OperationStatus; + /** * Retry info, even present when the operation isn't active to allow indexing * on the next retry timestamp. + * + * FIXME: No, this can be optional, indexing is still possible */ retryInfo: RetryInfo; @@ -1350,6 +1362,8 @@ export interface WithdrawalGroupRecord { */ timestampFinish?: Timestamp; + operationStatus: OperationStatus; + /** * Amount including fees (i.e. the amount subtracted from the * reserve to withdraw all coins in this withdrawal session). @@ -1561,6 +1575,8 @@ export interface DepositGroupRecord { timestampFinished: Timestamp | undefined; + operationStatus: OperationStatus; + lastError: TalerErrorDetails | undefined; /** @@ -1601,6 +1617,18 @@ export interface TombstoneRecord { id: string; } +export interface BalancePerCurrencyRecord { + currency: string; + + availableNow: AmountString; + + availableExpected: AmountString; + + pendingIncoming: AmountString; + + pendingOutgoing: AmountString; +} + export const WalletStoresV1 = { coins: describeStore( describeContents<CoinRecord>("coins", { @@ -1671,7 +1699,9 @@ export const WalletStoresV1 = { describeContents<RefreshGroupRecord>("refreshGroups", { keyPath: "refreshGroupId", }), - {}, + { + byStatus: describeIndex("byStatus", "operationStatus"), + }, ), recoupGroups: describeStore( describeContents<RecoupGroupRecord>("recoupGroups", { @@ -1686,6 +1716,7 @@ export const WalletStoresV1 = { "byInitialWithdrawalGroupId", "initialWithdrawalGroupId", ), + byStatus: describeIndex("byStatus", "operationStatus"), }, ), purchases: describeStore( @@ -1716,6 +1747,7 @@ export const WalletStoresV1 = { }), { byReservePub: describeIndex("byReservePub", "reservePub"), + byStatus: describeIndex("byStatus", "operationStatus"), }, ), planchets: describeStore( @@ -1753,7 +1785,9 @@ export const WalletStoresV1 = { describeContents<DepositGroupRecord>("depositGroups", { keyPath: "depositGroupId", }), - {}, + { + byStatus: describeIndex("byStatus", "operationStatus"), + }, ), tombstones: describeStore( describeContents<TombstoneRecord>("tombstones", { keyPath: "id" }), @@ -1765,6 +1799,12 @@ export const WalletStoresV1 = { }), {}, ), + balancesPerCurrency: describeStore( + describeContents<BalancePerCurrencyRecord>("balancesPerCurrency", { + keyPath: "currency", + }), + {}, + ), }; export interface MetaConfigRecord { diff --git a/packages/taler-wallet-core/src/headless/helpers.ts b/packages/taler-wallet-core/src/headless/helpers.ts index 191c48441..d8616f716 100644 --- a/packages/taler-wallet-core/src/headless/helpers.ts +++ b/packages/taler-wallet-core/src/headless/helpers.ts @@ -37,6 +37,7 @@ import type { IDBFactory } from "@gnu-taler/idb-bridge"; import { WalletNotification } from "@gnu-taler/taler-util"; import { Wallet } from "../wallet.js"; import * as fs from "fs"; +import { AccessStats } from "@gnu-taler/idb-bridge/src/MemoryBackend"; const logger = new Logger("headless/helpers.ts"); @@ -80,6 +81,21 @@ function makeId(length: number): string { export async function getDefaultNodeWallet( args: DefaultNodeWalletArgs = {}, ): Promise<Wallet> { + const res = await getDefaultNodeWallet2(args); + return res.wallet; +} + +/** + * Get a wallet instance with default settings for node. + * + * Extended version that allows getting DB stats. + */ +export async function getDefaultNodeWallet2( + args: DefaultNodeWalletArgs = {}, +): Promise<{ + wallet: Wallet; + getDbStats: () => AccessStats; +}> { BridgeIDBFactory.enableTracing = false; const myBackend = new MemoryBackend(); myBackend.enableTracing = false; @@ -121,7 +137,7 @@ export async function getDefaultNodeWallet( BridgeIDBFactory.enableTracing = false; const myBridgeIdbFactory = new BridgeIDBFactory(myBackend); - const myIdbFactory: IDBFactory = (myBridgeIdbFactory as any) as IDBFactory; + const myIdbFactory: IDBFactory = myBridgeIdbFactory as any as IDBFactory; let myHttpLib; if (args.httpLib) { @@ -164,5 +180,8 @@ export async function getDefaultNodeWallet( if (args.notifyHandler) { w.addNotificationListener(args.notifyHandler); } - return w; + return { + wallet: w, + getDbStats: () => myBackend.accessStats, + }; } diff --git a/packages/taler-wallet-core/src/index.node.ts b/packages/taler-wallet-core/src/index.node.ts index 0860ccc26..7a6ad6a74 100644 --- a/packages/taler-wallet-core/src/index.node.ts +++ b/packages/taler-wallet-core/src/index.node.ts @@ -20,6 +20,9 @@ export * from "./index.js"; export { NodeHttpLib } from "./headless/NodeHttpLib.js"; export { getDefaultNodeWallet, + getDefaultNodeWallet2, DefaultNodeWalletArgs, } from "./headless/helpers.js"; export * from "./crypto/workers/nodeThreadWorker.js"; + +export type { AccessStats } from "@gnu-taler/idb-bridge"; diff --git a/packages/taler-wallet-core/src/operations/README.md b/packages/taler-wallet-core/src/operations/README.md index 32e2fbfc8..ca7140d6a 100644 --- a/packages/taler-wallet-core/src/operations/README.md +++ b/packages/taler-wallet-core/src/operations/README.md @@ -4,4 +4,4 @@ This folder contains the implementations for all wallet operations that operate To avoid cyclic dependencies, these files must **not** reference each other. Instead, other operations should only be accessed via injected dependencies. -Avoiding cyclic dependencies is important for module bundlers.
\ No newline at end of file +Avoiding cyclic dependencies is important for module bundlers. diff --git a/packages/taler-wallet-core/src/operations/backup/import.ts b/packages/taler-wallet-core/src/operations/backup/import.ts index 564d39797..5ca1ebb9d 100644 --- a/packages/taler-wallet-core/src/operations/backup/import.ts +++ b/packages/taler-wallet-core/src/operations/backup/import.ts @@ -47,6 +47,7 @@ import { WireInfo, WalletStoresV1, RefreshCoinStatus, + OperationStatus, } from "../../db.js"; import { PayCoinSelection } from "../../util/coinSelection.js"; import { j2s } from "@gnu-taler/taler-util"; @@ -180,8 +181,11 @@ async function getDenomSelStateFromBackup( const d = await tx.denominations.get([exchangeBaseUrl, s.denom_pub_hash]); checkBackupInvariant(!!d); totalCoinValue = Amounts.add(totalCoinValue, d.value).amount; - totalWithdrawCost = Amounts.add(totalWithdrawCost, d.value, d.feeWithdraw) - .amount; + totalWithdrawCost = Amounts.add( + totalWithdrawCost, + d.value, + d.feeWithdraw, + ).amount; } return { selectedDenoms, @@ -475,6 +479,8 @@ export async function importBackup( backupExchangeDetails.base_url, backupReserve.initial_selected_denoms, ), + // FIXME! + operationStatus: OperationStatus.Pending, }); } for (const backupWg of backupReserve.withdrawal_groups) { @@ -507,6 +513,9 @@ export async function importBackup( timestampFinish: backupWg.timestamp_finish, withdrawalGroupId: backupWg.withdrawal_group_id, denomSelUid: backupWg.selected_denoms_id, + operationStatus: backupWg.timestamp_finish + ? OperationStatus.Finished + : OperationStatus.Pending, }); } } @@ -758,7 +767,8 @@ export async function importBackup( // FIXME! payRetryInfo: initRetryInfo(), download, - paymentSubmitPending: !backupPurchase.timestamp_first_successful_pay, + paymentSubmitPending: + !backupPurchase.timestamp_first_successful_pay, refundQueryRequested: false, payCoinSelection: await recoverPayCoinSelection( tx, @@ -809,10 +819,8 @@ export async function importBackup( reason = RefreshReason.Scheduled; break; } - const refreshSessionPerCoin: ( - | RefreshSessionRecord - | undefined - )[] = []; + const refreshSessionPerCoin: (RefreshSessionRecord | undefined)[] = + []; for (const oldCoin of backupRefreshGroup.old_coins) { const c = await tx.coins.get(oldCoin.coin_pub); checkBackupInvariant(!!c); @@ -848,6 +856,9 @@ export async function importBackup( ? RefreshCoinStatus.Finished : RefreshCoinStatus.Pending, ), + operationStatus: backupRefreshGroup.timestamp_finish + ? OperationStatus.Finished + : OperationStatus.Pending, inputPerCoin: backupRefreshGroup.old_coins.map((x) => Amounts.parseOrThrow(x.input_amount), ), diff --git a/packages/taler-wallet-core/src/operations/balance.ts b/packages/taler-wallet-core/src/operations/balance.ts index 298893920..61bae8286 100644 --- a/packages/taler-wallet-core/src/operations/balance.ts +++ b/packages/taler-wallet-core/src/operations/balance.ts @@ -47,6 +47,10 @@ export async function getBalancesInsideTransaction( withdrawalGroups: typeof WalletStoresV1.withdrawalGroups; }>, ): Promise<BalancesResponse> { + return { + balances: [], + }; + const balanceStore: Record<string, WalletBalance> = {}; /** @@ -148,6 +152,9 @@ export async function getBalancesInsideTransaction( export async function getBalances( ws: InternalWalletState, ): Promise<BalancesResponse> { + return { + balances: [], + }; logger.trace("starting to compute balance"); const wbal = await ws.db diff --git a/packages/taler-wallet-core/src/operations/deposits.ts b/packages/taler-wallet-core/src/operations/deposits.ts index 0a90e0216..afe8e6f30 100644 --- a/packages/taler-wallet-core/src/operations/deposits.ts +++ b/packages/taler-wallet-core/src/operations/deposits.ts @@ -50,7 +50,7 @@ import { getRandomBytes, stringToBytes, } from "@gnu-taler/taler-util"; -import { DepositGroupRecord } from "../db.js"; +import { DepositGroupRecord, OperationStatus } from "../db.js"; import { guardOperationException } from "../errors.js"; import { PayCoinSelection, selectPayCoins } from "../util/coinSelection.js"; import { readSuccessResponseJsonOrThrow } from "../util/http.js"; @@ -281,6 +281,7 @@ async function processDepositGroupImpl( } if (allDeposited) { dg.timestampFinished = getTimestampNow(); + dg.operationStatus = OperationStatus.Finished; delete dg.lastError; delete dg.retryInfo; await tx.depositGroups.put(dg); @@ -409,11 +410,7 @@ export async function getFeeForDeposit( refund_deadline: { t_ms: 0 }, }; - const contractData = extractContractData( - contractTerms, - "", - "", - ); + const contractData = extractContractData(contractTerms, "", ""); const candidates = await getCandidatePayCoins(ws, contractData); @@ -436,7 +433,6 @@ export async function getFeeForDeposit( amount, payCoinSel, ); - } export async function createDepositGroup( @@ -570,6 +566,7 @@ export async function createDepositGroup( salt: wireSalt, }, retryInfo: initRetryInfo(), + operationStatus: OperationStatus.Pending, lastError: undefined, }; @@ -708,8 +705,10 @@ export async function getTotalFeeForDepositAmount( .filter((x) => Amounts.isSameCurrency(x.value, pcs.coinContributions[i]), ); - const amountLeft = Amounts.sub(denom.value, pcs.coinContributions[i]) - .amount; + const amountLeft = Amounts.sub( + denom.value, + pcs.coinContributions[i], + ).amount; const refreshCost = getTotalRefreshCost(allDenoms, denom, amountLeft); refreshFee.push(refreshCost); } @@ -736,8 +735,17 @@ export async function getTotalFeeForDepositAmount( }); return { - coin: coinFee.length === 0 ? Amounts.getZero(total.currency) : Amounts.sum(coinFee).amount, - wire: wireFee.length === 0 ? Amounts.getZero(total.currency) : Amounts.sum(wireFee).amount, - refresh: refreshFee.length === 0 ? Amounts.getZero(total.currency) : Amounts.sum(refreshFee).amount + coin: + coinFee.length === 0 + ? Amounts.getZero(total.currency) + : Amounts.sum(coinFee).amount, + wire: + wireFee.length === 0 + ? Amounts.getZero(total.currency) + : Amounts.sum(wireFee).amount, + refresh: + refreshFee.length === 0 + ? Amounts.getZero(total.currency) + : Amounts.sum(refreshFee).amount, }; } diff --git a/packages/taler-wallet-core/src/operations/pending.ts b/packages/taler-wallet-core/src/operations/pending.ts index e3d22bfe6..07c29e874 100644 --- a/packages/taler-wallet-core/src/operations/pending.ts +++ b/packages/taler-wallet-core/src/operations/pending.ts @@ -28,6 +28,7 @@ import { WalletStoresV1, BackupProviderStateTag, RefreshCoinStatus, + OperationStatus, } from "../db.js"; import { PendingOperationsResponse, @@ -37,6 +38,8 @@ import { import { getTimestampNow, isTimestampExpired, + j2s, + Logger, Timestamp, } from "@gnu-taler/taler-util"; import { InternalWalletState } from "../common.js"; @@ -82,33 +85,35 @@ async function gatherReservePending( now: Timestamp, resp: PendingOperationsResponse, ): Promise<void> { - await tx.reserves.iter().forEach((reserve) => { - const reserveType = reserve.bankInfo - ? ReserveType.TalerBankWithdraw - : ReserveType.Manual; - switch (reserve.reserveStatus) { - case ReserveRecordStatus.DORMANT: - // nothing to report as pending - break; - case ReserveRecordStatus.WAIT_CONFIRM_BANK: - case ReserveRecordStatus.QUERYING_STATUS: - case ReserveRecordStatus.REGISTERING_BANK: - resp.pendingOperations.push({ - type: PendingTaskType.Reserve, - givesLifeness: true, - timestampDue: reserve.retryInfo.nextRetry, - stage: reserve.reserveStatus, - timestampCreated: reserve.timestampCreated, - reserveType, - reservePub: reserve.reservePub, - retryInfo: reserve.retryInfo, - }); - break; - default: - // FIXME: report problem! - break; - } - }); + await tx.reserves.indexes.byStatus + .iter(OperationStatus.Pending) + .forEach((reserve) => { + const reserveType = reserve.bankInfo + ? ReserveType.TalerBankWithdraw + : ReserveType.Manual; + switch (reserve.reserveStatus) { + case ReserveRecordStatus.DORMANT: + // nothing to report as pending + break; + case ReserveRecordStatus.WAIT_CONFIRM_BANK: + case ReserveRecordStatus.QUERYING_STATUS: + case ReserveRecordStatus.REGISTERING_BANK: + resp.pendingOperations.push({ + type: PendingTaskType.Reserve, + givesLifeness: true, + timestampDue: reserve.retryInfo.nextRetry, + stage: reserve.reserveStatus, + timestampCreated: reserve.timestampCreated, + reserveType, + reservePub: reserve.reservePub, + retryInfo: reserve.retryInfo, + }); + break; + default: + // FIXME: report problem! + break; + } + }); } async function gatherRefreshPending( @@ -116,24 +121,26 @@ async function gatherRefreshPending( now: Timestamp, resp: PendingOperationsResponse, ): Promise<void> { - await tx.refreshGroups.iter().forEach((r) => { - if (r.timestampFinished) { - return; - } - if (r.frozen) { - return; - } - resp.pendingOperations.push({ - type: PendingTaskType.Refresh, - givesLifeness: true, - timestampDue: r.retryInfo.nextRetry, - refreshGroupId: r.refreshGroupId, - finishedPerCoin: r.statusPerCoin.map( - (x) => x === RefreshCoinStatus.Finished, - ), - retryInfo: r.retryInfo, + await tx.refreshGroups.indexes.byStatus + .iter(OperationStatus.Pending) + .forEach((r) => { + if (r.timestampFinished) { + return; + } + if (r.frozen) { + return; + } + resp.pendingOperations.push({ + type: PendingTaskType.Refresh, + givesLifeness: true, + timestampDue: r.retryInfo.nextRetry, + refreshGroupId: r.refreshGroupId, + finishedPerCoin: r.statusPerCoin.map( + (x) => x === RefreshCoinStatus.Finished, + ), + retryInfo: r.retryInfo, + }); }); - }); } async function gatherWithdrawalPending( @@ -144,29 +151,31 @@ async function gatherWithdrawalPending( now: Timestamp, resp: PendingOperationsResponse, ): Promise<void> { - await tx.withdrawalGroups.iter().forEachAsync(async (wsr) => { - if (wsr.timestampFinish) { - return; - } - let numCoinsWithdrawn = 0; - let numCoinsTotal = 0; - await tx.planchets.indexes.byGroup - .iter(wsr.withdrawalGroupId) - .forEach((x) => { - numCoinsTotal++; - if (x.withdrawalDone) { - numCoinsWithdrawn++; - } + await tx.withdrawalGroups.indexes.byStatus + .iter(OperationStatus.Pending) + .forEachAsync(async (wsr) => { + if (wsr.timestampFinish) { + return; + } + let numCoinsWithdrawn = 0; + let numCoinsTotal = 0; + await tx.planchets.indexes.byGroup + .iter(wsr.withdrawalGroupId) + .forEach((x) => { + numCoinsTotal++; + if (x.withdrawalDone) { + numCoinsWithdrawn++; + } + }); + resp.pendingOperations.push({ + type: PendingTaskType.Withdraw, + givesLifeness: true, + timestampDue: wsr.retryInfo.nextRetry, + withdrawalGroupId: wsr.withdrawalGroupId, + lastError: wsr.lastError, + retryInfo: wsr.retryInfo, }); - resp.pendingOperations.push({ - type: PendingTaskType.Withdraw, - givesLifeness: true, - timestampDue: wsr.retryInfo.nextRetry, - withdrawalGroupId: wsr.withdrawalGroupId, - lastError: wsr.lastError, - retryInfo: wsr.retryInfo, }); - }); } async function gatherProposalPending( @@ -199,20 +208,22 @@ async function gatherDepositPending( now: Timestamp, resp: PendingOperationsResponse, ): Promise<void> { - await tx.depositGroups.iter().forEach((dg) => { - if (dg.timestampFinished) { - return; - } - const timestampDue = dg.retryInfo?.nextRetry ?? getTimestampNow(); - resp.pendingOperations.push({ - type: PendingTaskType.Deposit, - givesLifeness: true, - timestampDue, - depositGroupId: dg.depositGroupId, - lastError: dg.lastError, - retryInfo: dg.retryInfo, + await tx.depositGroups.indexes.byStatus + .iter(OperationStatus.Pending) + .forEach((dg) => { + if (dg.timestampFinished) { + return; + } + const timestampDue = dg.retryInfo?.nextRetry ?? getTimestampNow(); + resp.pendingOperations.push({ + type: PendingTaskType.Deposit, + givesLifeness: true, + timestampDue, + depositGroupId: dg.depositGroupId, + lastError: dg.lastError, + retryInfo: dg.retryInfo, + }); }); - }); } async function gatherTipPending( diff --git a/packages/taler-wallet-core/src/operations/refresh.ts b/packages/taler-wallet-core/src/operations/refresh.ts index 00eaa0eac..5b589f1fa 100644 --- a/packages/taler-wallet-core/src/operations/refresh.ts +++ b/packages/taler-wallet-core/src/operations/refresh.ts @@ -26,6 +26,7 @@ import { CoinSourceType, CoinStatus, DenominationRecord, + OperationStatus, RefreshCoinStatus, RefreshGroupRecord, WalletStoresV1, @@ -127,6 +128,7 @@ function updateGroupStatus(rg: RefreshGroupRecord): void { rg.retryInfo = initRetryInfo(); } else { rg.timestampFinished = getTimestampNow(); + rg.operationStatus = OperationStatus.Finished; rg.retryInfo = initRetryInfo(); } } @@ -929,6 +931,7 @@ export async function createRefreshGroup( } const refreshGroup: RefreshGroupRecord = { + operationStatus: OperationStatus.Pending, timestampFinished: undefined, statusPerCoin: oldCoinPubs.map(() => RefreshCoinStatus.Pending), lastError: undefined, diff --git a/packages/taler-wallet-core/src/operations/reserves.ts b/packages/taler-wallet-core/src/operations/reserves.ts index 75d517d68..1550d946b 100644 --- a/packages/taler-wallet-core/src/operations/reserves.ts +++ b/packages/taler-wallet-core/src/operations/reserves.ts @@ -41,6 +41,7 @@ import { } from "@gnu-taler/taler-util"; import { InternalWalletState } from "../common.js"; import { + OperationStatus, ReserveBankInfo, ReserveRecord, ReserveRecordStatus, @@ -155,6 +156,7 @@ export async function createReserve( lastError: undefined, currency: req.amount.currency, requestedQuery: false, + operationStatus: OperationStatus.Pending, }; const exchangeInfo = await updateExchangeFromUrl(ws, req.exchange); @@ -250,6 +252,7 @@ export async function forceQueryReserve( switch (reserve.reserveStatus) { case ReserveRecordStatus.DORMANT: reserve.reserveStatus = ReserveRecordStatus.QUERYING_STATUS; + reserve.operationStatus = OperationStatus.Pending; break; default: reserve.requestedQuery = true; @@ -338,6 +341,7 @@ async function registerReserveWithBank( } r.timestampReserveInfoPosted = getTimestampNow(); r.reserveStatus = ReserveRecordStatus.WAIT_CONFIRM_BANK; + r.operationStatus = OperationStatus.Pending; if (!r.bankInfo) { throw Error("invariant failed"); } @@ -419,6 +423,7 @@ async function processReserveBankStatusImpl( const now = getTimestampNow(); r.timestampBankConfirmed = now; r.reserveStatus = ReserveRecordStatus.BANK_ABORTED; + r.operationStatus = OperationStatus.Finished; r.retryInfo = initRetryInfo(); await tx.reserves.put(r); }); @@ -455,6 +460,7 @@ async function processReserveBankStatusImpl( const now = getTimestampNow(); r.timestampBankConfirmed = now; r.reserveStatus = ReserveRecordStatus.QUERYING_STATUS; + r.operationStatus = OperationStatus.Pending; r.retryInfo = initRetryInfo(); } else { switch (r.reserveStatus) { @@ -658,6 +664,7 @@ async function updateReserve( if (denomSelInfo.selectedDenoms.length === 0) { newReserve.reserveStatus = ReserveRecordStatus.DORMANT; + newReserve.operationStatus = OperationStatus.Finished; newReserve.lastError = undefined; newReserve.retryInfo = initRetryInfo(); await tx.reserves.put(newReserve); @@ -684,11 +691,13 @@ async function updateReserve( denomsSel: denomSelectionInfoToState(denomSelInfo), secretSeed: encodeCrock(getRandomBytes(64)), denomSelUid: encodeCrock(getRandomBytes(32)), + operationStatus: OperationStatus.Pending, }; newReserve.lastError = undefined; newReserve.retryInfo = initRetryInfo(); newReserve.reserveStatus = ReserveRecordStatus.DORMANT; + newReserve.operationStatus = OperationStatus.Finished; await tx.reserves.put(newReserve); await tx.withdrawalGroups.put(withdrawalRecord); diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts index 8b72c40e8..c44435e81 100644 --- a/packages/taler-wallet-core/src/operations/withdraw.ts +++ b/packages/taler-wallet-core/src/operations/withdraw.ts @@ -53,6 +53,7 @@ import { DenomSelectionState, ExchangeDetailsRecord, ExchangeRecord, + OperationStatus, PlanchetRecord, } from "../db.js"; import { walletCoreDebugFlags } from "../util/debugFlags.js"; @@ -968,7 +969,8 @@ async function processWithdrawGroupImpl( if (wg.timestampFinish === undefined && numFinished === numTotalCoins) { finishedForFirstTime = true; wg.timestampFinish = getTimestampNow(); - wg.lastError = undefined; + wg.operationStatus = OperationStatus.Finished; + delete wg.lastError; wg.retryInfo = initRetryInfo(); } |