wallet-core: try to abort withdrawals wallet-side with the bank

This commit is contained in:
Florian Dold 2023-06-06 17:07:09 +02:00
parent f9c33136b4
commit 002ab0dab7
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
2 changed files with 147 additions and 71 deletions

View File

@ -210,6 +210,8 @@ export enum WithdrawalGroupStatus {
* wired or not. * wired or not.
*/ */
AbortedExchange = 60, AbortedExchange = 60,
AbortedBank = 61,
} }
/** /**

View File

@ -316,6 +316,7 @@ export async function abortWithdrawalTransaction(
case WithdrawalGroupStatus.Finished: case WithdrawalGroupStatus.Finished:
case WithdrawalGroupStatus.FailedBankAborted: case WithdrawalGroupStatus.FailedBankAborted:
case WithdrawalGroupStatus.AbortedExchange: case WithdrawalGroupStatus.AbortedExchange:
case WithdrawalGroupStatus.AbortedBank:
case WithdrawalGroupStatus.FailedAbortingBank: case WithdrawalGroupStatus.FailedAbortingBank:
// Not allowed // Not allowed
throw Error("abort not allowed in current state"); throw Error("abort not allowed in current state");
@ -481,6 +482,12 @@ export function computeWithdrawalTransactionStatus(
major: TransactionMajorState.Aborted, major: TransactionMajorState.Aborted,
minor: TransactionMinorState.Exchange, minor: TransactionMinorState.Exchange,
}; };
case WithdrawalGroupStatus.AbortedBank:
return {
major: TransactionMajorState.Aborted,
minor: TransactionMinorState.Bank,
};
} }
} }
@ -507,7 +514,7 @@ export function computeWithdrawalTransactionActions(
case WithdrawalGroupStatus.SuspendedQueryingStatus: case WithdrawalGroupStatus.SuspendedQueryingStatus:
return [TransactionAction.Resume, TransactionAction.Abort]; return [TransactionAction.Resume, TransactionAction.Abort];
case WithdrawalGroupStatus.SuspendedRegisteringBank: case WithdrawalGroupStatus.SuspendedRegisteringBank:
return [TransactionAction.Resume, TransactionAction.Abort] return [TransactionAction.Resume, TransactionAction.Abort];
case WithdrawalGroupStatus.SuspendedWaitConfirmBank: case WithdrawalGroupStatus.SuspendedWaitConfirmBank:
return [TransactionAction.Resume, TransactionAction.Abort]; return [TransactionAction.Resume, TransactionAction.Abort];
case WithdrawalGroupStatus.SuspendedReady: case WithdrawalGroupStatus.SuspendedReady:
@ -519,11 +526,13 @@ export function computeWithdrawalTransactionActions(
case WithdrawalGroupStatus.SuspendedAml: case WithdrawalGroupStatus.SuspendedAml:
return [TransactionAction.Resume, TransactionAction.Abort]; return [TransactionAction.Resume, TransactionAction.Abort];
case WithdrawalGroupStatus.SuspendedKyc: case WithdrawalGroupStatus.SuspendedKyc:
return [TransactionAction.Resume, TransactionAction.Abort] return [TransactionAction.Resume, TransactionAction.Abort];
case WithdrawalGroupStatus.FailedAbortingBank: case WithdrawalGroupStatus.FailedAbortingBank:
return [TransactionAction.Delete]; return [TransactionAction.Delete];
case WithdrawalGroupStatus.AbortedExchange: case WithdrawalGroupStatus.AbortedExchange:
return [TransactionAction.Delete]; return [TransactionAction.Delete];
case WithdrawalGroupStatus.AbortedBank:
return [TransactionAction.Delete];
} }
} }
@ -1270,87 +1279,61 @@ export interface WithdrawalGroupContext {
wgRecord: WithdrawalGroupRecord; wgRecord: WithdrawalGroupRecord;
} }
export async function processWithdrawalGroup( async function processWithdrawalGroupAbortingBank(
ws: InternalWalletState, ws: InternalWalletState,
withdrawalGroupId: string, withdrawalGroup: WithdrawalGroupRecord,
): Promise<OperationAttemptResult> { ): Promise<OperationAttemptResult> {
logger.trace("processing withdrawal group", withdrawalGroupId); const { withdrawalGroupId } = withdrawalGroup;
const withdrawalGroup = await ws.db
.mktx((x) => [x.withdrawalGroups])
.runReadOnly(async (tx) => {
return tx.withdrawalGroups.get(withdrawalGroupId);
});
if (!withdrawalGroup) {
throw Error(`withdrawal group ${withdrawalGroupId} not found`);
}
const retryTag = TaskIdentifiers.forWithdrawal(withdrawalGroup);
const transactionId = constructTransactionIdentifier({ const transactionId = constructTransactionIdentifier({
tag: TransactionType.Withdrawal, tag: TransactionType.Withdrawal,
withdrawalGroupId, withdrawalGroupId,
}); });
// We're already running! const wgInfo = withdrawalGroup.wgInfo;
if (ws.activeLongpoll[retryTag]) { if (wgInfo.withdrawalType != WithdrawalRecordType.BankIntegrated) {
logger.info("withdrawal group already in long-polling, returning!"); throw Error("invalid state (aborting(bank) without bank info");
return {
type: OperationAttemptResultType.Longpoll,
};
} }
const abortUrl = getBankAbortUrl(wgInfo.bankInfo.talerWithdrawUri);
switch (withdrawalGroup.status) { logger.info(`aborting withdrawal at ${abortUrl}`);
case WithdrawalGroupStatus.PendingRegisteringBank: const abortResp = await ws.http.fetch(abortUrl, {
await processReserveBankStatus(ws, withdrawalGroupId); method: "POST",
// FIXME: This will get called by the main task loop, why call it here?! body: {},
return await processWithdrawalGroup(ws, withdrawalGroupId);
case WithdrawalGroupStatus.PendingQueryingStatus: {
runLongpollAsync(ws, retryTag, (ct) => {
return queryReserve(ws, withdrawalGroupId, ct);
}); });
logger.trace( logger.info(`abort response status: ${abortResp.status}`);
"returning early from withdrawal for long-polling in background",
); const transitionInfo = await ws.db
return { .mktx((x) => [x.withdrawalGroups])
type: OperationAttemptResultType.Longpoll, .runReadWrite(async (tx) => {
}; const wg = await tx.withdrawalGroups.get(withdrawalGroupId);
if (!wg) {
return undefined;
} }
case WithdrawalGroupStatus.PendingWaitConfirmBank: { const txStatusOld = computeWithdrawalTransactionStatus(wg);
const res = await processReserveBankStatus(ws, withdrawalGroupId); wg.status = WithdrawalGroupStatus.AbortedBank;
switch (res.status) { wg.timestampFinish = TalerPreciseTimestamp.now();
case BankStatusResultCode.Aborted: const txStatusNew = computeWithdrawalTransactionStatus(wg);
case BankStatusResultCode.Done: await tx.withdrawalGroups.put(wg);
return {
oldTxState: txStatusOld,
newTxState: txStatusNew,
};
});
notifyTransition(ws, transactionId, transitionInfo);
return { return {
type: OperationAttemptResultType.Finished, type: OperationAttemptResultType.Finished,
result: undefined, result: undefined,
}; };
case BankStatusResultCode.Waiting: { }
return {
type: OperationAttemptResultType.Pending, async function processWithdrawalGroupPendingReady(
result: undefined, ws: InternalWalletState,
}; withdrawalGroup: WithdrawalGroupRecord,
} ): Promise<OperationAttemptResult> {
} const { withdrawalGroupId } = withdrawalGroup;
break; const transactionId = constructTransactionIdentifier({
} tag: TransactionType.Withdrawal,
case WithdrawalGroupStatus.FailedBankAborted: { withdrawalGroupId,
// FIXME });
return {
type: OperationAttemptResultType.Pending,
result: undefined,
};
}
case WithdrawalGroupStatus.Finished:
// We can try to withdraw, nothing needs to be done with the reserve.
break;
case WithdrawalGroupStatus.PendingReady:
// Continue with the actual withdrawal!
break;
default:
throw new InvariantViolatedError(
`unknown reserve record status: ${withdrawalGroup.status}`,
);
}
await ws.exchangeOps.updateExchangeFromUrl( await ws.exchangeOps.updateExchangeFromUrl(
ws, ws,
@ -1544,6 +1527,85 @@ export async function processWithdrawalGroup(
}; };
} }
export async function processWithdrawalGroup(
ws: InternalWalletState,
withdrawalGroupId: string,
): Promise<OperationAttemptResult> {
logger.trace("processing withdrawal group", withdrawalGroupId);
const withdrawalGroup = await ws.db
.mktx((x) => [x.withdrawalGroups])
.runReadOnly(async (tx) => {
return tx.withdrawalGroups.get(withdrawalGroupId);
});
if (!withdrawalGroup) {
throw Error(`withdrawal group ${withdrawalGroupId} not found`);
}
const retryTag = TaskIdentifiers.forWithdrawal(withdrawalGroup);
// We're already running!
if (ws.activeLongpoll[retryTag]) {
logger.info("withdrawal group already in long-polling, returning!");
return {
type: OperationAttemptResultType.Longpoll,
};
}
switch (withdrawalGroup.status) {
case WithdrawalGroupStatus.PendingRegisteringBank:
await processReserveBankStatus(ws, withdrawalGroupId);
// FIXME: This will get called by the main task loop, why call it here?!
return await processWithdrawalGroup(ws, withdrawalGroupId);
case WithdrawalGroupStatus.PendingQueryingStatus: {
runLongpollAsync(ws, retryTag, (ct) => {
return queryReserve(ws, withdrawalGroupId, ct);
});
logger.trace(
"returning early from withdrawal for long-polling in background",
);
return {
type: OperationAttemptResultType.Longpoll,
};
}
case WithdrawalGroupStatus.PendingWaitConfirmBank: {
const res = await processReserveBankStatus(ws, withdrawalGroupId);
switch (res.status) {
case BankStatusResultCode.Aborted:
case BankStatusResultCode.Done:
return {
type: OperationAttemptResultType.Finished,
result: undefined,
};
case BankStatusResultCode.Waiting: {
return {
type: OperationAttemptResultType.Pending,
result: undefined,
};
}
}
break;
}
case WithdrawalGroupStatus.Finished:
case WithdrawalGroupStatus.FailedBankAborted: {
// FIXME
return {
type: OperationAttemptResultType.Pending,
result: undefined,
};
}
case WithdrawalGroupStatus.PendingReady:
// Continue with the actual withdrawal!
return await processWithdrawalGroupPendingReady(ws, withdrawalGroup);
case WithdrawalGroupStatus.AbortingBank:
return await processWithdrawalGroupAbortingBank(ws, withdrawalGroup);
default:
throw new InvariantViolatedError(
`unknown withdrawal group status: ${withdrawalGroup.status}`,
);
}
}
export async function checkWithdrawalKycStatus( export async function checkWithdrawalKycStatus(
ws: InternalWalletState, ws: InternalWalletState,
exchangeUrl: string, exchangeUrl: string,
@ -1890,6 +1952,18 @@ export function getBankStatusUrl(talerWithdrawUri: string): string {
return url.href; return url.href;
} }
export function getBankAbortUrl(talerWithdrawUri: string): string {
const uriResult = parseWithdrawUri(talerWithdrawUri);
if (!uriResult) {
throw Error(`can't parse withdrawal URL ${talerWithdrawUri}`);
}
const url = new URL(
`withdrawal-operation/${uriResult.withdrawalOperationId}/abort`,
uriResult.bankIntegrationApiBaseUrl,
);
return url.href;
}
async function registerReserveWithBank( async function registerReserveWithBank(
ws: InternalWalletState, ws: InternalWalletState,
withdrawalGroupId: string, withdrawalGroupId: string,