commit 0ea6d7f07beba25ba2f774ec9bc625bb75cff2a9
parent 59606ac65563fe908ac58bc67f518e2869fb1d41
Author: Florian Dold <dold@taler.net>
Date: Thu, 20 Aug 2026 19:06:50 +0200
wallet-core: report deposit abort outcomes
Diffstat:
4 files changed, 427 insertions(+), 36 deletions(-)
diff --git a/packages/taler-util/src/types-taler-wallet-transactions.ts b/packages/taler-util/src/types-taler-wallet-transactions.ts
@@ -231,6 +231,11 @@ export enum TransactionMinorState {
CreatePurse = "create-purse",
DeletePurse = "delete-purse",
Deposit = "deposit",
+ DepositAbortPartial = "deposit-abort-partial",
+ DepositAbortRecovered = "deposit-abort-recovered",
+ DepositAbortRecoveryFailed = "deposit-abort-recovery-failed",
+ DepositAbortRefundFailed = "deposit-abort-refund-failed",
+ DepositAbortTooLate = "deposit-abort-too-late",
Exchange = "exchange",
ExchangeWaitReserve = "exchange-wait-reserve",
KycAuthRequired = "kyc-auth",
@@ -380,7 +385,7 @@ export interface KycAuthTransferInfo {
/**
* Account public key.
- *
+ *
* Included in the transfer subject for some of the transfer options.
*/
accountPub: string;
diff --git a/packages/taler-wallet-core/src/db-common.ts b/packages/taler-wallet-core/src/db-common.ts
@@ -1988,6 +1988,8 @@ export enum DepositElementStatus {
Tracking = 0x0100_0001,
KycRequired = 0x0100_0002,
Wired = 0x0500_0000,
+ /** The exchange has already wired the deposit to the target account. */
+ RefundTooLate = 0x0500_0001,
RefundSuccess = 0x0503_0000,
RefundFailed = 0x0501_0000,
RefundNotFound = 0x0501_0001,
@@ -2184,10 +2186,22 @@ export enum DepositOperationStatus {
Finished = 0x0500_0000,
+ /** The abort lost the race: every selected coin was already wired. */
+ FinishedAbortTooLate = 0x0500_0001,
+
FailedDeposit = 0x0501_0000,
FailedTrack = 0x0501_0001,
+ /** Some selected coins were recovered and others were already wired. */
+ FailedAbortPartial = 0x0501_0002,
+
+ /** The abort refund succeeded, but the recovery refresh did not. */
+ FailedAbortRecovery = 0x0501_0003,
+
+ /** A permanent refund response did not prove recovery or delivery. */
+ FailedAbortRefund = 0x0501_0004,
+
AbortedDeposit = 0x0503_0000,
}
diff --git a/packages/taler-wallet-core/src/deposits.test.ts b/packages/taler-wallet-core/src/deposits.test.ts
@@ -14,11 +14,24 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>.
*/
-import { HttpStatusCode } from "@gnu-taler/taler-util";
+import {
+ HttpStatusCode,
+ TransactionMajorState,
+ TransactionMinorState,
+} from "@gnu-taler/taler-util";
import assert from "node:assert";
import { test } from "node:test";
-import { DepositElementStatus, WalletDepositGroup } from "./db-common.js";
import {
+ DepositElementStatus,
+ DepositOperationStatus,
+ RefreshCoinStatus,
+ WalletDepositGroup,
+ WalletRefreshGroup,
+} from "./db-common.js";
+import {
+ classifyDepositAbortOutcome,
+ computeDepositAbortAmountEffective,
+ computeDepositTransactionStatus,
depositRefundStatusIsRetryable,
reconstructDepositRefundRequests,
} from "./deposits.js";
@@ -87,3 +100,85 @@ test("deposit abort reconstructs 404 refund handoffs after restart", async () =>
]);
assert.strictEqual(signed.length, 1);
});
+
+test("deposit abort distinguishes delivery, recovery, and unknown failures", () => {
+ assert.strictEqual(
+ classifyDepositAbortOutcome([DepositElementStatus.RefundTooLate]),
+ "too-late",
+ );
+ assert.strictEqual(
+ classifyDepositAbortOutcome(
+ [DepositElementStatus.RefundSuccess],
+ "recovered",
+ ),
+ "recovered",
+ );
+ assert.strictEqual(
+ classifyDepositAbortOutcome(
+ [DepositElementStatus.RefundSuccess, DepositElementStatus.RefundTooLate],
+ "recovered",
+ ),
+ "partial",
+ );
+ assert.strictEqual(
+ classifyDepositAbortOutcome(
+ [DepositElementStatus.RefundNotFound],
+ "failed",
+ ),
+ "recovery-failed",
+ );
+ assert.strictEqual(
+ classifyDepositAbortOutcome(
+ [DepositElementStatus.RefundSuccess, DepositElementStatus.RefundFailed],
+ "recovered",
+ ),
+ "refund-failed",
+ );
+});
+
+test("deposit abort effective amount credits only completed recovery", () => {
+ const refreshGroup = {
+ expectedOutputPerCoin: ["TESTKUDOS:4", "TESTKUDOS:3"],
+ statusPerCoin: [RefreshCoinStatus.Finished, RefreshCoinStatus.Failed],
+ } as WalletRefreshGroup;
+
+ assert.strictEqual(
+ computeDepositAbortAmountEffective("TESTKUDOS:10", [refreshGroup]),
+ "TESTKUDOS:6",
+ );
+ assert.strictEqual(
+ computeDepositAbortAmountEffective("TESTKUDOS:10", []),
+ "TESTKUDOS:10",
+ );
+});
+
+test("deposit abort terminal states expose precise transaction minors", () => {
+ const stateFor = (operationStatus: DepositOperationStatus) =>
+ computeDepositTransactionStatus({
+ operationStatus,
+ } as WalletDepositGroup);
+
+ assert.deepStrictEqual(
+ stateFor(DepositOperationStatus.FinishedAbortTooLate),
+ {
+ major: TransactionMajorState.Done,
+ minor: TransactionMinorState.DepositAbortTooLate,
+ },
+ );
+ assert.deepStrictEqual(stateFor(DepositOperationStatus.AbortedDeposit), {
+ major: TransactionMajorState.Aborted,
+ minor: TransactionMinorState.DepositAbortRecovered,
+ });
+ assert.deepStrictEqual(stateFor(DepositOperationStatus.FailedAbortPartial), {
+ major: TransactionMajorState.Failed,
+ minor: TransactionMinorState.DepositAbortPartial,
+ });
+ assert.deepStrictEqual(stateFor(DepositOperationStatus.FailedAbortRecovery), {
+ major: TransactionMajorState.Failed,
+ minor: TransactionMinorState.DepositAbortRecoveryFailed,
+ });
+ assert.deepStrictEqual(stateFor(DepositOperationStatus.FailedAbortRefund), {
+ major: TransactionMajorState.Failed,
+ minor: TransactionMinorState.DepositAbortRefundFailed,
+ });
+});
diff --git a/packages/taler-wallet-core/src/deposits.ts b/packages/taler-wallet-core/src/deposits.ts
@@ -100,10 +100,12 @@ import {
DepositElementStatus,
DepositOperationStatus,
KycAuthTransferOptionRaw,
+ RefreshCoinStatus,
RefreshOperationStatus,
WalletDepositGroup,
WalletDepositInfoPerExchange,
WalletDepositTrackingInfo,
+ WalletRefreshGroup,
WalletWithdrawalGroup,
WithdrawalRecordType,
timestampAbsoluteFromDb,
@@ -361,6 +363,47 @@ export class DepositTransactionContext implements TransactionContext {
}
const txState = computeDepositTransactionStatus(dg);
+ let amountEffective: AmountString;
+ if (isDepositAbortState(dg.operationStatus) || dg.abortRefreshGroupId) {
+ const selectedCoins = dg.payCoinSelection
+ ? await tx.getCoinsByPubs(dg.payCoinSelection.coinPubs)
+ : [];
+ const denoms = await getDenomInfos(this.wex, tx, selectedCoins);
+ const selectedCoinValues = selectedCoins.map((coin) =>
+ denoms.get(denomRefKey(coin)),
+ );
+ const haveGrossInput =
+ dg.payCoinSelection !== undefined &&
+ selectedCoins.length === dg.payCoinSelection.coinPubs.length &&
+ selectedCoinValues.every((denom) => denom !== undefined);
+ if (haveGrossInput) {
+ const grossCoinInput = Amounts.sumOrZero(
+ dg.currency,
+ selectedCoinValues.map((denom) => denom!.value),
+ ).amount;
+ const relatedRefreshes =
+ await tx.getRefreshGroupsByOriginatingTransaction(this.transactionId);
+ amountEffective = computeDepositAbortAmountEffective(
+ Amounts.stringify(grossCoinInput),
+ relatedRefreshes,
+ );
+ } else {
+ // Old or damaged records can lack the selected coins/denominations.
+ // Fall back to the historical estimate, crediting only the dedicated
+ // abort refresh rather than inventing unavailable change outputs.
+ const recoveryRefresh = dg.abortRefreshGroupId
+ ? await tx.getRefreshGroup(dg.abortRefreshGroupId)
+ : undefined;
+ amountEffective = computeDepositAbortAmountEffective(
+ dg.totalPayCost,
+ recoveryRefresh ? [recoveryRefresh] : [],
+ );
+ }
+ } else {
+ amountEffective = isUnsuccessfulTransaction(txState)
+ ? Amounts.stringify(Amounts.zeroOfAmount(dg.totalPayCost))
+ : Amounts.stringify(dg.totalPayCost);
+ }
return {
type: TransactionType.Deposit,
txState,
@@ -371,9 +414,7 @@ export class DepositTransactionContext implements TransactionContext {
),
txActions: computeDepositTransactionActions(dg),
amountRaw: Amounts.stringify(dg.counterpartyEffectiveDepositAmount),
- amountEffective: isUnsuccessfulTransaction(txState)
- ? Amounts.stringify(Amounts.zeroOfAmount(dg.totalPayCost))
- : Amounts.stringify(dg.totalPayCost),
+ amountEffective,
timestamp: timestampPreciseFromDb(dg.timestampCreated),
targetPaytoUri: dg.wire.payto_uri,
wireTransferDeadline: timestampProtocolFromDb(dg.wireTransferDeadline),
@@ -460,6 +501,10 @@ export class DepositTransactionContext implements TransactionContext {
let newOpStatus: DepositOperationStatus | undefined;
switch (dg.operationStatus) {
case DepositOperationStatus.AbortedDeposit:
+ case DepositOperationStatus.FinishedAbortTooLate:
+ case DepositOperationStatus.FailedAbortPartial:
+ case DepositOperationStatus.FailedAbortRecovery:
+ case DepositOperationStatus.FailedAbortRefund:
case DepositOperationStatus.FailedDeposit:
case DepositOperationStatus.FailedTrack:
case DepositOperationStatus.Finished:
@@ -542,6 +587,10 @@ export class DepositTransactionContext implements TransactionContext {
case DepositOperationStatus.LegacyPendingTrack:
case DepositOperationStatus.LegacySuspendedTrack:
case DepositOperationStatus.AbortedDeposit:
+ case DepositOperationStatus.FinishedAbortTooLate:
+ case DepositOperationStatus.FailedAbortPartial:
+ case DepositOperationStatus.FailedAbortRecovery:
+ case DepositOperationStatus.FailedAbortRefund:
case DepositOperationStatus.Aborting:
case DepositOperationStatus.FailedDeposit:
case DepositOperationStatus.FailedTrack:
@@ -573,6 +622,10 @@ export class DepositTransactionContext implements TransactionContext {
let newOpStatus: DepositOperationStatus | undefined;
switch (dg.operationStatus) {
case DepositOperationStatus.AbortedDeposit:
+ case DepositOperationStatus.FinishedAbortTooLate:
+ case DepositOperationStatus.FailedAbortPartial:
+ case DepositOperationStatus.FailedAbortRecovery:
+ case DepositOperationStatus.FailedAbortRefund:
case DepositOperationStatus.Aborting:
case DepositOperationStatus.FailedDeposit:
case DepositOperationStatus.FailedTrack:
@@ -641,10 +694,15 @@ export class DepositTransactionContext implements TransactionContext {
let newState: DepositOperationStatus;
switch (dg.operationStatus) {
case DepositOperationStatus.PendingAggregateKyc:
- case DepositOperationStatus.SuspendedAggregateKyc:
+ case DepositOperationStatus.SuspendedAggregateKyc: {
+ newState = DepositOperationStatus.FailedDeposit;
+ break;
+ }
case DepositOperationStatus.SuspendedAborting:
case DepositOperationStatus.Aborting: {
- newState = DepositOperationStatus.FailedDeposit;
+ newState = dg.abortRefreshGroupId
+ ? DepositOperationStatus.FailedAbortRecovery
+ : DepositOperationStatus.FailedAbortRefund;
break;
}
case DepositOperationStatus.LegacyPendingTrack:
@@ -653,6 +711,10 @@ export class DepositTransactionContext implements TransactionContext {
break;
}
case DepositOperationStatus.AbortedDeposit:
+ case DepositOperationStatus.FinishedAbortTooLate:
+ case DepositOperationStatus.FailedAbortPartial:
+ case DepositOperationStatus.FailedAbortRecovery:
+ case DepositOperationStatus.FailedAbortRefund:
case DepositOperationStatus.FailedDeposit:
case DepositOperationStatus.FailedTrack:
case DepositOperationStatus.Finished:
@@ -697,6 +759,11 @@ export function computeDepositTransactionStatus(
return {
major: TransactionMajorState.Done,
};
+ case DepositOperationStatus.FinishedAbortTooLate:
+ return {
+ major: TransactionMajorState.Done,
+ minor: TransactionMinorState.DepositAbortTooLate,
+ };
case DepositOperationStatus.PendingDeposit:
return {
major: TransactionMajorState.Pending,
@@ -749,6 +816,22 @@ export function computeDepositTransactionStatus(
case DepositOperationStatus.AbortedDeposit:
return {
major: TransactionMajorState.Aborted,
+ minor: TransactionMinorState.DepositAbortRecovered,
+ };
+ case DepositOperationStatus.FailedAbortPartial:
+ return {
+ major: TransactionMajorState.Failed,
+ minor: TransactionMinorState.DepositAbortPartial,
+ };
+ case DepositOperationStatus.FailedAbortRecovery:
+ return {
+ major: TransactionMajorState.Failed,
+ minor: TransactionMinorState.DepositAbortRecoveryFailed,
+ };
+ case DepositOperationStatus.FailedAbortRefund:
+ return {
+ major: TransactionMajorState.Failed,
+ minor: TransactionMinorState.DepositAbortRefundFailed,
};
case DepositOperationStatus.FailedDeposit:
return {
@@ -828,6 +911,7 @@ export function computeDepositTransactionActions(
): TransactionAction[] {
switch (dg.operationStatus) {
case DepositOperationStatus.Finished:
+ case DepositOperationStatus.FinishedAbortTooLate:
return [TransactionAction.Delete];
case DepositOperationStatus.PendingDeposit:
return [
@@ -845,6 +929,9 @@ export function computeDepositTransactionActions(
];
case DepositOperationStatus.AbortedDeposit:
return [TransactionAction.Delete];
+ case DepositOperationStatus.FailedAbortPartial:
+ case DepositOperationStatus.FailedAbortRecovery:
+ case DepositOperationStatus.FailedAbortRefund:
case DepositOperationStatus.FailedDeposit:
case DepositOperationStatus.FailedTrack:
return [TransactionAction.Delete];
@@ -883,6 +970,191 @@ export function computeDepositTransactionActions(
}
}
+export type DepositAbortOutcome =
+ | "refunding"
+ | "recovering"
+ | "recovered"
+ | "too-late"
+ | "partial"
+ | "refund-failed"
+ | "recovery-failed";
+
+function isDepositAbortState(status: DepositOperationStatus): boolean {
+ switch (status) {
+ case DepositOperationStatus.Aborting:
+ case DepositOperationStatus.SuspendedAborting:
+ case DepositOperationStatus.AbortedDeposit:
+ case DepositOperationStatus.FinishedAbortTooLate:
+ case DepositOperationStatus.FailedAbortPartial:
+ case DepositOperationStatus.FailedAbortRecovery:
+ case DepositOperationStatus.FailedAbortRefund:
+ return true;
+ default:
+ return false;
+ }
+}
+
+/**
+ * Classify the financial result of a deposit abort.
+ *
+ * A successful refund and a 404 both still require refresh before the wallet
+ * has recovered value. A 410 is different: it proves that the exchange has
+ * already wired the deposit. Other permanent refund failures prove neither
+ * recovery nor delivery and therefore get their own outcome.
+ */
+export function classifyDepositAbortOutcome(
+ statusPerCoin: DepositElementStatus[],
+ recovery?: "pending" | "recovered" | "failed",
+): DepositAbortOutcome {
+ let hasRecoverable = false;
+ let hasTooLate = false;
+ let hasRefundFailure = false;
+ for (const status of statusPerCoin) {
+ switch (status) {
+ case DepositElementStatus.RefundSuccess:
+ case DepositElementStatus.RefundNotFound:
+ hasRecoverable = true;
+ break;
+ case DepositElementStatus.RefundTooLate:
+ hasTooLate = true;
+ break;
+ case DepositElementStatus.RefundFailed:
+ hasRefundFailure = true;
+ break;
+ default:
+ return "refunding";
+ }
+ }
+ if (hasRecoverable) {
+ if (recovery === undefined || recovery === "pending") {
+ return "recovering";
+ }
+ if (recovery === "failed") {
+ return "recovery-failed";
+ }
+ }
+ if (hasRefundFailure) {
+ return "refund-failed";
+ }
+ if (hasTooLate) {
+ return hasRecoverable ? "partial" : "too-late";
+ }
+ return "recovered";
+}
+
+/**
+ * Compute the net wallet loss of an abort from value that was actually
+ * recovered. This includes both the change refresh created by the original
+ * deposit and the later abort refresh. Finished refresh elements have minted
+ * their expected output; pending and failed elements are deliberately not
+ * credited here.
+ */
+export function computeDepositAbortAmountEffective(
+ grossCoinInput: AmountString,
+ relatedRefreshes: WalletRefreshGroup[],
+): AmountString {
+ const recoveredOutputs = relatedRefreshes.flatMap((refreshGroup) =>
+ refreshGroup.expectedOutputPerCoin.filter(
+ (_amount, index) =>
+ refreshGroup.statusPerCoin[index] === RefreshCoinStatus.Finished,
+ ),
+ );
+ const recoveredAmount = Amounts.sumOrZero(
+ Amounts.currencyOf(grossCoinInput),
+ recoveredOutputs,
+ ).amount;
+ return Amounts.stringify(Amounts.sub(grossCoinInput, recoveredAmount).amount);
+}
+
+function abortOutcomeStatus(
+ outcome: DepositAbortOutcome,
+): DepositOperationStatus {
+ switch (outcome) {
+ case "recovered":
+ return DepositOperationStatus.AbortedDeposit;
+ case "too-late":
+ return DepositOperationStatus.FinishedAbortTooLate;
+ case "partial":
+ return DepositOperationStatus.FailedAbortPartial;
+ case "recovery-failed":
+ return DepositOperationStatus.FailedAbortRecovery;
+ case "refund-failed":
+ return DepositOperationStatus.FailedAbortRefund;
+ case "refunding":
+ case "recovering":
+ throw Error(`deposit abort outcome ${outcome} is not final`);
+ default:
+ assertUnreachable(outcome);
+ }
+}
+
+function describeDepositAbortFailure(
+ outcome: DepositAbortOutcome,
+ statusPerCoin: DepositElementStatus[],
+ refreshGroupId?: string,
+): TalerErrorDetail | undefined {
+ const recovered = statusPerCoin.filter(
+ (x) =>
+ x === DepositElementStatus.RefundSuccess ||
+ x === DepositElementStatus.RefundNotFound,
+ ).length;
+ const delivered = statusPerCoin.filter(
+ (x) => x === DepositElementStatus.RefundTooLate,
+ ).length;
+ const unresolved = statusPerCoin.filter(
+ (x) => x === DepositElementStatus.RefundFailed,
+ ).length;
+ let message: string | undefined;
+ switch (outcome) {
+ case "partial":
+ message =
+ `deposit abort recovered ${recovered} coin(s), but ` +
+ `${delivered} coin(s) had already been wired`;
+ break;
+ case "recovery-failed":
+ message =
+ `deposit abort recovery refresh ${refreshGroupId ?? "<missing>"} ` +
+ `failed or disappeared; ${delivered} coin(s) were already wired and ` +
+ `${unresolved} refund outcome(s) remain inconclusive`;
+ break;
+ case "refund-failed":
+ message =
+ `deposit abort received a permanent, inconclusive refund failure ` +
+ `for ${unresolved} coin(s); ${delivered} coin(s) were already wired`;
+ break;
+ case "refunding":
+ case "recovering":
+ case "recovered":
+ case "too-late":
+ return undefined;
+ default:
+ assertUnreachable(outcome);
+ }
+ return makeErrorDetail(
+ TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
+ {},
+ message,
+ );
+}
+
+async function finalizeDepositAbort(
+ h: RecordHandle<WalletDepositGroup>,
+ depositGroup: WalletDepositGroup,
+ outcome: DepositAbortOutcome,
+ cause: string,
+): Promise<void> {
+ depositGroup.operationStatus = abortOutcomeStatus(outcome);
+ depositGroup.timestampFinished = timestampPreciseToDb(
+ TalerPreciseTimestamp.now(),
+ );
+ depositGroup.failReason = describeDepositAbortFailure(
+ outcome,
+ depositGroup.statusPerCoin ?? [],
+ depositGroup.abortRefreshGroupId,
+ );
+ await h.update(depositGroup, cause);
+}
+
async function makeDepositRefundRequest(
wex: WalletExecutionContext,
depositGroup: WalletDepositGroup,
@@ -978,6 +1250,7 @@ async function refundDepositGroup(
switch (st) {
case DepositElementStatus.RefundFailed:
case DepositElementStatus.RefundSuccess:
+ case DepositElementStatus.RefundTooLate:
break;
case DepositElementStatus.RefundNotFound: {
break;
@@ -1013,6 +1286,11 @@ async function refundDepositGroup(
// so the subsequent refresh request might fail.
newStatus = DepositElementStatus.RefundNotFound;
refundReqPerCoin[i] = refundReq;
+ } else if (refundResp.case === HttpStatusCode.Gone) {
+ // The exchange explicitly confirms that aggregation already won
+ // the race. This value was delivered and must not be sent to a
+ // refresh that can no longer recover it.
+ newStatus = DepositElementStatus.RefundTooLate;
} else if (depositRefundStatusIsRetryable(refundResp.case)) {
return TaskRunResult.backoff();
} else {
@@ -1036,33 +1314,33 @@ async function refundDepositGroup(
// Check if we are done trying to refund.
const res = await wex.runWalletDbTx(async (tx) => {
- const newDg = await tx.getDepositGroup(depositGroup.depositGroupId);
+ const [newDg, h] = await ctx.getRecordHandle(tx);
if (!newDg || !newDg.statusPerCoin) {
return;
}
- let refundsAllDone = true;
- for (let i = 0; i < newTxPerCoin.length; i++) {
- switch (newTxPerCoin[i]) {
- case DepositElementStatus.RefundFailed:
- case DepositElementStatus.RefundNotFound:
- case DepositElementStatus.RefundSuccess:
- break;
- default:
- refundsAllDone = false;
- }
- }
- if (!refundsAllDone) {
+ newTxPerCoin = [...newDg.statusPerCoin];
+ const outcome = classifyDepositAbortOutcome(newTxPerCoin);
+ if (outcome === "refunding") {
return;
}
- newTxPerCoin = [...newDg.statusPerCoin];
const refreshCoins: CoinRefreshRequest[] = [];
for (let i = 0; i < newTxPerCoin.length; i++) {
+ if (
+ newTxPerCoin[i] !== DepositElementStatus.RefundSuccess &&
+ newTxPerCoin[i] !== DepositElementStatus.RefundNotFound
+ ) {
+ continue;
+ }
refreshCoins.push({
amount: payCoinSelection.coinContributions[i],
coinPub: payCoinSelection.coinPubs[i],
refundRequest: refundReqPerCoin[i],
});
}
+ if (refreshCoins.length === 0) {
+ await finalizeDepositAbort(h, newDg, outcome, "refund-final");
+ return { abortFinalized: true };
+ }
const refreshRes = await createRefreshGroup(
wex,
tx,
@@ -1075,12 +1353,11 @@ async function refundDepositGroup(
}),
);
newDg.abortRefreshGroupId = refreshRes.refreshGroupId;
- await tx.upsertDepositGroup(newDg);
- await ctx.updateTransactionMeta(tx);
+ await h.update(newDg, "refund-refresh");
return { refreshRes };
});
- if (res?.refreshRes) {
+ if (res?.refreshRes || res?.abortFinalized) {
return TaskRunResult.progress();
}
@@ -1141,18 +1418,18 @@ async function waitForRefreshOnDepositGroup(
if (!newDg) {
return false;
}
- if (recovery === "recovered") {
- newDg.operationStatus = DepositOperationStatus.AbortedDeposit;
- await h.update(newDg, "refresh-done");
- } else {
- newDg.operationStatus = DepositOperationStatus.FailedDeposit;
- newDg.failReason = makeErrorDetail(
- TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
- {},
- `abort recovery refresh ${abortRefreshGroupId} failed or disappeared`,
- );
- await h.update(newDg, "refresh-failed");
+ const outcome = newDg.statusPerCoin
+ ? classifyDepositAbortOutcome(newDg.statusPerCoin, recovery)
+ : "recovery-failed";
+ if (outcome === "refunding" || outcome === "recovering") {
+ return false;
}
+ await finalizeDepositAbort(
+ h,
+ newDg,
+ outcome,
+ recovery === "recovered" ? "refresh-done" : "refresh-failed",
+ );
return true;
});
if (didTransition) {