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.
*/
AbortedExchange = 60,
AbortedBank = 61,
}
/**

View File

@ -316,6 +316,7 @@ export async function abortWithdrawalTransaction(
case WithdrawalGroupStatus.Finished:
case WithdrawalGroupStatus.FailedBankAborted:
case WithdrawalGroupStatus.AbortedExchange:
case WithdrawalGroupStatus.AbortedBank:
case WithdrawalGroupStatus.FailedAbortingBank:
// Not allowed
throw Error("abort not allowed in current state");
@ -481,6 +482,12 @@ export function computeWithdrawalTransactionStatus(
major: TransactionMajorState.Aborted,
minor: TransactionMinorState.Exchange,
};
case WithdrawalGroupStatus.AbortedBank:
return {
major: TransactionMajorState.Aborted,
minor: TransactionMinorState.Bank,
};
}
}
@ -507,7 +514,7 @@ export function computeWithdrawalTransactionActions(
case WithdrawalGroupStatus.SuspendedQueryingStatus:
return [TransactionAction.Resume, TransactionAction.Abort];
case WithdrawalGroupStatus.SuspendedRegisteringBank:
return [TransactionAction.Resume, TransactionAction.Abort]
return [TransactionAction.Resume, TransactionAction.Abort];
case WithdrawalGroupStatus.SuspendedWaitConfirmBank:
return [TransactionAction.Resume, TransactionAction.Abort];
case WithdrawalGroupStatus.SuspendedReady:
@ -519,11 +526,13 @@ export function computeWithdrawalTransactionActions(
case WithdrawalGroupStatus.SuspendedAml:
return [TransactionAction.Resume, TransactionAction.Abort];
case WithdrawalGroupStatus.SuspendedKyc:
return [TransactionAction.Resume, TransactionAction.Abort]
return [TransactionAction.Resume, TransactionAction.Abort];
case WithdrawalGroupStatus.FailedAbortingBank:
return [TransactionAction.Delete];
case WithdrawalGroupStatus.AbortedExchange:
return [TransactionAction.Delete];
case WithdrawalGroupStatus.AbortedBank:
return [TransactionAction.Delete];
}
}
@ -1270,87 +1279,61 @@ export interface WithdrawalGroupContext {
wgRecord: WithdrawalGroupRecord;
}
export async function processWithdrawalGroup(
async function processWithdrawalGroupAbortingBank(
ws: InternalWalletState,
withdrawalGroupId: string,
withdrawalGroup: WithdrawalGroupRecord,
): 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);
const { withdrawalGroupId } = withdrawalGroup;
const transactionId = constructTransactionIdentifier({
tag: TransactionType.Withdrawal,
withdrawalGroupId,
});
// We're already running!
if (ws.activeLongpoll[retryTag]) {
logger.info("withdrawal group already in long-polling, returning!");
return {
type: OperationAttemptResultType.Longpoll,
};
const wgInfo = withdrawalGroup.wgInfo;
if (wgInfo.withdrawalType != WithdrawalRecordType.BankIntegrated) {
throw Error("invalid state (aborting(bank) without bank info");
}
const abortUrl = getBankAbortUrl(wgInfo.bankInfo.talerWithdrawUri);
logger.info(`aborting withdrawal at ${abortUrl}`);
const abortResp = await ws.http.fetch(abortUrl, {
method: "POST",
body: {},
});
logger.info(`abort response status: ${abortResp.status}`);
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,
};
}
const transitionInfo = await ws.db
.mktx((x) => [x.withdrawalGroups])
.runReadWrite(async (tx) => {
const wg = await tx.withdrawalGroups.get(withdrawalGroupId);
if (!wg) {
return undefined;
}
break;
}
case WithdrawalGroupStatus.FailedBankAborted: {
// FIXME
const txStatusOld = computeWithdrawalTransactionStatus(wg);
wg.status = WithdrawalGroupStatus.AbortedBank;
wg.timestampFinish = TalerPreciseTimestamp.now();
const txStatusNew = computeWithdrawalTransactionStatus(wg);
await tx.withdrawalGroups.put(wg);
return {
type: OperationAttemptResultType.Pending,
result: undefined,
oldTxState: txStatusOld,
newTxState: txStatusNew,
};
}
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}`,
);
}
});
notifyTransition(ws, transactionId, transitionInfo);
return {
type: OperationAttemptResultType.Finished,
result: undefined,
};
}
async function processWithdrawalGroupPendingReady(
ws: InternalWalletState,
withdrawalGroup: WithdrawalGroupRecord,
): Promise<OperationAttemptResult> {
const { withdrawalGroupId } = withdrawalGroup;
const transactionId = constructTransactionIdentifier({
tag: TransactionType.Withdrawal,
withdrawalGroupId,
});
await ws.exchangeOps.updateExchangeFromUrl(
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(
ws: InternalWalletState,
exchangeUrl: string,
@ -1890,6 +1952,18 @@ export function getBankStatusUrl(talerWithdrawUri: string): string {
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(
ws: InternalWalletState,
withdrawalGroupId: string,