wallet-core: implement withdrawal tx transitions

This commit is contained in:
Florian Dold 2023-05-02 10:04:58 +02:00
parent 7c04fbd806
commit c4f5c83b8e
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
3 changed files with 325 additions and 3 deletions

View File

@ -101,7 +101,8 @@ export enum TransactionMinorState {
// Placeholder until D37 is fully implemented
Unknown = "unknown",
Deposit = "deposit",
KycRequired = "kyc-required",
KycRequired = "kyc",
AmlRequired = "aml",
Track = "track",
Refresh = "refresh",
Pickup = "pickup",
@ -111,6 +112,7 @@ export enum TransactionMinorState {
BankConfirmTransfer = "bank-confirm-transfer",
WithdrawCoins = "withdraw-coins",
ExchangeWaitReserve = "exchange-wait-reserve",
AbortingBank = "aborting-bank",
}
export interface TransactionsResponse {

View File

@ -171,6 +171,16 @@ export enum WithdrawalGroupStatus {
*/
AbortingBank = 14,
/**
* Exchange wants KYC info from the user.
*/
Kyc = 16,
/**
* Exchange is doing AML checks.
*/
Aml = 17,
/**
* The corresponding withdraw record has been created.
* No further processing is done, unless explicitly requested
@ -187,7 +197,10 @@ export enum WithdrawalGroupStatus {
SuspendedWaitConfirmBank = 53,
SuspendedQueryingStatus = 54,
SuspendedReady = 55,
SuspendedAbortingBank = 55,
SuspendedAbortingBank = 56,
SuspendedKyc = 57,
SuspendedAml = 58,
FailedAbortingBank = 59,
}
/**

View File

@ -112,6 +112,7 @@ import {
OperationAttemptResult,
OperationAttemptResultType,
TaskIdentifiers,
constructTaskIdentifier,
} from "../util/retries.js";
import {
WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
@ -128,13 +129,284 @@ import {
selectForcedWithdrawalDenominations,
selectWithdrawalDenominations,
} from "../util/coinSelection.js";
import { isWithdrawableDenom } from "../index.js";
import { PendingTaskType, isWithdrawableDenom } from "../index.js";
import {
constructTransactionIdentifier,
stopLongpolling,
} from "./transactions.js";
/**
* Logger for this file.
*/
const logger = new Logger("operations/withdraw.ts");
export async function suspendWithdrawalTransaction(
ws: InternalWalletState,
withdrawalGroupId: string,
) {
const taskId = constructTaskIdentifier({
tag: PendingTaskType.Withdraw,
withdrawalGroupId,
});
stopLongpolling(ws, taskId);
const stateUpdate = await ws.db
.mktx((x) => [x.withdrawalGroups])
.runReadWrite(async (tx) => {
const wg = await tx.withdrawalGroups.get(withdrawalGroupId);
if (!wg) {
logger.warn(`withdrawal group ${withdrawalGroupId} not found`);
return;
}
let newStatus: WithdrawalGroupStatus | undefined = undefined;
switch (wg.status) {
case WithdrawalGroupStatus.Ready:
newStatus = WithdrawalGroupStatus.SuspendedReady;
break;
case WithdrawalGroupStatus.AbortingBank:
newStatus = WithdrawalGroupStatus.SuspendedAbortingBank;
break;
case WithdrawalGroupStatus.WaitConfirmBank:
newStatus = WithdrawalGroupStatus.SuspendedWaitConfirmBank;
break;
case WithdrawalGroupStatus.RegisteringBank:
newStatus = WithdrawalGroupStatus.SuspendedRegisteringBank;
break;
case WithdrawalGroupStatus.QueryingStatus:
newStatus = WithdrawalGroupStatus.QueryingStatus;
break;
case WithdrawalGroupStatus.Kyc:
newStatus = WithdrawalGroupStatus.SuspendedKyc;
break;
case WithdrawalGroupStatus.Aml:
newStatus = WithdrawalGroupStatus.SuspendedAml;
break;
default:
logger.warn(
`Unsupported 'suspend' on withdrawal transaction in status ${wg.status}`,
);
}
if (newStatus != null) {
const oldTxState = computeWithdrawalTransactionStatus(wg);
wg.status = newStatus;
const newTxState = computeWithdrawalTransactionStatus(wg);
await tx.withdrawalGroups.put(wg);
return {
oldTxState,
newTxState,
};
}
return undefined;
});
if (stateUpdate) {
ws.notify({
type: NotificationType.TransactionStateTransition,
transactionId: constructTransactionIdentifier({
tag: TransactionType.Withdrawal,
withdrawalGroupId,
}),
oldTxState: stateUpdate.oldTxState,
newTxState: stateUpdate.newTxState,
});
}
}
export async function resumeWithdrawalTransaction(
ws: InternalWalletState,
withdrawalGroupId: string,
) {
const stateUpdate = await ws.db
.mktx((x) => [x.withdrawalGroups])
.runReadWrite(async (tx) => {
const wg = await tx.withdrawalGroups.get(withdrawalGroupId);
if (!wg) {
logger.warn(`withdrawal group ${withdrawalGroupId} not found`);
return;
}
let newStatus: WithdrawalGroupStatus | undefined = undefined;
switch (wg.status) {
case WithdrawalGroupStatus.SuspendedReady:
newStatus = WithdrawalGroupStatus.Ready;
break;
case WithdrawalGroupStatus.SuspendedAbortingBank:
newStatus = WithdrawalGroupStatus.AbortingBank;
break;
case WithdrawalGroupStatus.SuspendedWaitConfirmBank:
newStatus = WithdrawalGroupStatus.WaitConfirmBank;
break;
case WithdrawalGroupStatus.SuspendedQueryingStatus:
newStatus = WithdrawalGroupStatus.QueryingStatus;
break;
case WithdrawalGroupStatus.SuspendedRegisteringBank:
newStatus = WithdrawalGroupStatus.RegisteringBank;
break;
case WithdrawalGroupStatus.SuspendedAml:
newStatus = WithdrawalGroupStatus.Aml;
break;
case WithdrawalGroupStatus.SuspendedKyc:
newStatus = WithdrawalGroupStatus.Kyc;
break;
default:
logger.warn(
`Unsupported 'resume' on withdrawal transaction in status ${wg.status}`,
);
}
if (newStatus != null) {
const oldTxState = computeWithdrawalTransactionStatus(wg);
wg.status = newStatus;
const newTxState = computeWithdrawalTransactionStatus(wg);
await tx.withdrawalGroups.put(wg);
return {
oldTxState,
newTxState,
};
}
return undefined;
});
if (stateUpdate) {
ws.notify({
type: NotificationType.TransactionStateTransition,
transactionId: constructTransactionIdentifier({
tag: TransactionType.Withdrawal,
withdrawalGroupId,
}),
oldTxState: stateUpdate.oldTxState,
newTxState: stateUpdate.newTxState,
});
}
}
export async function abortWithdrawalTransaction(
ws: InternalWalletState,
withdrawalGroupId: string,
) {
const taskId = constructTaskIdentifier({
tag: PendingTaskType.Withdraw,
withdrawalGroupId,
});
stopLongpolling(ws, taskId);
const stateUpdate = await ws.db
.mktx((x) => [x.withdrawalGroups])
.runReadWrite(async (tx) => {
const wg = await tx.withdrawalGroups.get(withdrawalGroupId);
if (!wg) {
logger.warn(`withdrawal group ${withdrawalGroupId} not found`);
return;
}
let newStatus: WithdrawalGroupStatus | undefined = undefined;
switch (wg.status) {
case WithdrawalGroupStatus.WaitConfirmBank:
case WithdrawalGroupStatus.RegisteringBank:
case WithdrawalGroupStatus.AbortingBank:
newStatus = WithdrawalGroupStatus.AbortingBank;
break;
case WithdrawalGroupStatus.Aml:
newStatus = WithdrawalGroupStatus.SuspendedAml;
break;
case WithdrawalGroupStatus.Kyc:
newStatus = WithdrawalGroupStatus.SuspendedKyc;
break;
case WithdrawalGroupStatus.QueryingStatus:
newStatus = WithdrawalGroupStatus.SuspendedQueryingStatus;
break;
case WithdrawalGroupStatus.Ready:
newStatus = WithdrawalGroupStatus.SuspendedReady;
break;
case WithdrawalGroupStatus.SuspendedAbortingBank:
case WithdrawalGroupStatus.SuspendedQueryingStatus:
case WithdrawalGroupStatus.SuspendedAml:
case WithdrawalGroupStatus.SuspendedKyc:
case WithdrawalGroupStatus.SuspendedReady:
// No transition needed
break;
case WithdrawalGroupStatus.SuspendedRegisteringBank:
case WithdrawalGroupStatus.SuspendedWaitConfirmBank:
case WithdrawalGroupStatus.Finished:
case WithdrawalGroupStatus.BankAborted:
// Not allowed
break;
}
if (newStatus != null) {
const oldTxState = computeWithdrawalTransactionStatus(wg);
wg.status = newStatus;
const newTxState = computeWithdrawalTransactionStatus(wg);
await tx.withdrawalGroups.put(wg);
return {
oldTxState,
newTxState,
};
}
return undefined;
});
if (stateUpdate) {
ws.notify({
type: NotificationType.TransactionStateTransition,
transactionId: constructTransactionIdentifier({
tag: TransactionType.Withdrawal,
withdrawalGroupId,
}),
oldTxState: stateUpdate.oldTxState,
newTxState: stateUpdate.newTxState,
});
}
}
// Called "cancel" in the spec right now,
// from suspended-aborting.
export async function cancelAbortingWithdrawalTransaction(
ws: InternalWalletState,
withdrawalGroupId: string,
) {
const taskId = constructTaskIdentifier({
tag: PendingTaskType.Withdraw,
withdrawalGroupId,
});
stopLongpolling(ws, taskId);
const stateUpdate = await ws.db
.mktx((x) => [x.withdrawalGroups])
.runReadWrite(async (tx) => {
const wg = await tx.withdrawalGroups.get(withdrawalGroupId);
if (!wg) {
logger.warn(`withdrawal group ${withdrawalGroupId} not found`);
return;
}
let newStatus: WithdrawalGroupStatus | undefined = undefined;
switch (wg.status) {
case WithdrawalGroupStatus.AbortingBank:
newStatus = WithdrawalGroupStatus.FailedAbortingBank;
break;
default:
break;
}
if (newStatus != null) {
const oldTxState = computeWithdrawalTransactionStatus(wg);
wg.status = newStatus;
const newTxState = computeWithdrawalTransactionStatus(wg);
await tx.withdrawalGroups.put(wg);
return {
oldTxState,
newTxState,
};
}
return undefined;
});
if (stateUpdate) {
ws.notify({
type: NotificationType.TransactionStateTransition,
transactionId: constructTransactionIdentifier({
tag: TransactionType.Withdrawal,
withdrawalGroupId,
}),
oldTxState: stateUpdate.oldTxState,
newTxState: stateUpdate.newTxState,
});
}
}
export function computeWithdrawalTransactionStatus(
wgRecord: WithdrawalGroupRecord,
): TransactionState {
@ -192,6 +464,41 @@ export function computeWithdrawalTransactionStatus(
major: TransactionMajorState.Suspended,
minor: TransactionMinorState.BankConfirmTransfer,
};
case WithdrawalGroupStatus.SuspendedReady: {
return {
major: TransactionMajorState.Suspended,
minor: TransactionMinorState.WithdrawCoins,
};
}
case WithdrawalGroupStatus.Aml: {
return {
major: TransactionMajorState.Pending,
minor: TransactionMinorState.AmlRequired,
};
}
case WithdrawalGroupStatus.Kyc: {
return {
major: TransactionMajorState.Pending,
minor: TransactionMinorState.KycRequired,
};
}
case WithdrawalGroupStatus.SuspendedAml: {
return {
major: TransactionMajorState.Suspended,
minor: TransactionMinorState.AmlRequired,
};
}
case WithdrawalGroupStatus.SuspendedKyc: {
return {
major: TransactionMajorState.Suspended,
minor: TransactionMinorState.KycRequired,
};
}
case WithdrawalGroupStatus.FailedAbortingBank:
return {
major: TransactionMajorState.Failed,
minor: TransactionMinorState.AbortingBank,
};
}
}