wallet-core: try to abort withdrawals wallet-side with the bank
This commit is contained in:
parent
f9c33136b4
commit
002ab0dab7
@ -210,6 +210,8 @@ export enum WithdrawalGroupStatus {
|
|||||||
* wired or not.
|
* wired or not.
|
||||||
*/
|
*/
|
||||||
AbortedExchange = 60,
|
AbortedExchange = 60,
|
||||||
|
|
||||||
|
AbortedBank = 61,
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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);
|
||||||
|
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) {
|
const transitionInfo = await ws.db
|
||||||
case WithdrawalGroupStatus.PendingRegisteringBank:
|
.mktx((x) => [x.withdrawalGroups])
|
||||||
await processReserveBankStatus(ws, withdrawalGroupId);
|
.runReadWrite(async (tx) => {
|
||||||
// FIXME: This will get called by the main task loop, why call it here?!
|
const wg = await tx.withdrawalGroups.get(withdrawalGroupId);
|
||||||
return await processWithdrawalGroup(ws, withdrawalGroupId);
|
if (!wg) {
|
||||||
case WithdrawalGroupStatus.PendingQueryingStatus: {
|
return undefined;
|
||||||
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;
|
const txStatusOld = computeWithdrawalTransactionStatus(wg);
|
||||||
}
|
wg.status = WithdrawalGroupStatus.AbortedBank;
|
||||||
case WithdrawalGroupStatus.FailedBankAborted: {
|
wg.timestampFinish = TalerPreciseTimestamp.now();
|
||||||
// FIXME
|
const txStatusNew = computeWithdrawalTransactionStatus(wg);
|
||||||
|
await tx.withdrawalGroups.put(wg);
|
||||||
return {
|
return {
|
||||||
type: OperationAttemptResultType.Pending,
|
oldTxState: txStatusOld,
|
||||||
result: undefined,
|
newTxState: txStatusNew,
|
||||||
};
|
};
|
||||||
}
|
});
|
||||||
case WithdrawalGroupStatus.Finished:
|
notifyTransition(ws, transactionId, transitionInfo);
|
||||||
// We can try to withdraw, nothing needs to be done with the reserve.
|
return {
|
||||||
break;
|
type: OperationAttemptResultType.Finished,
|
||||||
case WithdrawalGroupStatus.PendingReady:
|
result: undefined,
|
||||||
// Continue with the actual withdrawal!
|
};
|
||||||
break;
|
}
|
||||||
default:
|
|
||||||
throw new InvariantViolatedError(
|
async function processWithdrawalGroupPendingReady(
|
||||||
`unknown reserve record status: ${withdrawalGroup.status}`,
|
ws: InternalWalletState,
|
||||||
);
|
withdrawalGroup: WithdrawalGroupRecord,
|
||||||
}
|
): Promise<OperationAttemptResult> {
|
||||||
|
const { withdrawalGroupId } = withdrawalGroup;
|
||||||
|
const transactionId = constructTransactionIdentifier({
|
||||||
|
tag: TransactionType.Withdrawal,
|
||||||
|
withdrawalGroupId,
|
||||||
|
});
|
||||||
|
|
||||||
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,
|
||||||
|
Loading…
Reference in New Issue
Block a user