2019-12-02 00:42:40 +01:00
|
|
|
/*
|
|
|
|
This file is part of GNU Taler
|
2021-04-07 19:29:51 +02:00
|
|
|
(C) 2019-2021 Taler Systems SA
|
2019-12-02 00:42:40 +01:00
|
|
|
|
|
|
|
GNU Taler is free software; you can redistribute it and/or modify it under the
|
|
|
|
terms of the GNU General Public License as published by the Free Software
|
|
|
|
Foundation; either version 3, or (at your option) any later version.
|
|
|
|
|
|
|
|
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
|
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
|
|
|
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
|
|
|
|
|
|
You should have received a copy of the GNU General Public License along with
|
|
|
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
|
|
|
*/
|
|
|
|
|
2021-04-07 19:29:51 +02:00
|
|
|
/**
|
|
|
|
* Imports.
|
|
|
|
*/
|
|
|
|
import {
|
2022-03-28 23:21:49 +02:00
|
|
|
AbsoluteTime,
|
2022-08-09 15:00:45 +02:00
|
|
|
AcceptManualWithdrawalResult,
|
|
|
|
AcceptWithdrawalResponse,
|
|
|
|
addPaytoQueryParams,
|
2022-09-16 16:20:47 +02:00
|
|
|
AgeRestriction,
|
2021-04-07 19:29:51 +02:00
|
|
|
AmountJson,
|
2022-08-09 15:00:45 +02:00
|
|
|
AmountLike,
|
2021-04-07 19:29:51 +02:00
|
|
|
Amounts,
|
2021-06-17 15:49:05 +02:00
|
|
|
BankWithdrawDetails,
|
2022-09-23 18:56:21 +02:00
|
|
|
CancellationToken,
|
2022-08-09 15:00:45 +02:00
|
|
|
canonicalizeBaseUrl,
|
|
|
|
codecForBankWithdrawalOperationPostResponse,
|
|
|
|
codecForReserveStatus,
|
2021-06-17 15:49:05 +02:00
|
|
|
codecForTalerConfigResponse,
|
2023-01-10 17:31:01 +01:00
|
|
|
codecForWalletKycUuid,
|
2022-05-03 17:53:32 +02:00
|
|
|
codecForWithdrawBatchResponse,
|
2021-06-17 15:49:05 +02:00
|
|
|
codecForWithdrawOperationStatusResponse,
|
|
|
|
codecForWithdrawResponse,
|
2022-10-15 11:52:07 +02:00
|
|
|
CoinStatus,
|
2022-03-28 23:21:49 +02:00
|
|
|
DenomKeyType,
|
2022-10-14 18:40:04 +02:00
|
|
|
DenomSelectionState,
|
2022-03-28 23:21:49 +02:00
|
|
|
Duration,
|
2022-08-24 19:44:24 +02:00
|
|
|
encodeCrock,
|
2021-06-17 15:49:05 +02:00
|
|
|
ExchangeListItem,
|
2022-10-14 18:40:04 +02:00
|
|
|
ExchangeWithdrawalDetails,
|
2022-03-28 23:21:49 +02:00
|
|
|
ExchangeWithdrawRequest,
|
2022-03-29 21:21:57 +02:00
|
|
|
ForcedDenomSel,
|
2022-08-09 15:00:45 +02:00
|
|
|
getRandomBytes,
|
2022-11-01 13:39:42 +01:00
|
|
|
HttpStatusCode,
|
2022-08-09 15:00:45 +02:00
|
|
|
j2s,
|
2022-03-28 23:21:49 +02:00
|
|
|
LibtoolVersion,
|
2021-06-17 15:49:05 +02:00
|
|
|
Logger,
|
|
|
|
NotificationType,
|
2021-04-07 19:29:51 +02:00
|
|
|
parseWithdrawUri,
|
2021-06-17 15:49:05 +02:00
|
|
|
TalerErrorCode,
|
2022-03-22 21:16:38 +01:00
|
|
|
TalerErrorDetail,
|
2022-03-28 23:21:49 +02:00
|
|
|
TalerProtocolTimestamp,
|
2022-09-16 16:06:55 +02:00
|
|
|
TransactionType,
|
2022-03-28 23:21:49 +02:00
|
|
|
UnblindedSignature,
|
2021-06-20 21:14:45 +02:00
|
|
|
URL,
|
2023-02-10 13:21:37 +01:00
|
|
|
ExchangeWithdrawBatchResponse,
|
|
|
|
ExchangeWithdrawResponse,
|
2022-08-24 19:44:24 +02:00
|
|
|
WithdrawUriInfoResponse,
|
2023-02-10 13:21:37 +01:00
|
|
|
ExchangeBatchWithdrawRequest,
|
|
|
|
WalletNotification,
|
2023-04-25 23:56:57 +02:00
|
|
|
TransactionState,
|
|
|
|
TransactionMajorState,
|
|
|
|
TransactionMinorState,
|
2021-04-07 19:29:51 +02:00
|
|
|
} from "@gnu-taler/taler-util";
|
2022-08-09 15:00:45 +02:00
|
|
|
import { EddsaKeypair } from "../crypto/cryptoImplementation.js";
|
2019-12-02 00:42:40 +01:00
|
|
|
import {
|
|
|
|
CoinRecord,
|
2020-03-11 20:14:28 +01:00
|
|
|
CoinSourceType,
|
2021-06-17 15:49:05 +02:00
|
|
|
DenominationRecord,
|
2021-08-24 15:43:06 +02:00
|
|
|
DenominationVerificationStatus,
|
2023-01-17 19:59:30 +01:00
|
|
|
KycPendingInfo,
|
|
|
|
KycUserType,
|
2021-06-17 15:49:05 +02:00
|
|
|
PlanchetRecord,
|
2022-10-14 21:00:13 +02:00
|
|
|
PlanchetStatus,
|
2022-08-09 15:00:45 +02:00
|
|
|
WalletStoresV1,
|
2022-08-24 22:42:30 +02:00
|
|
|
WgInfo,
|
2022-08-24 19:44:24 +02:00
|
|
|
WithdrawalGroupRecord,
|
2022-10-08 20:56:57 +02:00
|
|
|
WithdrawalGroupStatus,
|
2022-08-24 22:17:19 +02:00
|
|
|
WithdrawalRecordType,
|
2021-06-14 16:08:58 +02:00
|
|
|
} from "../db.js";
|
2020-09-01 15:37:14 +02:00
|
|
|
import {
|
2022-03-22 21:16:38 +01:00
|
|
|
getErrorDetailFromException,
|
|
|
|
makeErrorDetail,
|
2022-08-24 19:44:24 +02:00
|
|
|
TalerError,
|
2023-02-15 23:32:42 +01:00
|
|
|
} from "@gnu-taler/taler-util";
|
2022-03-23 13:11:36 +01:00
|
|
|
import { InternalWalletState } from "../internal-wallet-state.js";
|
2022-10-08 20:56:57 +02:00
|
|
|
import {
|
|
|
|
makeCoinAvailable,
|
2022-10-15 21:26:36 +02:00
|
|
|
makeExchangeListItem,
|
2023-02-20 21:26:08 +01:00
|
|
|
runLongpollAsync,
|
2022-10-08 20:56:57 +02:00
|
|
|
runOperationWithErrorReporting,
|
|
|
|
} from "../operations/common.js";
|
2022-03-28 23:21:49 +02:00
|
|
|
import {
|
|
|
|
HttpRequestLibrary,
|
2023-02-10 13:21:37 +01:00
|
|
|
HttpResponse,
|
2022-08-09 15:00:45 +02:00
|
|
|
readSuccessResponseJsonOrErrorCode,
|
2022-03-28 23:21:49 +02:00
|
|
|
readSuccessResponseJsonOrThrow,
|
2022-08-24 19:44:24 +02:00
|
|
|
throwUnexpectedRequestError,
|
2023-02-15 23:32:42 +01:00
|
|
|
} from "@gnu-taler/taler-util/http";
|
2022-09-21 20:46:45 +02:00
|
|
|
import {
|
|
|
|
checkDbInvariant,
|
|
|
|
checkLogicInvariant,
|
|
|
|
InvariantViolatedError,
|
|
|
|
} from "../util/invariants.js";
|
2022-08-24 19:44:24 +02:00
|
|
|
import { DbAccess, GetReadOnlyAccess } from "../util/query.js";
|
2022-09-16 19:27:24 +02:00
|
|
|
import {
|
|
|
|
OperationAttemptResult,
|
|
|
|
OperationAttemptResultType,
|
2023-02-20 20:14:37 +01:00
|
|
|
TaskIdentifiers,
|
2023-05-02 10:04:58 +02:00
|
|
|
constructTaskIdentifier,
|
2022-09-16 19:27:24 +02:00
|
|
|
} from "../util/retries.js";
|
2019-12-19 20:42:49 +01:00
|
|
|
import {
|
2021-06-17 15:49:05 +02:00
|
|
|
WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
|
2022-08-24 19:44:24 +02:00
|
|
|
WALLET_EXCHANGE_PROTOCOL_VERSION,
|
2021-06-17 15:49:05 +02:00
|
|
|
} from "../versions.js";
|
2023-03-29 05:06:24 +02:00
|
|
|
import { makeTransactionId } from "./common.js";
|
2022-08-09 15:00:45 +02:00
|
|
|
import {
|
|
|
|
getExchangeDetails,
|
|
|
|
getExchangePaytoUri,
|
|
|
|
getExchangeTrust,
|
2022-08-24 19:44:24 +02:00
|
|
|
updateExchangeFromUrl,
|
2022-08-09 15:00:45 +02:00
|
|
|
} from "./exchanges.js";
|
2023-03-31 17:27:05 +02:00
|
|
|
import {
|
|
|
|
selectForcedWithdrawalDenominations,
|
|
|
|
selectWithdrawalDenominations,
|
|
|
|
} from "../util/coinSelection.js";
|
2023-05-02 10:04:58 +02:00
|
|
|
import { PendingTaskType, isWithdrawableDenom } from "../index.js";
|
|
|
|
import {
|
|
|
|
constructTransactionIdentifier,
|
|
|
|
stopLongpolling,
|
|
|
|
} from "./transactions.js";
|
2019-12-02 00:42:40 +01:00
|
|
|
|
2021-04-07 19:29:51 +02:00
|
|
|
/**
|
|
|
|
* Logger for this file.
|
|
|
|
*/
|
2022-01-12 16:54:38 +01:00
|
|
|
const logger = new Logger("operations/withdraw.ts");
|
2019-12-02 00:42:40 +01:00
|
|
|
|
2023-05-02 10:04:58 +02:00
|
|
|
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,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2023-04-25 23:56:57 +02:00
|
|
|
export function computeWithdrawalTransactionStatus(
|
|
|
|
wgRecord: WithdrawalGroupRecord,
|
|
|
|
): TransactionState {
|
|
|
|
switch (wgRecord.status) {
|
|
|
|
case WithdrawalGroupStatus.BankAborted:
|
|
|
|
return {
|
|
|
|
major: TransactionMajorState.Aborted,
|
|
|
|
};
|
|
|
|
case WithdrawalGroupStatus.Finished:
|
|
|
|
return {
|
|
|
|
major: TransactionMajorState.Done,
|
|
|
|
};
|
|
|
|
case WithdrawalGroupStatus.RegisteringBank:
|
|
|
|
return {
|
|
|
|
major: TransactionMajorState.Pending,
|
|
|
|
minor: TransactionMinorState.BankRegisterReserve,
|
|
|
|
};
|
|
|
|
case WithdrawalGroupStatus.Ready:
|
|
|
|
return {
|
|
|
|
major: TransactionMajorState.Pending,
|
|
|
|
minor: TransactionMinorState.WithdrawCoins,
|
|
|
|
};
|
|
|
|
case WithdrawalGroupStatus.QueryingStatus:
|
|
|
|
return {
|
|
|
|
major: TransactionMajorState.Pending,
|
|
|
|
minor: TransactionMinorState.ExchangeWaitReserve,
|
|
|
|
};
|
|
|
|
case WithdrawalGroupStatus.WaitConfirmBank:
|
|
|
|
return {
|
|
|
|
major: TransactionMajorState.Pending,
|
|
|
|
minor: TransactionMinorState.BankConfirmTransfer,
|
|
|
|
};
|
|
|
|
case WithdrawalGroupStatus.AbortingBank:
|
|
|
|
return {
|
|
|
|
major: TransactionMajorState.Aborting,
|
|
|
|
minor: TransactionMinorState.Bank,
|
|
|
|
};
|
|
|
|
case WithdrawalGroupStatus.SuspendedAbortingBank:
|
|
|
|
return {
|
|
|
|
major: TransactionMajorState.SuspendedAborting,
|
|
|
|
minor: TransactionMinorState.Bank,
|
|
|
|
};
|
|
|
|
case WithdrawalGroupStatus.SuspendedQueryingStatus:
|
|
|
|
return {
|
|
|
|
major: TransactionMajorState.Suspended,
|
|
|
|
minor: TransactionMinorState.ExchangeWaitReserve,
|
|
|
|
};
|
|
|
|
case WithdrawalGroupStatus.SuspendedRegisteringBank:
|
|
|
|
return {
|
|
|
|
major: TransactionMajorState.Suspended,
|
|
|
|
minor: TransactionMinorState.BankRegisterReserve,
|
|
|
|
};
|
|
|
|
case WithdrawalGroupStatus.SuspendedWaitConfirmBank:
|
|
|
|
return {
|
|
|
|
major: TransactionMajorState.Suspended,
|
|
|
|
minor: TransactionMinorState.BankConfirmTransfer,
|
|
|
|
};
|
2023-05-02 10:04:58 +02:00
|
|
|
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,
|
|
|
|
};
|
2023-04-25 23:56:57 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-12-02 00:42:40 +01:00
|
|
|
/**
|
|
|
|
* Get information about a withdrawal from
|
2019-12-09 19:59:08 +01:00
|
|
|
* a taler://withdraw URI by asking the bank.
|
2022-03-18 15:32:41 +01:00
|
|
|
*
|
2022-03-14 18:31:30 +01:00
|
|
|
* FIXME: Move into bank client.
|
2019-12-02 00:42:40 +01:00
|
|
|
*/
|
2019-12-16 16:59:09 +01:00
|
|
|
export async function getBankWithdrawalInfo(
|
2022-03-14 18:31:30 +01:00
|
|
|
http: HttpRequestLibrary,
|
2019-12-02 00:42:40 +01:00
|
|
|
talerWithdrawUri: string,
|
2019-12-09 19:59:08 +01:00
|
|
|
): Promise<BankWithdrawDetails> {
|
2019-12-02 00:42:40 +01:00
|
|
|
const uriResult = parseWithdrawUri(talerWithdrawUri);
|
|
|
|
if (!uriResult) {
|
2019-12-20 11:35:51 +01:00
|
|
|
throw Error(`can't parse URL ${talerWithdrawUri}`);
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
2020-09-09 17:46:20 +02:00
|
|
|
|
2020-12-14 16:45:15 +01:00
|
|
|
const configReqUrl = new URL("config", uriResult.bankIntegrationApiBaseUrl);
|
2020-09-09 17:46:20 +02:00
|
|
|
|
2022-03-14 18:31:30 +01:00
|
|
|
const configResp = await http.get(configReqUrl.href);
|
2020-09-09 17:46:20 +02:00
|
|
|
const config = await readSuccessResponseJsonOrThrow(
|
|
|
|
configResp,
|
|
|
|
codecForTalerConfigResponse(),
|
|
|
|
);
|
|
|
|
|
2021-11-23 23:51:12 +01:00
|
|
|
const versionRes = LibtoolVersion.compare(
|
2020-12-14 16:45:15 +01:00
|
|
|
WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
|
|
|
|
config.version,
|
|
|
|
);
|
2020-09-09 17:46:20 +02:00
|
|
|
if (versionRes?.compatible != true) {
|
2022-03-22 21:16:38 +01:00
|
|
|
throw TalerError.fromDetail(
|
2020-09-09 17:46:20 +02:00
|
|
|
TalerErrorCode.WALLET_BANK_INTEGRATION_PROTOCOL_VERSION_INCOMPATIBLE,
|
|
|
|
{
|
|
|
|
exchangeProtocolVersion: config.version,
|
|
|
|
walletProtocolVersion: WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
|
|
|
|
},
|
2022-03-22 21:16:38 +01:00
|
|
|
"bank integration protocol version not compatible with wallet",
|
2020-09-09 17:46:20 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-07-27 13:39:52 +02:00
|
|
|
const reqUrl = new URL(
|
2020-09-09 17:46:20 +02:00
|
|
|
`withdrawal-operation/${uriResult.withdrawalOperationId}`,
|
2020-07-27 13:39:52 +02:00
|
|
|
uriResult.bankIntegrationApiBaseUrl,
|
|
|
|
);
|
2022-08-25 23:35:29 +02:00
|
|
|
|
|
|
|
logger.info(`bank withdrawal status URL: ${reqUrl.href}}`);
|
|
|
|
|
2022-03-14 18:31:30 +01:00
|
|
|
const resp = await http.get(reqUrl.href);
|
2020-07-22 10:52:03 +02:00
|
|
|
const status = await readSuccessResponseJsonOrThrow(
|
|
|
|
resp,
|
|
|
|
codecForWithdrawOperationStatusResponse(),
|
|
|
|
);
|
2019-12-19 20:42:49 +01:00
|
|
|
|
2022-08-25 18:34:25 +02:00
|
|
|
logger.info(`bank withdrawal operation status: ${j2s(status)}`);
|
|
|
|
|
2019-12-02 00:42:40 +01:00
|
|
|
return {
|
|
|
|
amount: Amounts.parseOrThrow(status.amount),
|
|
|
|
confirmTransferUrl: status.confirm_transfer_url,
|
|
|
|
selectionDone: status.selection_done,
|
|
|
|
senderWire: status.sender_wire,
|
|
|
|
suggestedExchange: status.suggested_exchange,
|
|
|
|
transferDone: status.transfer_done,
|
|
|
|
wireTypes: status.wire_types,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2020-07-16 11:43:52 +02:00
|
|
|
/**
|
|
|
|
* Return denominations that can potentially used for a withdrawal.
|
|
|
|
*/
|
2021-01-14 18:00:00 +01:00
|
|
|
export async function getCandidateWithdrawalDenoms(
|
2019-12-02 00:42:40 +01:00
|
|
|
ws: InternalWalletState,
|
|
|
|
exchangeBaseUrl: string,
|
|
|
|
): Promise<DenominationRecord[]> {
|
2019-12-19 20:42:49 +01:00
|
|
|
return await ws.db
|
2022-09-13 13:25:41 +02:00
|
|
|
.mktx((x) => [x.denominations])
|
2021-06-09 15:14:17 +02:00
|
|
|
.runReadOnly(async (tx) => {
|
2021-08-24 15:43:06 +02:00
|
|
|
const allDenoms = await tx.denominations.indexes.byExchangeBaseUrl.getAll(
|
|
|
|
exchangeBaseUrl,
|
|
|
|
);
|
2023-04-19 17:42:47 +02:00
|
|
|
return allDenoms.filter((d) =>
|
|
|
|
isWithdrawableDenom(d, ws.config.testing.denomselAllowLate),
|
|
|
|
);
|
2019-12-19 20:42:49 +01:00
|
|
|
});
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2020-08-13 11:45:01 +02:00
|
|
|
* Generate a planchet for a coin index in a withdrawal group.
|
|
|
|
* Does not actually withdraw the coin yet.
|
|
|
|
*
|
|
|
|
* Split up so that we can parallelize the crypto, but serialize
|
|
|
|
* the exchange requests per reserve.
|
2019-12-02 00:42:40 +01:00
|
|
|
*/
|
2020-08-13 11:45:01 +02:00
|
|
|
async function processPlanchetGenerate(
|
2019-12-02 00:42:40 +01:00
|
|
|
ws: InternalWalletState,
|
2022-01-12 16:54:38 +01:00
|
|
|
withdrawalGroup: WithdrawalGroupRecord,
|
2019-12-02 00:42:40 +01:00
|
|
|
coinIdx: number,
|
|
|
|
): Promise<void> {
|
2021-06-09 15:14:17 +02:00
|
|
|
let planchet = await ws.db
|
2022-09-13 13:25:41 +02:00
|
|
|
.mktx((x) => [x.planchets])
|
2021-06-09 15:14:17 +02:00
|
|
|
.runReadOnly(async (tx) => {
|
|
|
|
return tx.planchets.indexes.byGroupAndIndex.get([
|
2022-01-12 16:54:38 +01:00
|
|
|
withdrawalGroup.withdrawalGroupId,
|
2021-06-09 15:14:17 +02:00
|
|
|
coinIdx,
|
|
|
|
]);
|
|
|
|
});
|
2022-01-12 16:54:38 +01:00
|
|
|
if (planchet) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
let ci = 0;
|
2022-08-09 15:00:45 +02:00
|
|
|
let maybeDenomPubHash: string | undefined;
|
2022-01-12 16:54:38 +01:00
|
|
|
for (let di = 0; di < withdrawalGroup.denomsSel.selectedDenoms.length; di++) {
|
|
|
|
const d = withdrawalGroup.denomsSel.selectedDenoms[di];
|
|
|
|
if (coinIdx >= ci && coinIdx < ci + d.count) {
|
2022-08-09 15:00:45 +02:00
|
|
|
maybeDenomPubHash = d.denomPubHash;
|
2022-01-12 16:54:38 +01:00
|
|
|
break;
|
2020-05-11 17:13:19 +02:00
|
|
|
}
|
2022-01-12 16:54:38 +01:00
|
|
|
ci += d.count;
|
|
|
|
}
|
2022-08-09 15:00:45 +02:00
|
|
|
if (!maybeDenomPubHash) {
|
2022-01-12 16:54:38 +01:00
|
|
|
throw Error("invariant violated");
|
|
|
|
}
|
2022-08-09 15:00:45 +02:00
|
|
|
const denomPubHash = maybeDenomPubHash;
|
2021-06-09 15:14:17 +02:00
|
|
|
|
2022-08-09 15:00:45 +02:00
|
|
|
const denom = await ws.db
|
2022-09-13 13:25:41 +02:00
|
|
|
.mktx((x) => [x.denominations])
|
2022-01-12 16:54:38 +01:00
|
|
|
.runReadOnly(async (tx) => {
|
2022-08-09 15:00:45 +02:00
|
|
|
return ws.getDenomInfo(
|
|
|
|
ws,
|
|
|
|
tx,
|
2022-01-12 16:54:38 +01:00
|
|
|
withdrawalGroup.exchangeBaseUrl,
|
2022-08-09 15:00:45 +02:00
|
|
|
denomPubHash,
|
|
|
|
);
|
2022-01-12 16:54:38 +01:00
|
|
|
});
|
2022-08-09 15:00:45 +02:00
|
|
|
checkDbInvariant(!!denom);
|
2022-01-12 16:54:38 +01:00
|
|
|
const r = await ws.cryptoApi.createPlanchet({
|
|
|
|
denomPub: denom.denomPub,
|
2022-11-02 17:42:14 +01:00
|
|
|
feeWithdraw: Amounts.parseOrThrow(denom.feeWithdraw),
|
2022-08-09 15:00:45 +02:00
|
|
|
reservePriv: withdrawalGroup.reservePriv,
|
|
|
|
reservePub: withdrawalGroup.reservePub,
|
2022-11-02 17:42:14 +01:00
|
|
|
value: Amounts.parseOrThrow(denom.value),
|
2022-01-12 16:54:38 +01:00
|
|
|
coinIndex: coinIdx,
|
|
|
|
secretSeed: withdrawalGroup.secretSeed,
|
2022-08-09 15:00:45 +02:00
|
|
|
restrictAge: withdrawalGroup.restrictAge,
|
2022-01-12 16:54:38 +01:00
|
|
|
});
|
|
|
|
const newPlanchet: PlanchetRecord = {
|
|
|
|
blindingKey: r.blindingKey,
|
|
|
|
coinEv: r.coinEv,
|
|
|
|
coinEvHash: r.coinEvHash,
|
|
|
|
coinIdx,
|
|
|
|
coinPriv: r.coinPriv,
|
|
|
|
coinPub: r.coinPub,
|
|
|
|
denomPubHash: r.denomPubHash,
|
2022-10-14 21:00:13 +02:00
|
|
|
planchetStatus: PlanchetStatus.Pending,
|
2022-01-12 16:54:38 +01:00
|
|
|
withdrawSig: r.withdrawSig,
|
|
|
|
withdrawalGroupId: withdrawalGroup.withdrawalGroupId,
|
2022-04-19 17:12:43 +02:00
|
|
|
ageCommitmentProof: r.ageCommitmentProof,
|
2022-01-12 16:54:38 +01:00
|
|
|
lastError: undefined,
|
|
|
|
};
|
|
|
|
await ws.db
|
2022-09-13 13:25:41 +02:00
|
|
|
.mktx((x) => [x.planchets])
|
2022-01-12 16:54:38 +01:00
|
|
|
.runReadWrite(async (tx) => {
|
|
|
|
const p = await tx.planchets.indexes.byGroupAndIndex.get([
|
|
|
|
withdrawalGroup.withdrawalGroupId,
|
|
|
|
coinIdx,
|
|
|
|
]);
|
|
|
|
if (p) {
|
|
|
|
planchet = p;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
await tx.planchets.put(newPlanchet);
|
|
|
|
planchet = newPlanchet;
|
2020-05-11 17:13:19 +02:00
|
|
|
});
|
2020-08-13 11:45:01 +02:00
|
|
|
}
|
|
|
|
|
2023-02-10 13:21:37 +01:00
|
|
|
interface WithdrawalRequestBatchArgs {
|
|
|
|
/**
|
|
|
|
* Use the batched request on the network level.
|
|
|
|
* Not supported by older exchanges.
|
|
|
|
*/
|
|
|
|
useBatchRequest: boolean;
|
2020-05-11 17:13:19 +02:00
|
|
|
|
2023-02-10 13:21:37 +01:00
|
|
|
coinStartIndex: number;
|
2021-06-09 15:14:17 +02:00
|
|
|
|
2023-02-10 13:21:37 +01:00
|
|
|
batchSize: number;
|
|
|
|
}
|
2021-06-09 15:14:17 +02:00
|
|
|
|
2023-02-10 13:21:37 +01:00
|
|
|
interface WithdrawalBatchResult {
|
|
|
|
coinIdxs: number[];
|
|
|
|
batchResp: ExchangeWithdrawBatchResponse;
|
2020-08-13 11:45:01 +02:00
|
|
|
}
|
2019-12-02 00:42:40 +01:00
|
|
|
|
2022-05-03 17:53:32 +02:00
|
|
|
/**
|
|
|
|
* Send the withdrawal request for a generated planchet to the exchange.
|
|
|
|
*
|
|
|
|
* The verification of the response is done asynchronously to enable parallelism.
|
|
|
|
*/
|
|
|
|
async function processPlanchetExchangeBatchRequest(
|
|
|
|
ws: InternalWalletState,
|
2023-02-09 22:44:36 +01:00
|
|
|
wgContext: WithdrawalGroupContext,
|
2023-02-10 13:21:37 +01:00
|
|
|
args: WithdrawalRequestBatchArgs,
|
|
|
|
): Promise<WithdrawalBatchResult> {
|
2023-02-09 22:44:36 +01:00
|
|
|
const withdrawalGroup: WithdrawalGroupRecord = wgContext.wgRecord;
|
2022-05-03 17:53:32 +02:00
|
|
|
logger.info(
|
2023-02-10 13:21:37 +01:00
|
|
|
`processing planchet exchange batch request ${withdrawalGroup.withdrawalGroupId}, start=${args.coinStartIndex}, len=${args.batchSize}`,
|
2022-05-03 17:53:32 +02:00
|
|
|
);
|
2023-02-10 13:21:37 +01:00
|
|
|
|
|
|
|
const batchReq: ExchangeBatchWithdrawRequest = { planchets: [] };
|
|
|
|
// Indices of coins that are included in the batch request
|
2023-02-10 13:40:57 +01:00
|
|
|
const requestCoinIdxs: number[] = [];
|
2023-02-10 13:21:37 +01:00
|
|
|
|
|
|
|
await ws.db
|
2022-09-13 13:25:41 +02:00
|
|
|
.mktx((x) => [
|
|
|
|
x.withdrawalGroups,
|
|
|
|
x.planchets,
|
|
|
|
x.exchanges,
|
|
|
|
x.denominations,
|
|
|
|
])
|
2022-05-03 17:53:32 +02:00
|
|
|
.runReadOnly(async (tx) => {
|
2023-02-10 13:21:37 +01:00
|
|
|
for (
|
|
|
|
let coinIdx = args.coinStartIndex;
|
|
|
|
coinIdx < args.coinStartIndex + args.batchSize &&
|
|
|
|
coinIdx < wgContext.numPlanchets;
|
|
|
|
coinIdx++
|
|
|
|
) {
|
2022-05-03 17:53:32 +02:00
|
|
|
let planchet = await tx.planchets.indexes.byGroupAndIndex.get([
|
|
|
|
withdrawalGroup.withdrawalGroupId,
|
|
|
|
coinIdx,
|
|
|
|
]);
|
|
|
|
if (!planchet) {
|
2023-02-10 13:21:37 +01:00
|
|
|
continue;
|
2022-05-03 17:53:32 +02:00
|
|
|
}
|
2022-10-14 21:00:13 +02:00
|
|
|
if (planchet.planchetStatus === PlanchetStatus.WithdrawalDone) {
|
2022-05-03 17:53:32 +02:00
|
|
|
logger.warn("processPlanchet: planchet already withdrawn");
|
2023-02-10 13:21:37 +01:00
|
|
|
continue;
|
2022-05-03 17:53:32 +02:00
|
|
|
}
|
|
|
|
const denom = await ws.getDenomInfo(
|
|
|
|
ws,
|
|
|
|
tx,
|
|
|
|
withdrawalGroup.exchangeBaseUrl,
|
|
|
|
planchet.denomPubHash,
|
|
|
|
);
|
|
|
|
|
|
|
|
if (!denom) {
|
|
|
|
logger.error("db inconsistent: denom for planchet not found");
|
2023-02-10 13:21:37 +01:00
|
|
|
continue;
|
2022-05-03 17:53:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
const planchetReq: ExchangeWithdrawRequest = {
|
|
|
|
denom_pub_hash: planchet.denomPubHash,
|
|
|
|
reserve_sig: planchet.withdrawSig,
|
|
|
|
coin_ev: planchet.coinEv,
|
|
|
|
};
|
2023-02-10 13:21:37 +01:00
|
|
|
batchReq.planchets.push(planchetReq);
|
2023-02-10 13:40:57 +01:00
|
|
|
requestCoinIdxs.push(coinIdx);
|
2022-05-03 17:53:32 +02:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2023-02-10 13:21:37 +01:00
|
|
|
if (batchReq.planchets.length == 0) {
|
|
|
|
logger.warn("empty withdrawal batch");
|
|
|
|
return {
|
|
|
|
batchResp: { ev_sigs: [] },
|
|
|
|
coinIdxs: [],
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2023-02-10 16:53:27 +01:00
|
|
|
async function handleKycRequired(
|
|
|
|
resp: HttpResponse,
|
|
|
|
startIdx: number,
|
|
|
|
): Promise<void> {
|
2023-02-10 13:21:37 +01:00
|
|
|
logger.info("withdrawal requires KYC");
|
|
|
|
const respJson = await resp.json();
|
|
|
|
const uuidResp = codecForWalletKycUuid().decode(respJson);
|
|
|
|
logger.info(`kyc uuid response: ${j2s(uuidResp)}`);
|
|
|
|
await ws.db
|
|
|
|
.mktx((x) => [x.planchets, x.withdrawalGroups])
|
|
|
|
.runReadWrite(async (tx) => {
|
2023-02-10 19:25:04 +01:00
|
|
|
for (let i = startIdx; i < requestCoinIdxs.length; i++) {
|
2023-02-10 13:21:37 +01:00
|
|
|
let planchet = await tx.planchets.indexes.byGroupAndIndex.get([
|
|
|
|
withdrawalGroup.withdrawalGroupId,
|
2023-02-10 13:40:57 +01:00
|
|
|
requestCoinIdxs[i],
|
2023-02-10 13:21:37 +01:00
|
|
|
]);
|
|
|
|
if (!planchet) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
planchet.planchetStatus = PlanchetStatus.KycRequired;
|
|
|
|
await tx.planchets.put(planchet);
|
|
|
|
}
|
|
|
|
const wg2 = await tx.withdrawalGroups.get(
|
|
|
|
withdrawalGroup.withdrawalGroupId,
|
|
|
|
);
|
|
|
|
if (!wg2) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
wg2.kycPending = {
|
|
|
|
paytoHash: uuidResp.h_payto,
|
|
|
|
requirementRow: uuidResp.requirement_row,
|
|
|
|
};
|
|
|
|
await tx.withdrawalGroups.put(wg2);
|
|
|
|
});
|
2022-05-03 17:53:32 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-02-10 16:53:02 +01:00
|
|
|
async function storeCoinError(e: any, coinIdx: number): Promise<void> {
|
2023-02-10 13:21:37 +01:00
|
|
|
const errDetail = getErrorDetailFromException(e);
|
|
|
|
logger.trace("withdrawal request failed", e);
|
|
|
|
logger.trace(String(e));
|
|
|
|
await ws.db
|
|
|
|
.mktx((x) => [x.planchets])
|
|
|
|
.runReadWrite(async (tx) => {
|
|
|
|
let planchet = await tx.planchets.indexes.byGroupAndIndex.get([
|
|
|
|
withdrawalGroup.withdrawalGroupId,
|
|
|
|
coinIdx,
|
|
|
|
]);
|
|
|
|
if (!planchet) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
planchet.lastError = errDetail;
|
|
|
|
await tx.planchets.put(planchet);
|
|
|
|
});
|
|
|
|
}
|
2022-05-03 17:53:32 +02:00
|
|
|
|
2023-02-10 13:21:37 +01:00
|
|
|
// FIXME: handle individual error codes better!
|
|
|
|
|
|
|
|
if (args.useBatchRequest) {
|
|
|
|
const reqUrl = new URL(
|
|
|
|
`reserves/${withdrawalGroup.reservePub}/batch-withdraw`,
|
|
|
|
withdrawalGroup.exchangeBaseUrl,
|
|
|
|
).href;
|
|
|
|
|
|
|
|
try {
|
|
|
|
const resp = await ws.http.postJson(reqUrl, batchReq);
|
|
|
|
if (resp.status === HttpStatusCode.UnavailableForLegalReasons) {
|
|
|
|
await handleKycRequired(resp, 0);
|
|
|
|
}
|
|
|
|
const r = await readSuccessResponseJsonOrThrow(
|
|
|
|
resp,
|
|
|
|
codecForWithdrawBatchResponse(),
|
|
|
|
);
|
|
|
|
return {
|
2023-02-10 13:40:57 +01:00
|
|
|
coinIdxs: requestCoinIdxs,
|
2023-02-10 13:21:37 +01:00
|
|
|
batchResp: r,
|
|
|
|
};
|
|
|
|
} catch (e) {
|
2023-02-10 13:40:57 +01:00
|
|
|
await storeCoinError(e, requestCoinIdxs[0]);
|
2023-02-10 13:21:37 +01:00
|
|
|
return {
|
|
|
|
batchResp: { ev_sigs: [] },
|
|
|
|
coinIdxs: [],
|
|
|
|
};
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// We emulate the batch response here by making multiple individual requests
|
|
|
|
const responses: ExchangeWithdrawBatchResponse = {
|
|
|
|
ev_sigs: [],
|
|
|
|
};
|
2023-02-10 13:40:57 +01:00
|
|
|
const responseCoinIdxs: number[] = [];
|
2023-02-10 13:21:37 +01:00
|
|
|
for (let i = 0; i < batchReq.planchets.length; i++) {
|
|
|
|
try {
|
|
|
|
const p = batchReq.planchets[i];
|
|
|
|
const reqUrl = new URL(
|
|
|
|
`reserves/${withdrawalGroup.reservePub}/withdraw`,
|
|
|
|
withdrawalGroup.exchangeBaseUrl,
|
|
|
|
).href;
|
|
|
|
const resp = await ws.http.postJson(reqUrl, p);
|
|
|
|
if (resp.status === HttpStatusCode.UnavailableForLegalReasons) {
|
|
|
|
await handleKycRequired(resp, i);
|
|
|
|
// We still return blinded coins that we could actually withdraw.
|
|
|
|
return {
|
2023-02-10 13:40:57 +01:00
|
|
|
coinIdxs: responseCoinIdxs,
|
2023-02-10 13:21:37 +01:00
|
|
|
batchResp: responses,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
const r = await readSuccessResponseJsonOrThrow(
|
|
|
|
resp,
|
|
|
|
codecForWithdrawResponse(),
|
|
|
|
);
|
|
|
|
responses.ev_sigs.push(r);
|
2023-02-10 13:40:57 +01:00
|
|
|
responseCoinIdxs.push(requestCoinIdxs[i]);
|
2023-02-10 13:21:37 +01:00
|
|
|
} catch (e) {
|
2023-02-10 13:40:57 +01:00
|
|
|
await storeCoinError(e, requestCoinIdxs[i]);
|
2023-02-10 13:21:37 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return {
|
2023-02-10 13:40:57 +01:00
|
|
|
coinIdxs: responseCoinIdxs,
|
2023-02-10 13:21:37 +01:00
|
|
|
batchResp: responses,
|
|
|
|
};
|
|
|
|
}
|
2022-05-03 17:53:32 +02:00
|
|
|
}
|
|
|
|
|
2020-08-13 11:45:01 +02:00
|
|
|
async function processPlanchetVerifyAndStoreCoin(
|
|
|
|
ws: InternalWalletState,
|
2023-02-09 22:44:36 +01:00
|
|
|
wgContext: WithdrawalGroupContext,
|
2020-08-13 11:45:01 +02:00
|
|
|
coinIdx: number,
|
2023-02-10 13:21:37 +01:00
|
|
|
resp: ExchangeWithdrawResponse,
|
2020-08-13 11:45:01 +02:00
|
|
|
): Promise<void> {
|
2023-02-09 22:44:36 +01:00
|
|
|
const withdrawalGroup = wgContext.wgRecord;
|
2023-02-10 13:21:37 +01:00
|
|
|
logger.info(`checking and storing planchet idx=${coinIdx}`);
|
2021-06-09 15:14:17 +02:00
|
|
|
const d = await ws.db
|
2022-09-13 13:25:41 +02:00
|
|
|
.mktx((x) => [x.withdrawalGroups, x.planchets, x.denominations])
|
2021-06-09 15:14:17 +02:00
|
|
|
.runReadOnly(async (tx) => {
|
|
|
|
let planchet = await tx.planchets.indexes.byGroupAndIndex.get([
|
2022-01-12 16:54:38 +01:00
|
|
|
withdrawalGroup.withdrawalGroupId,
|
2021-06-09 15:14:17 +02:00
|
|
|
coinIdx,
|
|
|
|
]);
|
|
|
|
if (!planchet) {
|
|
|
|
return;
|
|
|
|
}
|
2022-10-14 21:00:13 +02:00
|
|
|
if (planchet.planchetStatus === PlanchetStatus.WithdrawalDone) {
|
2021-06-09 15:14:17 +02:00
|
|
|
logger.warn("processPlanchet: planchet already withdrawn");
|
|
|
|
return;
|
|
|
|
}
|
2022-03-10 16:30:24 +01:00
|
|
|
const denomInfo = await ws.getDenomInfo(
|
|
|
|
ws,
|
|
|
|
tx,
|
|
|
|
withdrawalGroup.exchangeBaseUrl,
|
|
|
|
planchet.denomPubHash,
|
|
|
|
);
|
|
|
|
if (!denomInfo) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
return {
|
|
|
|
planchet,
|
|
|
|
denomInfo,
|
|
|
|
exchangeBaseUrl: withdrawalGroup.exchangeBaseUrl,
|
|
|
|
};
|
2021-06-09 15:14:17 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
if (!d) {
|
2020-08-13 11:45:01 +02:00
|
|
|
return;
|
|
|
|
}
|
2020-05-11 17:13:19 +02:00
|
|
|
|
2022-03-10 16:30:24 +01:00
|
|
|
const { planchet, denomInfo } = d;
|
2021-06-09 15:14:17 +02:00
|
|
|
|
2022-03-10 16:30:24 +01:00
|
|
|
const planchetDenomPub = denomInfo.denomPub;
|
2022-02-21 12:40:51 +01:00
|
|
|
if (planchetDenomPub.cipher !== DenomKeyType.Rsa) {
|
2021-11-27 20:56:58 +01:00
|
|
|
throw Error(`cipher (${planchetDenomPub.cipher}) not supported`);
|
2021-11-17 10:23:22 +01:00
|
|
|
}
|
|
|
|
|
2021-11-27 20:56:58 +01:00
|
|
|
let evSig = resp.ev_sig;
|
2022-02-21 12:40:51 +01:00
|
|
|
if (!(evSig.cipher === DenomKeyType.Rsa)) {
|
2021-11-17 10:23:22 +01:00
|
|
|
throw Error("unsupported cipher");
|
|
|
|
}
|
|
|
|
|
2022-03-23 21:24:23 +01:00
|
|
|
const denomSigRsa = await ws.cryptoApi.rsaUnblind({
|
|
|
|
bk: planchet.blindingKey,
|
|
|
|
blindedSig: evSig.blinded_rsa_signature,
|
|
|
|
pk: planchetDenomPub.rsa_public_key,
|
|
|
|
});
|
2019-12-02 00:42:40 +01:00
|
|
|
|
2022-03-23 21:24:23 +01:00
|
|
|
const isValid = await ws.cryptoApi.rsaVerify({
|
|
|
|
hm: planchet.coinPub,
|
|
|
|
pk: planchetDenomPub.rsa_public_key,
|
|
|
|
sig: denomSigRsa.sig,
|
|
|
|
});
|
2020-05-11 17:13:19 +02:00
|
|
|
|
2019-12-05 19:38:19 +01:00
|
|
|
if (!isValid) {
|
2021-06-09 15:14:17 +02:00
|
|
|
await ws.db
|
2022-09-13 13:25:41 +02:00
|
|
|
.mktx((x) => [x.planchets])
|
2021-06-09 15:14:17 +02:00
|
|
|
.runReadWrite(async (tx) => {
|
|
|
|
let planchet = await tx.planchets.indexes.byGroupAndIndex.get([
|
2022-01-12 16:54:38 +01:00
|
|
|
withdrawalGroup.withdrawalGroupId,
|
2021-06-09 15:14:17 +02:00
|
|
|
coinIdx,
|
|
|
|
]);
|
|
|
|
if (!planchet) {
|
|
|
|
return;
|
|
|
|
}
|
2022-03-22 21:16:38 +01:00
|
|
|
planchet.lastError = makeErrorDetail(
|
2021-06-09 15:14:17 +02:00
|
|
|
TalerErrorCode.WALLET_EXCHANGE_COIN_SIGNATURE_INVALID,
|
|
|
|
{},
|
2022-03-22 21:16:38 +01:00
|
|
|
"invalid signature from the exchange after unblinding",
|
2021-06-09 15:14:17 +02:00
|
|
|
);
|
|
|
|
await tx.planchets.put(planchet);
|
|
|
|
});
|
2020-08-13 11:45:01 +02:00
|
|
|
return;
|
2019-12-05 19:38:19 +01:00
|
|
|
}
|
|
|
|
|
2021-11-27 20:56:58 +01:00
|
|
|
let denomSig: UnblindedSignature;
|
2022-03-10 16:30:24 +01:00
|
|
|
if (planchetDenomPub.cipher === DenomKeyType.Rsa) {
|
2021-11-27 20:56:58 +01:00
|
|
|
denomSig = {
|
2022-03-10 16:30:24 +01:00
|
|
|
cipher: planchetDenomPub.cipher,
|
2022-03-23 21:24:23 +01:00
|
|
|
rsa_signature: denomSigRsa.sig,
|
2021-11-27 20:56:58 +01:00
|
|
|
};
|
|
|
|
} else {
|
|
|
|
throw Error("unsupported cipher");
|
|
|
|
}
|
|
|
|
|
2019-12-02 00:42:40 +01:00
|
|
|
const coin: CoinRecord = {
|
|
|
|
blindingKey: planchet.blindingKey,
|
|
|
|
coinPriv: planchet.coinPriv,
|
|
|
|
coinPub: planchet.coinPub,
|
|
|
|
denomPubHash: planchet.denomPubHash,
|
2021-11-27 20:56:58 +01:00
|
|
|
denomSig,
|
2020-12-16 17:59:04 +01:00
|
|
|
coinEvHash: planchet.coinEvHash,
|
2022-03-10 16:30:24 +01:00
|
|
|
exchangeBaseUrl: d.exchangeBaseUrl,
|
2019-12-02 00:42:40 +01:00
|
|
|
status: CoinStatus.Fresh,
|
2020-03-11 20:14:28 +01:00
|
|
|
coinSource: {
|
|
|
|
type: CoinSourceType.Withdraw,
|
|
|
|
coinIndex: coinIdx,
|
2022-10-14 23:01:41 +02:00
|
|
|
reservePub: withdrawalGroup.reservePub,
|
2022-01-12 16:54:38 +01:00
|
|
|
withdrawalGroupId: withdrawalGroup.withdrawalGroupId,
|
2020-03-12 14:55:38 +01:00
|
|
|
},
|
2022-10-15 16:25:44 +02:00
|
|
|
maxAge: withdrawalGroup.restrictAge ?? AgeRestriction.AGE_UNRESTRICTED,
|
2022-04-19 17:12:43 +02:00
|
|
|
ageCommitmentProof: planchet.ageCommitmentProof,
|
2022-10-15 11:52:07 +02:00
|
|
|
spendAllocation: undefined,
|
2019-12-02 00:42:40 +01:00
|
|
|
};
|
|
|
|
|
2020-05-11 17:35:00 +02:00
|
|
|
const planchetCoinPub = planchet.coinPub;
|
|
|
|
|
2023-02-09 22:44:36 +01:00
|
|
|
wgContext.planchetsFinished.add(planchet.coinPub);
|
|
|
|
|
2023-02-10 13:21:37 +01:00
|
|
|
// We create the notification here, as the async transaction below
|
|
|
|
// allows other planchet withdrawals to change wgContext.planchetsFinished
|
|
|
|
const notification: WalletNotification = {
|
|
|
|
type: NotificationType.CoinWithdrawn,
|
|
|
|
numTotal: wgContext.numPlanchets,
|
|
|
|
numWithdrawn: wgContext.planchetsFinished.size,
|
2023-02-10 16:53:27 +01:00
|
|
|
};
|
2023-02-10 13:21:37 +01:00
|
|
|
|
2022-08-09 15:00:45 +02:00
|
|
|
// Check if this is the first time that the whole
|
|
|
|
// withdrawal succeeded. If so, mark the withdrawal
|
|
|
|
// group as finished.
|
2021-06-09 15:14:17 +02:00
|
|
|
const firstSuccess = await ws.db
|
2022-09-16 16:20:47 +02:00
|
|
|
.mktx((x) => [
|
|
|
|
x.coins,
|
|
|
|
x.denominations,
|
|
|
|
x.coinAvailability,
|
|
|
|
x.withdrawalGroups,
|
|
|
|
x.planchets,
|
|
|
|
])
|
2021-06-09 15:14:17 +02:00
|
|
|
.runReadWrite(async (tx) => {
|
|
|
|
const p = await tx.planchets.get(planchetCoinPub);
|
2022-10-14 21:00:13 +02:00
|
|
|
if (!p || p.planchetStatus === PlanchetStatus.WithdrawalDone) {
|
2019-12-06 02:52:16 +01:00
|
|
|
return false;
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
2022-10-14 21:00:13 +02:00
|
|
|
p.planchetStatus = PlanchetStatus.WithdrawalDone;
|
2021-06-09 15:14:17 +02:00
|
|
|
await tx.planchets.put(p);
|
2022-09-14 20:34:37 +02:00
|
|
|
await makeCoinAvailable(ws, tx, coin);
|
2019-12-06 02:52:16 +01:00
|
|
|
return true;
|
2021-06-09 15:14:17 +02:00
|
|
|
});
|
2019-12-05 19:38:19 +01:00
|
|
|
|
2020-08-13 11:45:01 +02:00
|
|
|
if (firstSuccess) {
|
2023-02-10 13:21:37 +01:00
|
|
|
ws.notify(notification);
|
2019-12-06 02:52:16 +01:00
|
|
|
}
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2020-12-16 17:59:04 +01:00
|
|
|
* Make sure that denominations that currently can be used for withdrawal
|
|
|
|
* are validated, and the result of validation is stored in the database.
|
2019-12-02 00:42:40 +01:00
|
|
|
*/
|
2020-12-16 17:59:04 +01:00
|
|
|
export async function updateWithdrawalDenoms(
|
2019-12-02 00:42:40 +01:00
|
|
|
ws: InternalWalletState,
|
|
|
|
exchangeBaseUrl: string,
|
2020-12-16 17:59:04 +01:00
|
|
|
): Promise<void> {
|
2021-08-06 16:27:18 +02:00
|
|
|
logger.trace(
|
|
|
|
`updating denominations used for withdrawal for ${exchangeBaseUrl}`,
|
|
|
|
);
|
2021-06-09 15:14:17 +02:00
|
|
|
const exchangeDetails = await ws.db
|
2022-09-13 13:25:41 +02:00
|
|
|
.mktx((x) => [x.exchanges, x.exchangeDetails])
|
2021-06-09 15:14:17 +02:00
|
|
|
.runReadOnly(async (tx) => {
|
2021-06-17 15:49:05 +02:00
|
|
|
return ws.exchangeOps.getExchangeDetails(tx, exchangeBaseUrl);
|
2021-06-09 15:14:17 +02:00
|
|
|
});
|
2019-12-02 00:42:40 +01:00
|
|
|
if (!exchangeDetails) {
|
2020-07-20 09:16:32 +02:00
|
|
|
logger.error("exchange details not available");
|
2019-12-02 00:42:40 +01:00
|
|
|
throw Error(`exchange ${exchangeBaseUrl} details not available`);
|
|
|
|
}
|
2021-04-07 19:29:51 +02:00
|
|
|
// First do a pass where the validity of candidate denominations
|
|
|
|
// is checked and the result is stored in the database.
|
2021-08-06 16:27:18 +02:00
|
|
|
logger.trace("getting candidate denominations");
|
2021-01-14 18:00:00 +01:00
|
|
|
const denominations = await getCandidateWithdrawalDenoms(ws, exchangeBaseUrl);
|
2021-08-06 16:27:18 +02:00
|
|
|
logger.trace(`got ${denominations.length} candidate denominations`);
|
|
|
|
const batchSize = 500;
|
|
|
|
let current = 0;
|
|
|
|
|
|
|
|
while (current < denominations.length) {
|
|
|
|
const updatedDenominations: DenominationRecord[] = [];
|
|
|
|
// Do a batch of batchSize
|
2021-08-19 15:12:33 +02:00
|
|
|
for (
|
|
|
|
let batchIdx = 0;
|
|
|
|
batchIdx < batchSize && current < denominations.length;
|
|
|
|
batchIdx++, current++
|
|
|
|
) {
|
2021-08-06 16:27:18 +02:00
|
|
|
const denom = denominations[current];
|
2021-11-17 10:23:22 +01:00
|
|
|
if (
|
|
|
|
denom.verificationStatus === DenominationVerificationStatus.Unverified
|
|
|
|
) {
|
2021-08-06 16:27:18 +02:00
|
|
|
logger.trace(
|
2022-09-21 01:14:57 +02:00
|
|
|
`Validating denomination (${current + 1}/${
|
|
|
|
denominations.length
|
2021-08-06 16:27:18 +02:00
|
|
|
}) signature of ${denom.denomPubHash}`,
|
|
|
|
);
|
2022-05-18 19:41:51 +02:00
|
|
|
let valid = false;
|
2023-04-19 17:42:47 +02:00
|
|
|
if (ws.config.testing.insecureTrustExchange) {
|
2021-12-08 16:23:00 +01:00
|
|
|
valid = true;
|
|
|
|
} else {
|
2022-03-23 21:24:23 +01:00
|
|
|
const res = await ws.cryptoApi.isValidDenom({
|
2021-12-08 16:23:00 +01:00
|
|
|
denom,
|
2022-03-23 21:24:23 +01:00
|
|
|
masterPub: exchangeDetails.masterPublicKey,
|
|
|
|
});
|
|
|
|
valid = res.valid;
|
2021-12-08 16:23:00 +01:00
|
|
|
}
|
2021-08-06 16:27:18 +02:00
|
|
|
logger.trace(`Done validating ${denom.denomPubHash}`);
|
|
|
|
if (!valid) {
|
|
|
|
logger.warn(
|
|
|
|
`Signature check for denomination h=${denom.denomPubHash} failed`,
|
|
|
|
);
|
2021-08-24 15:43:06 +02:00
|
|
|
denom.verificationStatus = DenominationVerificationStatus.VerifiedBad;
|
2021-08-06 16:27:18 +02:00
|
|
|
} else {
|
2021-11-17 10:23:22 +01:00
|
|
|
denom.verificationStatus =
|
|
|
|
DenominationVerificationStatus.VerifiedGood;
|
2021-08-06 16:27:18 +02:00
|
|
|
}
|
|
|
|
updatedDenominations.push(denom);
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
2021-08-06 16:27:18 +02:00
|
|
|
}
|
|
|
|
if (updatedDenominations.length > 0) {
|
|
|
|
logger.trace("writing denomination batch to db");
|
2021-06-09 15:14:17 +02:00
|
|
|
await ws.db
|
2022-09-13 13:25:41 +02:00
|
|
|
.mktx((x) => [x.denominations])
|
2021-06-09 15:14:17 +02:00
|
|
|
.runReadWrite(async (tx) => {
|
2021-08-06 16:27:18 +02:00
|
|
|
for (let i = 0; i < updatedDenominations.length; i++) {
|
|
|
|
const denom = updatedDenominations[i];
|
|
|
|
await tx.denominations.put(denom);
|
|
|
|
}
|
2021-06-09 15:14:17 +02:00
|
|
|
});
|
2021-08-06 16:27:18 +02:00
|
|
|
logger.trace("done with DB write");
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
2020-07-16 13:52:03 +02:00
|
|
|
}
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
|
|
|
|
2022-08-09 15:00:45 +02:00
|
|
|
/**
|
|
|
|
* Update the information about a reserve that is stored in the wallet
|
|
|
|
* by querying the reserve's exchange.
|
|
|
|
*
|
|
|
|
* If the reserve have funds that are not allocated in a withdrawal group yet
|
|
|
|
* and are big enough to withdraw with available denominations,
|
|
|
|
* create a new withdrawal group for the remaining amount.
|
|
|
|
*/
|
|
|
|
async function queryReserve(
|
|
|
|
ws: InternalWalletState,
|
|
|
|
withdrawalGroupId: string,
|
2022-09-23 18:56:21 +02:00
|
|
|
cancellationToken: CancellationToken,
|
2022-08-09 15:00:45 +02:00
|
|
|
): Promise<{ ready: boolean }> {
|
|
|
|
const withdrawalGroup = await getWithdrawalGroupRecordTx(ws.db, {
|
|
|
|
withdrawalGroupId,
|
|
|
|
});
|
|
|
|
checkDbInvariant(!!withdrawalGroup);
|
2022-09-21 20:46:45 +02:00
|
|
|
if (withdrawalGroup.status !== WithdrawalGroupStatus.QueryingStatus) {
|
2022-08-09 15:00:45 +02:00
|
|
|
return { ready: true };
|
|
|
|
}
|
|
|
|
const reservePub = withdrawalGroup.reservePub;
|
|
|
|
|
|
|
|
const reserveUrl = new URL(
|
|
|
|
`reserves/${reservePub}`,
|
|
|
|
withdrawalGroup.exchangeBaseUrl,
|
|
|
|
);
|
|
|
|
reserveUrl.searchParams.set("timeout_ms", "30000");
|
|
|
|
|
2023-01-17 19:59:30 +01:00
|
|
|
logger.info(`querying reserve status via ${reserveUrl.href}`);
|
2022-08-09 15:00:45 +02:00
|
|
|
|
|
|
|
const resp = await ws.http.get(reserveUrl.href, {
|
|
|
|
timeout: getReserveRequestTimeout(withdrawalGroup),
|
2022-09-23 18:56:21 +02:00
|
|
|
cancellationToken,
|
2022-08-09 15:00:45 +02:00
|
|
|
});
|
|
|
|
|
2023-01-18 20:08:16 +01:00
|
|
|
logger.info(`reserve status code: HTTP ${resp.status}`);
|
|
|
|
|
2022-08-09 15:00:45 +02:00
|
|
|
const result = await readSuccessResponseJsonOrErrorCode(
|
|
|
|
resp,
|
|
|
|
codecForReserveStatus(),
|
|
|
|
);
|
|
|
|
|
|
|
|
if (result.isError) {
|
2023-01-18 20:08:16 +01:00
|
|
|
logger.info(
|
|
|
|
`got reserve status error, EC=${result.talerErrorResponse.code}`,
|
|
|
|
);
|
2022-08-09 15:00:45 +02:00
|
|
|
if (
|
|
|
|
resp.status === 404 &&
|
|
|
|
result.talerErrorResponse.code ===
|
2022-09-21 01:14:57 +02:00
|
|
|
TalerErrorCode.EXCHANGE_RESERVES_STATUS_UNKNOWN
|
2022-08-09 15:00:45 +02:00
|
|
|
) {
|
|
|
|
ws.notify({
|
|
|
|
type: NotificationType.ReserveNotYetFound,
|
|
|
|
reservePub,
|
|
|
|
});
|
|
|
|
return { ready: false };
|
|
|
|
} else {
|
|
|
|
throwUnexpectedRequestError(resp, result.talerErrorResponse);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
logger.trace(`got reserve status ${j2s(result.response)}`);
|
|
|
|
|
|
|
|
await ws.db
|
2022-09-13 13:25:41 +02:00
|
|
|
.mktx((x) => [x.withdrawalGroups])
|
2022-08-09 15:00:45 +02:00
|
|
|
.runReadWrite(async (tx) => {
|
|
|
|
const wg = await tx.withdrawalGroups.get(withdrawalGroupId);
|
|
|
|
if (!wg) {
|
|
|
|
logger.warn(`withdrawal group ${withdrawalGroupId} not found`);
|
|
|
|
return;
|
|
|
|
}
|
2022-09-23 21:47:38 +02:00
|
|
|
wg.status = WithdrawalGroupStatus.Ready;
|
2022-11-02 17:42:14 +01:00
|
|
|
wg.reserveBalanceAmount = Amounts.stringify(result.response.balance);
|
2022-08-09 15:00:45 +02:00
|
|
|
await tx.withdrawalGroups.put(wg);
|
|
|
|
});
|
|
|
|
|
2023-02-13 13:15:47 +01:00
|
|
|
ws.notify({
|
|
|
|
type: NotificationType.WithdrawalGroupReserveReady,
|
|
|
|
transactionId: makeTransactionId(
|
|
|
|
TransactionType.Withdrawal,
|
|
|
|
withdrawalGroupId,
|
|
|
|
),
|
|
|
|
});
|
|
|
|
|
2022-08-09 15:00:45 +02:00
|
|
|
return { ready: true };
|
|
|
|
}
|
|
|
|
|
2022-09-16 16:06:55 +02:00
|
|
|
enum BankStatusResultCode {
|
|
|
|
Done = "done",
|
|
|
|
Waiting = "waiting",
|
|
|
|
Aborted = "aborted",
|
|
|
|
}
|
|
|
|
|
2023-02-09 22:44:36 +01:00
|
|
|
/**
|
|
|
|
* Withdrawal context that is kept in-memory.
|
|
|
|
*
|
|
|
|
* Used to store some cached info during a withdrawal operation.
|
|
|
|
*/
|
|
|
|
export interface WithdrawalGroupContext {
|
|
|
|
numPlanchets: number;
|
|
|
|
planchetsFinished: Set<string>;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Cached withdrawal group record from the database.
|
|
|
|
*/
|
|
|
|
wgRecord: WithdrawalGroupRecord;
|
|
|
|
}
|
|
|
|
|
2022-08-09 15:00:45 +02:00
|
|
|
export async function processWithdrawalGroup(
|
2019-12-02 00:42:40 +01:00
|
|
|
ws: InternalWalletState,
|
2020-04-02 17:03:01 +02:00
|
|
|
withdrawalGroupId: string,
|
2023-02-20 21:26:08 +01:00
|
|
|
options: {} = {},
|
2022-09-05 18:12:30 +02:00
|
|
|
): Promise<OperationAttemptResult> {
|
|
|
|
logger.trace("processing withdrawal group", withdrawalGroupId);
|
2021-06-09 15:14:17 +02:00
|
|
|
const withdrawalGroup = await ws.db
|
2022-09-13 13:25:41 +02:00
|
|
|
.mktx((x) => [x.withdrawalGroups])
|
2021-06-09 15:14:17 +02:00
|
|
|
.runReadOnly(async (tx) => {
|
|
|
|
return tx.withdrawalGroups.get(withdrawalGroupId);
|
|
|
|
});
|
2022-08-09 15:00:45 +02:00
|
|
|
|
2020-04-02 17:03:01 +02:00
|
|
|
if (!withdrawalGroup) {
|
2022-08-09 15:00:45 +02:00
|
|
|
throw Error(`withdrawal group ${withdrawalGroupId} not found`);
|
|
|
|
}
|
|
|
|
|
2023-02-20 20:14:37 +01:00
|
|
|
const retryTag = TaskIdentifiers.forWithdrawal(withdrawalGroup);
|
2022-09-23 18:56:21 +02:00
|
|
|
|
|
|
|
// We're already running!
|
2023-02-20 20:14:37 +01:00
|
|
|
if (ws.activeLongpoll[retryTag]) {
|
2022-09-23 18:56:21 +02:00
|
|
|
logger.info("withdrawal group already in long-polling, returning!");
|
|
|
|
return {
|
|
|
|
type: OperationAttemptResultType.Longpoll,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2022-09-21 20:46:45 +02:00
|
|
|
switch (withdrawalGroup.status) {
|
|
|
|
case WithdrawalGroupStatus.RegisteringBank:
|
2022-08-09 15:00:45 +02:00
|
|
|
await processReserveBankStatus(ws, withdrawalGroupId);
|
2022-09-05 18:12:30 +02:00
|
|
|
return await processWithdrawalGroup(ws, withdrawalGroupId, {
|
2022-08-09 15:00:45 +02:00
|
|
|
forceNow: true,
|
2021-12-13 11:28:15 +01:00
|
|
|
});
|
2022-09-21 20:46:45 +02:00
|
|
|
case WithdrawalGroupStatus.QueryingStatus: {
|
2023-02-20 21:26:08 +01:00
|
|
|
runLongpollAsync(ws, retryTag, (ct) => {
|
|
|
|
return queryReserve(ws, withdrawalGroupId, ct);
|
|
|
|
});
|
2022-10-05 18:31:56 +02:00
|
|
|
logger.trace(
|
2022-09-23 18:56:21 +02:00
|
|
|
"returning early from withdrawal for long-polling in background",
|
|
|
|
);
|
2022-09-05 18:12:30 +02:00
|
|
|
return {
|
2022-09-23 18:56:21 +02:00
|
|
|
type: OperationAttemptResultType.Longpoll,
|
2022-09-05 18:12:30 +02:00
|
|
|
};
|
|
|
|
}
|
2022-09-21 20:46:45 +02:00
|
|
|
case WithdrawalGroupStatus.WaitConfirmBank: {
|
2022-09-05 18:12:30 +02:00
|
|
|
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,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
2022-09-16 16:06:55 +02:00
|
|
|
break;
|
2021-12-13 11:28:15 +01:00
|
|
|
}
|
2022-09-21 20:46:45 +02:00
|
|
|
case WithdrawalGroupStatus.BankAborted: {
|
2022-08-09 15:00:45 +02:00
|
|
|
// FIXME
|
2022-09-05 18:12:30 +02:00
|
|
|
return {
|
|
|
|
type: OperationAttemptResultType.Pending,
|
|
|
|
result: undefined,
|
|
|
|
};
|
2022-09-16 16:06:55 +02:00
|
|
|
}
|
2022-09-21 20:46:45 +02:00
|
|
|
case WithdrawalGroupStatus.Finished:
|
2022-08-09 15:00:45 +02:00
|
|
|
// We can try to withdraw, nothing needs to be done with the reserve.
|
|
|
|
break;
|
2022-09-23 21:47:38 +02:00
|
|
|
case WithdrawalGroupStatus.Ready:
|
|
|
|
// Continue with the actual withdrawal!
|
|
|
|
break;
|
2022-08-09 15:00:45 +02:00
|
|
|
default:
|
2022-09-21 20:46:45 +02:00
|
|
|
throw new InvariantViolatedError(
|
|
|
|
`unknown reserve record status: ${withdrawalGroup.status}`,
|
2022-08-09 15:00:45 +02:00
|
|
|
);
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
|
|
|
|
2021-06-17 15:49:05 +02:00
|
|
|
await ws.exchangeOps.updateExchangeFromUrl(
|
|
|
|
ws,
|
|
|
|
withdrawalGroup.exchangeBaseUrl,
|
|
|
|
);
|
2020-09-03 13:59:09 +02:00
|
|
|
|
2022-08-26 01:18:01 +02:00
|
|
|
if (withdrawalGroup.denomsSel.selectedDenoms.length === 0) {
|
2022-09-20 21:44:21 +02:00
|
|
|
logger.warn("Finishing empty withdrawal group (no denoms)");
|
2022-08-26 01:18:01 +02:00
|
|
|
await ws.db
|
2022-09-13 13:25:41 +02:00
|
|
|
.mktx((x) => [x.withdrawalGroups])
|
2022-08-26 01:18:01 +02:00
|
|
|
.runReadWrite(async (tx) => {
|
|
|
|
const wg = await tx.withdrawalGroups.get(withdrawalGroupId);
|
|
|
|
if (!wg) {
|
|
|
|
return;
|
|
|
|
}
|
2022-09-21 20:46:45 +02:00
|
|
|
wg.status = WithdrawalGroupStatus.Finished;
|
2022-09-20 21:44:21 +02:00
|
|
|
wg.timestampFinish = TalerProtocolTimestamp.now();
|
2022-08-26 01:18:01 +02:00
|
|
|
await tx.withdrawalGroups.put(wg);
|
|
|
|
});
|
2022-09-05 18:12:30 +02:00
|
|
|
return {
|
|
|
|
type: OperationAttemptResultType.Finished,
|
|
|
|
result: undefined,
|
|
|
|
};
|
2022-08-26 01:18:01 +02:00
|
|
|
}
|
|
|
|
|
2020-08-13 11:45:01 +02:00
|
|
|
const numTotalCoins = withdrawalGroup.denomsSel.selectedDenoms
|
|
|
|
.map((x) => x.count)
|
|
|
|
.reduce((a, b) => a + b);
|
|
|
|
|
2023-02-09 22:44:36 +01:00
|
|
|
const wgContext: WithdrawalGroupContext = {
|
|
|
|
numPlanchets: numTotalCoins,
|
|
|
|
planchetsFinished: new Set<string>(),
|
|
|
|
wgRecord: withdrawalGroup,
|
|
|
|
};
|
|
|
|
|
|
|
|
await ws.db
|
|
|
|
.mktx((x) => [x.planchets])
|
|
|
|
.runReadOnly(async (tx) => {
|
|
|
|
const planchets = await tx.planchets.indexes.byGroup.getAll(
|
|
|
|
withdrawalGroupId,
|
|
|
|
);
|
|
|
|
for (const p of planchets) {
|
|
|
|
if (p.planchetStatus === PlanchetStatus.WithdrawalDone) {
|
|
|
|
wgContext.planchetsFinished.add(p.coinPub);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2023-02-10 13:21:37 +01:00
|
|
|
// We sequentially generate planchets, so that
|
|
|
|
// large withdrawal groups don't make the wallet unresponsive.
|
2020-08-13 11:45:01 +02:00
|
|
|
for (let i = 0; i < numTotalCoins; i++) {
|
2023-02-10 13:21:37 +01:00
|
|
|
await processPlanchetGenerate(ws, withdrawalGroup, i);
|
2020-08-13 11:45:01 +02:00
|
|
|
}
|
|
|
|
|
2023-02-10 13:21:37 +01:00
|
|
|
const maxBatchSize = 100;
|
2020-08-13 11:45:01 +02:00
|
|
|
|
2023-02-10 13:21:37 +01:00
|
|
|
for (let i = 0; i < numTotalCoins; i += maxBatchSize) {
|
|
|
|
const resp = await processPlanchetExchangeBatchRequest(ws, wgContext, {
|
|
|
|
batchSize: maxBatchSize,
|
|
|
|
coinStartIndex: i,
|
2023-04-19 17:42:47 +02:00
|
|
|
useBatchRequest: ws.config.features.batchWithdrawal,
|
2023-02-10 13:21:37 +01:00
|
|
|
});
|
|
|
|
let work: Promise<void>[] = [];
|
|
|
|
work = [];
|
|
|
|
for (let j = 0; j < resp.coinIdxs.length; j++) {
|
2023-02-10 16:52:05 +01:00
|
|
|
if (!resp.batchResp.ev_sigs[j]) {
|
|
|
|
//response may not be available when there is kyc needed
|
2023-02-10 16:53:27 +01:00
|
|
|
continue;
|
2023-02-10 16:52:05 +01:00
|
|
|
}
|
2022-05-03 17:53:32 +02:00
|
|
|
work.push(
|
|
|
|
processPlanchetVerifyAndStoreCoin(
|
|
|
|
ws,
|
2023-02-09 22:44:36 +01:00
|
|
|
wgContext,
|
2023-02-10 13:21:37 +01:00
|
|
|
resp.coinIdxs[j],
|
|
|
|
resp.batchResp.ev_sigs[j],
|
2022-05-03 17:53:32 +02:00
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
2023-02-10 13:21:37 +01:00
|
|
|
await Promise.all(work);
|
2020-08-13 11:45:01 +02:00
|
|
|
}
|
2020-05-11 14:33:25 +02:00
|
|
|
|
2020-08-13 11:45:01 +02:00
|
|
|
let numFinished = 0;
|
2022-11-01 13:39:42 +01:00
|
|
|
let numKycRequired = 0;
|
2020-08-13 11:45:01 +02:00
|
|
|
let finishedForFirstTime = false;
|
2023-01-17 19:59:30 +01:00
|
|
|
const errorsPerCoin: Record<number, TalerErrorDetail> = {};
|
2020-08-13 11:45:01 +02:00
|
|
|
|
2023-01-17 19:59:30 +01:00
|
|
|
const res = await ws.db
|
2022-09-13 13:25:41 +02:00
|
|
|
.mktx((x) => [x.coins, x.withdrawalGroups, x.planchets])
|
2021-06-09 15:14:17 +02:00
|
|
|
.runReadWrite(async (tx) => {
|
|
|
|
const wg = await tx.withdrawalGroups.get(withdrawalGroupId);
|
2020-09-01 14:30:46 +02:00
|
|
|
if (!wg) {
|
2020-08-13 11:45:01 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-06-09 15:14:17 +02:00
|
|
|
await tx.planchets.indexes.byGroup
|
|
|
|
.iter(withdrawalGroupId)
|
2020-08-13 11:45:01 +02:00
|
|
|
.forEach((x) => {
|
2022-10-14 21:00:13 +02:00
|
|
|
if (x.planchetStatus === PlanchetStatus.WithdrawalDone) {
|
2020-08-13 11:45:01 +02:00
|
|
|
numFinished++;
|
|
|
|
}
|
2022-11-01 13:39:42 +01:00
|
|
|
if (x.planchetStatus === PlanchetStatus.KycRequired) {
|
|
|
|
numKycRequired++;
|
|
|
|
}
|
2020-09-01 14:30:46 +02:00
|
|
|
if (x.lastError) {
|
|
|
|
errorsPerCoin[x.coinIdx] = x.lastError;
|
|
|
|
}
|
2020-08-13 11:45:01 +02:00
|
|
|
});
|
2022-09-20 21:44:21 +02:00
|
|
|
logger.info(`now withdrawn ${numFinished} of ${numTotalCoins} coins`);
|
2020-09-01 14:30:46 +02:00
|
|
|
if (wg.timestampFinish === undefined && numFinished === numTotalCoins) {
|
2020-08-13 11:45:01 +02:00
|
|
|
finishedForFirstTime = true;
|
2022-03-18 15:32:41 +01:00
|
|
|
wg.timestampFinish = TalerProtocolTimestamp.now();
|
2022-09-21 20:46:45 +02:00
|
|
|
wg.status = WithdrawalGroupStatus.Finished;
|
2020-08-13 11:45:01 +02:00
|
|
|
}
|
2020-09-01 14:30:46 +02:00
|
|
|
|
2021-06-09 15:14:17 +02:00
|
|
|
await tx.withdrawalGroups.put(wg);
|
2023-01-10 17:31:01 +01:00
|
|
|
|
|
|
|
return {
|
|
|
|
kycInfo: wg.kycPending,
|
|
|
|
};
|
2021-06-09 15:14:17 +02:00
|
|
|
});
|
2023-01-10 17:31:01 +01:00
|
|
|
|
|
|
|
if (!res) {
|
|
|
|
throw Error("withdrawal group does not exist anymore");
|
|
|
|
}
|
|
|
|
|
|
|
|
const { kycInfo } = res;
|
|
|
|
|
2022-11-01 13:39:42 +01:00
|
|
|
if (numKycRequired > 0) {
|
2023-01-10 17:31:01 +01:00
|
|
|
if (kycInfo) {
|
2023-03-29 05:06:24 +02:00
|
|
|
const txId = makeTransactionId(
|
|
|
|
TransactionType.Withdrawal,
|
|
|
|
withdrawalGroup.withdrawalGroupId,
|
|
|
|
);
|
2023-02-13 13:15:47 +01:00
|
|
|
await checkWithdrawalKycStatus(
|
|
|
|
ws,
|
2023-03-29 05:06:24 +02:00
|
|
|
withdrawalGroup.exchangeBaseUrl,
|
|
|
|
txId,
|
2023-02-13 13:15:47 +01:00
|
|
|
kycInfo,
|
|
|
|
"individual",
|
|
|
|
);
|
2023-01-17 19:59:30 +01:00
|
|
|
return {
|
|
|
|
type: OperationAttemptResultType.Pending,
|
|
|
|
result: undefined,
|
|
|
|
};
|
2023-01-10 17:31:01 +01:00
|
|
|
} else {
|
|
|
|
throw TalerError.fromDetail(
|
|
|
|
TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED,
|
2023-01-17 19:59:30 +01:00
|
|
|
{
|
|
|
|
//FIXME we can't rise KYC error here since we don't have the url
|
|
|
|
} as any,
|
2023-01-10 17:31:01 +01:00
|
|
|
`KYC check required for withdrawal (not yet implemented in wallet-core)`,
|
|
|
|
);
|
|
|
|
}
|
2022-11-01 13:39:42 +01:00
|
|
|
}
|
2020-08-13 11:45:01 +02:00
|
|
|
if (numFinished != numTotalCoins) {
|
2022-03-22 21:16:38 +01:00
|
|
|
throw TalerError.fromDetail(
|
2020-09-01 14:30:46 +02:00
|
|
|
TalerErrorCode.WALLET_WITHDRAWAL_GROUP_INCOMPLETE,
|
|
|
|
{
|
|
|
|
errorsPerCoin,
|
|
|
|
},
|
2022-03-22 21:16:38 +01:00
|
|
|
`withdrawal did not finish (${numFinished} / ${numTotalCoins} coins withdrawn)`,
|
2020-08-13 11:45:01 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (finishedForFirstTime) {
|
|
|
|
ws.notify({
|
|
|
|
type: NotificationType.WithdrawGroupFinished,
|
2020-09-08 15:57:08 +02:00
|
|
|
reservePub: withdrawalGroup.reservePub,
|
2020-08-13 11:45:01 +02:00
|
|
|
});
|
|
|
|
}
|
2022-09-05 18:12:30 +02:00
|
|
|
|
|
|
|
return {
|
|
|
|
type: OperationAttemptResultType.Finished,
|
|
|
|
result: undefined,
|
|
|
|
};
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
|
|
|
|
2023-02-12 19:30:59 +01:00
|
|
|
export async function checkWithdrawalKycStatus(
|
2023-01-17 19:59:30 +01:00
|
|
|
ws: InternalWalletState,
|
2023-03-29 05:06:24 +02:00
|
|
|
exchangeUrl: string,
|
|
|
|
txId: string,
|
2023-01-17 19:59:30 +01:00
|
|
|
kycInfo: KycPendingInfo,
|
|
|
|
userType: KycUserType,
|
|
|
|
): Promise<void> {
|
|
|
|
const url = new URL(
|
|
|
|
`kyc-check/${kycInfo.requirementRow}/${kycInfo.paytoHash}/${userType}`,
|
|
|
|
exchangeUrl,
|
|
|
|
);
|
|
|
|
logger.info(`kyc url ${url.href}`);
|
2023-03-29 05:06:24 +02:00
|
|
|
const kycStatusRes = await ws.http.fetch(url.href, {
|
2023-01-17 19:59:30 +01:00
|
|
|
method: "GET",
|
|
|
|
});
|
2023-03-29 05:06:24 +02:00
|
|
|
if (
|
|
|
|
kycStatusRes.status === HttpStatusCode.Ok ||
|
|
|
|
//FIXME: NoContent is not expected https://docs.taler.net/core/api-exchange.html#post--purses-$PURSE_PUB-merge
|
|
|
|
// remove after the exchange is fixed or clarified
|
|
|
|
kycStatusRes.status === HttpStatusCode.NoContent
|
|
|
|
) {
|
2023-02-12 19:30:59 +01:00
|
|
|
logger.warn("kyc requested, but already fulfilled");
|
2023-01-17 19:59:30 +01:00
|
|
|
return;
|
2023-03-29 05:06:24 +02:00
|
|
|
} else if (kycStatusRes.status === HttpStatusCode.Accepted) {
|
|
|
|
const kycStatus = await kycStatusRes.json();
|
2023-01-17 19:59:30 +01:00
|
|
|
logger.info(`kyc status: ${j2s(kycStatus)}`);
|
2023-02-12 19:30:59 +01:00
|
|
|
ws.notify({
|
2023-03-29 05:06:24 +02:00
|
|
|
type: NotificationType.KycRequested,
|
2023-02-12 19:30:59 +01:00
|
|
|
kycUrl: kycStatus.kyc_url,
|
2023-03-29 05:06:24 +02:00
|
|
|
transactionId: txId,
|
2023-02-12 19:30:59 +01:00
|
|
|
});
|
2023-01-17 19:59:30 +01:00
|
|
|
throw TalerError.fromDetail(
|
2023-03-29 05:06:24 +02:00
|
|
|
TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED, //FIXME: another error code or rename for merge
|
2023-01-17 19:59:30 +01:00
|
|
|
{
|
|
|
|
kycUrl: kycStatus.kyc_url,
|
|
|
|
},
|
2023-03-29 05:06:24 +02:00
|
|
|
`KYC check required for transfer`,
|
2023-01-17 19:59:30 +01:00
|
|
|
);
|
|
|
|
} else {
|
2023-03-29 05:06:24 +02:00
|
|
|
throw Error(`unexpected response from kyc-check (${kycStatusRes.status})`);
|
2023-01-17 19:59:30 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-13 13:25:41 +02:00
|
|
|
const AGE_MASK_GROUPS = "8:10:12:14:16:18"
|
|
|
|
.split(":")
|
|
|
|
.map((n) => parseInt(n, 10));
|
2022-09-06 22:17:44 +02:00
|
|
|
|
2019-12-09 19:59:08 +01:00
|
|
|
export async function getExchangeWithdrawalInfo(
|
2019-12-02 00:42:40 +01:00
|
|
|
ws: InternalWalletState,
|
2022-01-13 12:08:31 +01:00
|
|
|
exchangeBaseUrl: string,
|
2022-03-28 16:11:58 +02:00
|
|
|
instructedAmount: AmountJson,
|
2022-09-06 22:17:44 +02:00
|
|
|
ageRestricted: number | undefined,
|
2022-10-14 18:40:04 +02:00
|
|
|
): Promise<ExchangeWithdrawalDetails> {
|
2022-01-10 01:19:19 +01:00
|
|
|
const { exchange, exchangeDetails } =
|
2022-01-13 12:08:31 +01:00
|
|
|
await ws.exchangeOps.updateExchangeFromUrl(ws, exchangeBaseUrl);
|
|
|
|
await updateWithdrawalDenoms(ws, exchangeBaseUrl);
|
|
|
|
const denoms = await getCandidateWithdrawalDenoms(ws, exchangeBaseUrl);
|
2022-03-28 16:11:58 +02:00
|
|
|
const selectedDenoms = selectWithdrawalDenominations(
|
|
|
|
instructedAmount,
|
|
|
|
denoms,
|
2023-04-19 17:42:47 +02:00
|
|
|
ws.config.testing.denomselAllowLate,
|
2022-03-28 16:11:58 +02:00
|
|
|
);
|
2022-04-18 21:23:25 +02:00
|
|
|
|
|
|
|
if (selectedDenoms.selectedDenoms.length === 0) {
|
|
|
|
throw Error(
|
|
|
|
`unable to withdraw from ${exchangeBaseUrl}, can't select denominations for instructed amount (${Amounts.stringify(
|
|
|
|
instructedAmount,
|
|
|
|
)}`,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2019-12-02 00:42:40 +01:00
|
|
|
const exchangeWireAccounts: string[] = [];
|
2021-06-02 13:23:51 +02:00
|
|
|
for (const account of exchangeDetails.wireInfo.accounts) {
|
2020-01-19 19:02:47 +01:00
|
|
|
exchangeWireAccounts.push(account.payto_uri);
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
|
|
|
|
2021-06-17 15:49:05 +02:00
|
|
|
const { isTrusted, isAudited } = await ws.exchangeOps.getExchangeTrust(
|
|
|
|
ws,
|
|
|
|
exchange,
|
|
|
|
);
|
2019-12-02 00:42:40 +01:00
|
|
|
|
2022-09-13 13:25:41 +02:00
|
|
|
let hasDenomWithAgeRestriction = false;
|
2022-09-06 22:17:44 +02:00
|
|
|
|
2022-03-29 21:21:57 +02:00
|
|
|
let earliestDepositExpiration: TalerProtocolTimestamp | undefined;
|
2022-04-18 22:00:26 +02:00
|
|
|
for (let i = 0; i < selectedDenoms.selectedDenoms.length; i++) {
|
2022-03-29 21:21:57 +02:00
|
|
|
const ds = selectedDenoms.selectedDenoms[i];
|
|
|
|
// FIXME: Do in one transaction!
|
|
|
|
const denom = await ws.db
|
2022-09-13 13:25:41 +02:00
|
|
|
.mktx((x) => [x.denominations])
|
2022-03-29 21:21:57 +02:00
|
|
|
.runReadOnly(async (tx) => {
|
|
|
|
return ws.getDenomInfo(ws, tx, exchangeBaseUrl, ds.denomPubHash);
|
|
|
|
});
|
|
|
|
checkDbInvariant(!!denom);
|
2022-09-21 01:14:46 +02:00
|
|
|
hasDenomWithAgeRestriction =
|
|
|
|
hasDenomWithAgeRestriction || denom.denomPub.age_mask > 0;
|
2022-03-29 21:21:57 +02:00
|
|
|
const expireDeposit = denom.stampExpireDeposit;
|
|
|
|
if (!earliestDepositExpiration) {
|
|
|
|
earliestDepositExpiration = expireDeposit;
|
|
|
|
continue;
|
|
|
|
}
|
2022-03-18 15:32:41 +01:00
|
|
|
if (
|
|
|
|
AbsoluteTime.cmp(
|
|
|
|
AbsoluteTime.fromTimestamp(expireDeposit),
|
|
|
|
AbsoluteTime.fromTimestamp(earliestDepositExpiration),
|
|
|
|
) < 0
|
|
|
|
) {
|
2019-12-02 00:42:40 +01:00
|
|
|
earliestDepositExpiration = expireDeposit;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-03-29 21:21:57 +02:00
|
|
|
checkLogicInvariant(!!earliestDepositExpiration);
|
|
|
|
|
2019-12-19 20:42:49 +01:00
|
|
|
const possibleDenoms = await ws.db
|
2022-09-13 13:25:41 +02:00
|
|
|
.mktx((x) => [x.denominations])
|
2021-06-09 15:14:17 +02:00
|
|
|
.runReadOnly(async (tx) => {
|
2022-01-13 12:08:31 +01:00
|
|
|
const ds = await tx.denominations.indexes.byExchangeBaseUrl.getAll(
|
|
|
|
exchangeBaseUrl,
|
|
|
|
);
|
|
|
|
return ds.filter((x) => x.isOffered);
|
2021-06-09 15:14:17 +02:00
|
|
|
});
|
2019-12-02 00:42:40 +01:00
|
|
|
|
|
|
|
let versionMatch;
|
2022-10-14 21:00:13 +02:00
|
|
|
if (exchangeDetails.protocolVersionRange) {
|
2021-11-23 23:51:12 +01:00
|
|
|
versionMatch = LibtoolVersion.compare(
|
2019-12-16 16:59:09 +01:00
|
|
|
WALLET_EXCHANGE_PROTOCOL_VERSION,
|
2022-10-14 21:00:13 +02:00
|
|
|
exchangeDetails.protocolVersionRange,
|
2019-12-02 00:42:40 +01:00
|
|
|
);
|
|
|
|
|
|
|
|
if (
|
|
|
|
versionMatch &&
|
|
|
|
!versionMatch.compatible &&
|
|
|
|
versionMatch.currentCmp === -1
|
|
|
|
) {
|
2022-08-09 15:00:45 +02:00
|
|
|
logger.warn(
|
2019-12-16 16:59:09 +01:00
|
|
|
`wallet's support for exchange protocol version ${WALLET_EXCHANGE_PROTOCOL_VERSION} might be outdated ` +
|
2022-10-14 21:00:13 +02:00
|
|
|
`(exchange has ${exchangeDetails.protocolVersionRange}), checking for updates`,
|
2019-12-02 00:42:40 +01:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-12-09 19:59:08 +01:00
|
|
|
let tosAccepted = false;
|
2022-10-14 22:10:03 +02:00
|
|
|
if (exchangeDetails.tosAccepted?.timestamp) {
|
|
|
|
if (exchangeDetails.tosAccepted.etag === exchangeDetails.tosCurrentEtag) {
|
2019-12-09 19:59:08 +01:00
|
|
|
tosAccepted = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-14 18:40:04 +02:00
|
|
|
const paytoUris = exchangeDetails.wireInfo.accounts.map((x) => x.payto_uri);
|
|
|
|
if (!paytoUris) {
|
|
|
|
throw Error("exchange is in invalid state");
|
|
|
|
}
|
|
|
|
|
|
|
|
const ret: ExchangeWithdrawalDetails = {
|
2019-12-02 00:42:40 +01:00
|
|
|
earliestDepositExpiration,
|
2022-10-14 18:40:04 +02:00
|
|
|
exchangePaytoUris: paytoUris,
|
2019-12-02 00:42:40 +01:00
|
|
|
exchangeWireAccounts,
|
2022-10-14 21:00:13 +02:00
|
|
|
exchangeVersion: exchangeDetails.protocolVersionRange || "unknown",
|
2019-12-02 00:42:40 +01:00
|
|
|
isAudited,
|
|
|
|
isTrusted,
|
|
|
|
numOfferedDenoms: possibleDenoms.length,
|
|
|
|
selectedDenoms,
|
2021-05-20 13:14:47 +02:00
|
|
|
// FIXME: delete this field / replace by something we can display to the user
|
|
|
|
trustedAuditorPubs: [],
|
2019-12-02 00:42:40 +01:00
|
|
|
versionMatch,
|
2019-12-16 16:59:09 +01:00
|
|
|
walletVersion: WALLET_EXCHANGE_PROTOCOL_VERSION,
|
2019-12-09 19:59:08 +01:00
|
|
|
termsOfServiceAccepted: tosAccepted,
|
2022-03-28 16:11:58 +02:00
|
|
|
withdrawalAmountEffective: Amounts.stringify(selectedDenoms.totalCoinValue),
|
|
|
|
withdrawalAmountRaw: Amounts.stringify(instructedAmount),
|
2022-09-06 22:17:44 +02:00
|
|
|
// TODO: remove hardcoding, this should be calculated from the denominations info
|
|
|
|
// force enabled for testing
|
2022-09-13 13:25:41 +02:00
|
|
|
ageRestrictionOptions: hasDenomWithAgeRestriction
|
|
|
|
? AGE_MASK_GROUPS
|
|
|
|
: undefined,
|
2019-12-02 00:42:40 +01:00
|
|
|
};
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
2022-04-19 17:12:43 +02:00
|
|
|
export interface GetWithdrawalDetailsForUriOpts {
|
|
|
|
restrictAge?: number;
|
|
|
|
}
|
|
|
|
|
2022-01-12 16:54:38 +01:00
|
|
|
/**
|
|
|
|
* Get more information about a taler://withdraw URI.
|
|
|
|
*
|
|
|
|
* As side effects, the bank (via the bank integration API) is queried
|
|
|
|
* and the exchange suggested by the bank is permanently added
|
|
|
|
* to the wallet's list of known exchanges.
|
|
|
|
*/
|
2020-07-28 10:52:35 +02:00
|
|
|
export async function getWithdrawalDetailsForUri(
|
2019-12-02 00:42:40 +01:00
|
|
|
ws: InternalWalletState,
|
|
|
|
talerWithdrawUri: string,
|
2022-04-19 17:12:43 +02:00
|
|
|
opts: GetWithdrawalDetailsForUriOpts = {},
|
2020-07-28 10:52:35 +02:00
|
|
|
): Promise<WithdrawUriInfoResponse> {
|
2020-08-05 21:00:36 +02:00
|
|
|
logger.trace(`getting withdrawal details for URI ${talerWithdrawUri}`);
|
2022-03-14 18:31:30 +01:00
|
|
|
const info = await getBankWithdrawalInfo(ws.http, talerWithdrawUri);
|
2020-08-05 21:00:36 +02:00
|
|
|
logger.trace(`got bank info`);
|
2020-07-28 10:52:35 +02:00
|
|
|
if (info.suggestedExchange) {
|
|
|
|
// FIXME: right now the exchange gets permanently added,
|
|
|
|
// we might want to only temporarily add it.
|
|
|
|
try {
|
2021-06-17 15:49:05 +02:00
|
|
|
await ws.exchangeOps.updateExchangeFromUrl(ws, info.suggestedExchange);
|
2020-07-28 10:52:35 +02:00
|
|
|
} catch (e) {
|
|
|
|
// We still continued if it failed, as other exchanges might be available.
|
|
|
|
// We don't want to fail if the bank-suggested exchange is broken/offline.
|
2020-08-03 09:30:48 +02:00
|
|
|
logger.trace(
|
|
|
|
`querying bank-suggested exchange (${info.suggestedExchange}) failed`,
|
|
|
|
);
|
2020-07-28 10:52:35 +02:00
|
|
|
}
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
2020-07-28 10:52:35 +02:00
|
|
|
|
2022-01-12 16:54:38 +01:00
|
|
|
// Extract information about possible exchanges for the withdrawal
|
|
|
|
// operation from the database.
|
|
|
|
|
2021-06-02 13:23:51 +02:00
|
|
|
const exchanges: ExchangeListItem[] = [];
|
|
|
|
|
2021-06-09 15:14:17 +02:00
|
|
|
await ws.db
|
2022-10-14 22:10:03 +02:00
|
|
|
.mktx((x) => [
|
|
|
|
x.exchanges,
|
|
|
|
x.exchangeDetails,
|
|
|
|
x.exchangeTos,
|
|
|
|
x.denominations,
|
2022-11-02 14:23:26 +01:00
|
|
|
x.operationRetries,
|
2022-10-14 22:10:03 +02:00
|
|
|
])
|
2021-06-09 15:14:17 +02:00
|
|
|
.runReadOnly(async (tx) => {
|
|
|
|
const exchangeRecords = await tx.exchanges.iter().toArray();
|
|
|
|
for (const r of exchangeRecords) {
|
2022-10-15 16:25:44 +02:00
|
|
|
const exchangeDetails = await ws.exchangeOps.getExchangeDetails(
|
|
|
|
tx,
|
|
|
|
r.baseUrl,
|
|
|
|
);
|
2022-08-24 19:44:24 +02:00
|
|
|
const denominations = await tx.denominations.indexes.byExchangeBaseUrl
|
|
|
|
.iter(r.baseUrl)
|
|
|
|
.toArray();
|
2022-11-02 14:23:26 +01:00
|
|
|
const retryRecord = await tx.operationRetries.get(
|
2023-02-20 20:14:37 +01:00
|
|
|
TaskIdentifiers.forExchangeUpdate(r),
|
2022-11-02 14:23:26 +01:00
|
|
|
);
|
2022-10-15 12:59:26 +02:00
|
|
|
if (exchangeDetails && denominations) {
|
2022-11-02 14:23:26 +01:00
|
|
|
exchanges.push(
|
|
|
|
makeExchangeListItem(r, exchangeDetails, retryRecord?.lastError),
|
|
|
|
);
|
2021-06-09 15:14:17 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
2020-07-28 10:52:35 +02:00
|
|
|
|
2019-12-02 00:42:40 +01:00
|
|
|
return {
|
2020-07-28 10:52:35 +02:00
|
|
|
amount: Amounts.stringify(info.amount),
|
|
|
|
defaultExchangeBaseUrl: info.suggestedExchange,
|
|
|
|
possibleExchanges: exchanges,
|
2020-08-03 09:30:48 +02:00
|
|
|
};
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
2022-08-09 15:00:45 +02:00
|
|
|
|
|
|
|
export async function getFundingPaytoUrisTx(
|
|
|
|
ws: InternalWalletState,
|
|
|
|
withdrawalGroupId: string,
|
|
|
|
): Promise<string[]> {
|
|
|
|
return await ws.db
|
2022-09-13 13:25:41 +02:00
|
|
|
.mktx((x) => [x.exchanges, x.exchangeDetails, x.withdrawalGroups])
|
2022-08-09 15:00:45 +02:00
|
|
|
.runReadWrite((tx) => getFundingPaytoUris(tx, withdrawalGroupId));
|
|
|
|
}
|
|
|
|
|
2022-10-07 14:45:25 +02:00
|
|
|
export function augmentPaytoUrisForWithdrawal(
|
|
|
|
plainPaytoUris: string[],
|
|
|
|
reservePub: string,
|
2022-11-02 17:42:14 +01:00
|
|
|
instructedAmount: AmountLike,
|
2022-10-07 14:45:25 +02:00
|
|
|
): string[] {
|
|
|
|
return plainPaytoUris.map((x) =>
|
|
|
|
addPaytoQueryParams(x, {
|
|
|
|
amount: Amounts.stringify(instructedAmount),
|
|
|
|
message: `Taler Withdrawal ${reservePub}`,
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-08-09 15:00:45 +02:00
|
|
|
/**
|
|
|
|
* Get payto URIs that can be used to fund a withdrawal operation.
|
|
|
|
*/
|
|
|
|
export async function getFundingPaytoUris(
|
|
|
|
tx: GetReadOnlyAccess<{
|
|
|
|
withdrawalGroups: typeof WalletStoresV1.withdrawalGroups;
|
|
|
|
exchanges: typeof WalletStoresV1.exchanges;
|
|
|
|
exchangeDetails: typeof WalletStoresV1.exchangeDetails;
|
|
|
|
}>,
|
|
|
|
withdrawalGroupId: string,
|
|
|
|
): Promise<string[]> {
|
|
|
|
const withdrawalGroup = await tx.withdrawalGroups.get(withdrawalGroupId);
|
|
|
|
checkDbInvariant(!!withdrawalGroup);
|
|
|
|
const exchangeDetails = await getExchangeDetails(
|
|
|
|
tx,
|
|
|
|
withdrawalGroup.exchangeBaseUrl,
|
|
|
|
);
|
|
|
|
if (!exchangeDetails) {
|
|
|
|
logger.error(`exchange ${withdrawalGroup.exchangeBaseUrl} not found`);
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
const plainPaytoUris =
|
|
|
|
exchangeDetails.wireInfo?.accounts.map((x) => x.payto_uri) ?? [];
|
|
|
|
if (!plainPaytoUris) {
|
|
|
|
logger.error(
|
|
|
|
`exchange ${withdrawalGroup.exchangeBaseUrl} has no wire info`,
|
|
|
|
);
|
|
|
|
return [];
|
|
|
|
}
|
2022-10-07 14:45:25 +02:00
|
|
|
return augmentPaytoUrisForWithdrawal(
|
|
|
|
plainPaytoUris,
|
|
|
|
withdrawalGroup.reservePub,
|
|
|
|
withdrawalGroup.instructedAmount,
|
2022-08-09 15:00:45 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
async function getWithdrawalGroupRecordTx(
|
|
|
|
db: DbAccess<typeof WalletStoresV1>,
|
|
|
|
req: {
|
|
|
|
withdrawalGroupId: string;
|
|
|
|
},
|
|
|
|
): Promise<WithdrawalGroupRecord | undefined> {
|
|
|
|
return await db
|
2022-09-13 13:25:41 +02:00
|
|
|
.mktx((x) => [x.withdrawalGroups])
|
2022-08-09 15:00:45 +02:00
|
|
|
.runReadOnly(async (tx) => {
|
|
|
|
return tx.withdrawalGroups.get(req.withdrawalGroupId);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
export function getReserveRequestTimeout(r: WithdrawalGroupRecord): Duration {
|
2022-09-05 18:12:30 +02:00
|
|
|
return { d_ms: 60000 };
|
2022-08-09 15:00:45 +02:00
|
|
|
}
|
|
|
|
|
2022-08-24 19:44:24 +02:00
|
|
|
export function getBankStatusUrl(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}`,
|
|
|
|
uriResult.bankIntegrationApiBaseUrl,
|
|
|
|
);
|
|
|
|
return url.href;
|
|
|
|
}
|
|
|
|
|
2022-08-09 15:00:45 +02:00
|
|
|
async function registerReserveWithBank(
|
|
|
|
ws: InternalWalletState,
|
|
|
|
withdrawalGroupId: string,
|
|
|
|
): Promise<void> {
|
|
|
|
const withdrawalGroup = await ws.db
|
2022-09-13 13:25:41 +02:00
|
|
|
.mktx((x) => [x.withdrawalGroups])
|
2022-08-09 15:00:45 +02:00
|
|
|
.runReadOnly(async (tx) => {
|
|
|
|
return await tx.withdrawalGroups.get(withdrawalGroupId);
|
|
|
|
});
|
2022-09-21 20:46:45 +02:00
|
|
|
switch (withdrawalGroup?.status) {
|
|
|
|
case WithdrawalGroupStatus.WaitConfirmBank:
|
|
|
|
case WithdrawalGroupStatus.RegisteringBank:
|
2022-08-09 15:00:45 +02:00
|
|
|
break;
|
|
|
|
default:
|
|
|
|
return;
|
|
|
|
}
|
2022-08-24 22:42:30 +02:00
|
|
|
if (
|
|
|
|
withdrawalGroup.wgInfo.withdrawalType != WithdrawalRecordType.BankIntegrated
|
|
|
|
) {
|
|
|
|
throw Error();
|
|
|
|
}
|
|
|
|
const bankInfo = withdrawalGroup.wgInfo.bankInfo;
|
2022-08-09 15:00:45 +02:00
|
|
|
if (!bankInfo) {
|
|
|
|
return;
|
|
|
|
}
|
2022-08-24 19:44:24 +02:00
|
|
|
const bankStatusUrl = getBankStatusUrl(bankInfo.talerWithdrawUri);
|
2022-08-25 17:49:24 +02:00
|
|
|
const reqBody = {
|
|
|
|
reserve_pub: withdrawalGroup.reservePub,
|
|
|
|
selected_exchange: bankInfo.exchangePaytoUri,
|
|
|
|
};
|
|
|
|
logger.info(`registering reserve with bank: ${j2s(reqBody)}`);
|
|
|
|
const httpResp = await ws.http.postJson(bankStatusUrl, reqBody, {
|
|
|
|
timeout: getReserveRequestTimeout(withdrawalGroup),
|
|
|
|
});
|
2022-08-09 15:00:45 +02:00
|
|
|
await readSuccessResponseJsonOrThrow(
|
|
|
|
httpResp,
|
|
|
|
codecForBankWithdrawalOperationPostResponse(),
|
|
|
|
);
|
|
|
|
await ws.db
|
2022-09-13 13:25:41 +02:00
|
|
|
.mktx((x) => [x.withdrawalGroups])
|
2022-08-09 15:00:45 +02:00
|
|
|
.runReadWrite(async (tx) => {
|
|
|
|
const r = await tx.withdrawalGroups.get(withdrawalGroupId);
|
|
|
|
if (!r) {
|
|
|
|
return;
|
|
|
|
}
|
2022-09-21 20:46:45 +02:00
|
|
|
switch (r.status) {
|
|
|
|
case WithdrawalGroupStatus.RegisteringBank:
|
|
|
|
case WithdrawalGroupStatus.WaitConfirmBank:
|
2022-08-09 15:00:45 +02:00
|
|
|
break;
|
|
|
|
default:
|
|
|
|
return;
|
|
|
|
}
|
2022-08-24 22:42:30 +02:00
|
|
|
if (r.wgInfo.withdrawalType !== WithdrawalRecordType.BankIntegrated) {
|
2022-08-09 15:00:45 +02:00
|
|
|
throw Error("invariant failed");
|
|
|
|
}
|
2022-08-24 22:42:30 +02:00
|
|
|
r.wgInfo.bankInfo.timestampReserveInfoPosted = AbsoluteTime.toTimestamp(
|
2022-08-09 15:00:45 +02:00
|
|
|
AbsoluteTime.now(),
|
|
|
|
);
|
2022-09-21 20:46:45 +02:00
|
|
|
r.status = WithdrawalGroupStatus.WaitConfirmBank;
|
2022-08-09 15:00:45 +02:00
|
|
|
await tx.withdrawalGroups.put(r);
|
|
|
|
});
|
|
|
|
ws.notify({ type: NotificationType.ReserveRegisteredWithBank });
|
2022-09-05 18:12:30 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
interface BankStatusResult {
|
|
|
|
status: BankStatusResultCode;
|
2022-08-09 15:00:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
async function processReserveBankStatus(
|
|
|
|
ws: InternalWalletState,
|
|
|
|
withdrawalGroupId: string,
|
2022-09-05 18:12:30 +02:00
|
|
|
): Promise<BankStatusResult> {
|
2022-08-09 15:00:45 +02:00
|
|
|
const withdrawalGroup = await getWithdrawalGroupRecordTx(ws.db, {
|
|
|
|
withdrawalGroupId,
|
|
|
|
});
|
2022-09-21 20:46:45 +02:00
|
|
|
switch (withdrawalGroup?.status) {
|
|
|
|
case WithdrawalGroupStatus.WaitConfirmBank:
|
|
|
|
case WithdrawalGroupStatus.RegisteringBank:
|
2022-08-09 15:00:45 +02:00
|
|
|
break;
|
|
|
|
default:
|
2022-09-05 18:12:30 +02:00
|
|
|
return {
|
|
|
|
status: BankStatusResultCode.Done,
|
|
|
|
};
|
2022-08-09 15:00:45 +02:00
|
|
|
}
|
2022-08-24 22:42:30 +02:00
|
|
|
|
|
|
|
if (
|
|
|
|
withdrawalGroup.wgInfo.withdrawalType != WithdrawalRecordType.BankIntegrated
|
|
|
|
) {
|
2022-09-05 18:12:30 +02:00
|
|
|
throw Error("wrong withdrawal record type");
|
2022-08-24 22:42:30 +02:00
|
|
|
}
|
|
|
|
const bankInfo = withdrawalGroup.wgInfo.bankInfo;
|
|
|
|
if (!bankInfo) {
|
2022-09-05 18:12:30 +02:00
|
|
|
return {
|
|
|
|
status: BankStatusResultCode.Done,
|
|
|
|
};
|
2022-08-09 15:00:45 +02:00
|
|
|
}
|
2022-08-24 22:42:30 +02:00
|
|
|
|
|
|
|
const bankStatusUrl = getBankStatusUrl(bankInfo.talerWithdrawUri);
|
2022-08-09 15:00:45 +02:00
|
|
|
|
|
|
|
const statusResp = await ws.http.get(bankStatusUrl, {
|
|
|
|
timeout: getReserveRequestTimeout(withdrawalGroup),
|
|
|
|
});
|
|
|
|
const status = await readSuccessResponseJsonOrThrow(
|
|
|
|
statusResp,
|
|
|
|
codecForWithdrawOperationStatusResponse(),
|
|
|
|
);
|
|
|
|
|
|
|
|
if (status.aborted) {
|
|
|
|
logger.info("bank aborted the withdrawal");
|
|
|
|
await ws.db
|
2022-09-13 13:25:41 +02:00
|
|
|
.mktx((x) => [x.withdrawalGroups])
|
2022-08-09 15:00:45 +02:00
|
|
|
.runReadWrite(async (tx) => {
|
|
|
|
const r = await tx.withdrawalGroups.get(withdrawalGroupId);
|
|
|
|
if (!r) {
|
|
|
|
return;
|
|
|
|
}
|
2022-09-21 20:46:45 +02:00
|
|
|
switch (r.status) {
|
|
|
|
case WithdrawalGroupStatus.RegisteringBank:
|
|
|
|
case WithdrawalGroupStatus.WaitConfirmBank:
|
2022-08-09 15:00:45 +02:00
|
|
|
break;
|
|
|
|
default:
|
|
|
|
return;
|
|
|
|
}
|
2022-08-24 22:42:30 +02:00
|
|
|
if (r.wgInfo.withdrawalType !== WithdrawalRecordType.BankIntegrated) {
|
2022-08-09 15:00:45 +02:00
|
|
|
throw Error("invariant failed");
|
|
|
|
}
|
|
|
|
const now = AbsoluteTime.toTimestamp(AbsoluteTime.now());
|
2022-08-24 22:42:30 +02:00
|
|
|
r.wgInfo.bankInfo.timestampBankConfirmed = now;
|
2022-09-21 20:46:45 +02:00
|
|
|
r.status = WithdrawalGroupStatus.BankAborted;
|
2022-08-09 15:00:45 +02:00
|
|
|
await tx.withdrawalGroups.put(r);
|
|
|
|
});
|
2022-09-05 18:12:30 +02:00
|
|
|
return {
|
|
|
|
status: BankStatusResultCode.Aborted,
|
|
|
|
};
|
2022-08-09 15:00:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Bank still needs to know our reserve info
|
|
|
|
if (!status.selection_done) {
|
|
|
|
await registerReserveWithBank(ws, withdrawalGroupId);
|
|
|
|
return await processReserveBankStatus(ws, withdrawalGroupId);
|
|
|
|
}
|
|
|
|
|
|
|
|
// FIXME: Why do we do this?!
|
2022-09-21 20:46:45 +02:00
|
|
|
if (withdrawalGroup.status === WithdrawalGroupStatus.RegisteringBank) {
|
2022-08-09 15:00:45 +02:00
|
|
|
await registerReserveWithBank(ws, withdrawalGroupId);
|
|
|
|
return await processReserveBankStatus(ws, withdrawalGroupId);
|
|
|
|
}
|
|
|
|
|
|
|
|
await ws.db
|
2022-09-13 13:25:41 +02:00
|
|
|
.mktx((x) => [x.withdrawalGroups])
|
2022-08-09 15:00:45 +02:00
|
|
|
.runReadWrite(async (tx) => {
|
|
|
|
const r = await tx.withdrawalGroups.get(withdrawalGroupId);
|
|
|
|
if (!r) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// Re-check reserve status within transaction
|
2022-09-21 20:46:45 +02:00
|
|
|
switch (r.status) {
|
|
|
|
case WithdrawalGroupStatus.RegisteringBank:
|
|
|
|
case WithdrawalGroupStatus.WaitConfirmBank:
|
2022-08-09 15:00:45 +02:00
|
|
|
break;
|
|
|
|
default:
|
|
|
|
return;
|
|
|
|
}
|
2022-08-24 22:42:30 +02:00
|
|
|
if (r.wgInfo.withdrawalType !== WithdrawalRecordType.BankIntegrated) {
|
|
|
|
throw Error("invariant failed");
|
|
|
|
}
|
2022-08-09 15:00:45 +02:00
|
|
|
if (status.transfer_done) {
|
|
|
|
logger.info("withdrawal: transfer confirmed by bank.");
|
|
|
|
const now = AbsoluteTime.toTimestamp(AbsoluteTime.now());
|
2022-08-24 22:42:30 +02:00
|
|
|
r.wgInfo.bankInfo.timestampBankConfirmed = now;
|
2022-09-21 20:46:45 +02:00
|
|
|
r.status = WithdrawalGroupStatus.QueryingStatus;
|
2023-02-13 13:15:47 +01:00
|
|
|
ws.notify({
|
|
|
|
type: NotificationType.WithdrawalGroupBankConfirmed,
|
|
|
|
transactionId: makeTransactionId(
|
|
|
|
TransactionType.Withdrawal,
|
|
|
|
r.withdrawalGroupId,
|
|
|
|
),
|
|
|
|
});
|
2022-08-09 15:00:45 +02:00
|
|
|
} else {
|
|
|
|
logger.info("withdrawal: transfer not yet confirmed by bank");
|
2022-08-24 22:42:30 +02:00
|
|
|
r.wgInfo.bankInfo.confirmUrl = status.confirm_transfer_url;
|
2022-08-29 18:23:22 +02:00
|
|
|
r.senderWire = status.sender_wire;
|
2022-08-09 15:00:45 +02:00
|
|
|
}
|
|
|
|
await tx.withdrawalGroups.put(r);
|
|
|
|
});
|
2022-09-05 18:12:30 +02:00
|
|
|
|
2022-09-30 13:22:00 +02:00
|
|
|
if (status.transfer_done) {
|
|
|
|
return {
|
|
|
|
status: BankStatusResultCode.Done,
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
return {
|
|
|
|
status: BankStatusResultCode.Waiting,
|
|
|
|
};
|
|
|
|
}
|
2022-08-09 15:00:45 +02:00
|
|
|
}
|
|
|
|
|
2023-02-20 21:26:08 +01:00
|
|
|
/**
|
|
|
|
* Create a withdrawal group.
|
|
|
|
*
|
|
|
|
* If a forcedWithdrawalGroupId is given and a
|
|
|
|
* withdrawal group with this ID already exists,
|
|
|
|
* the existing one is returned. No conflict checking
|
|
|
|
* of the other arguments is done in that case.
|
|
|
|
*/
|
2022-08-09 15:00:45 +02:00
|
|
|
export async function internalCreateWithdrawalGroup(
|
|
|
|
ws: InternalWalletState,
|
|
|
|
args: {
|
2022-09-21 20:46:45 +02:00
|
|
|
reserveStatus: WithdrawalGroupStatus;
|
2022-08-09 15:00:45 +02:00
|
|
|
amount: AmountJson;
|
|
|
|
exchangeBaseUrl: string;
|
2023-02-20 00:36:02 +01:00
|
|
|
forcedWithdrawalGroupId?: string;
|
2022-08-09 15:00:45 +02:00
|
|
|
forcedDenomSel?: ForcedDenomSel;
|
|
|
|
reserveKeyPair?: EddsaKeypair;
|
|
|
|
restrictAge?: number;
|
2022-08-24 22:42:30 +02:00
|
|
|
wgInfo: WgInfo;
|
2022-08-09 15:00:45 +02:00
|
|
|
},
|
|
|
|
): Promise<WithdrawalGroupRecord> {
|
|
|
|
const reserveKeyPair =
|
|
|
|
args.reserveKeyPair ?? (await ws.cryptoApi.createEddsaKeypair({}));
|
|
|
|
const now = AbsoluteTime.toTimestamp(AbsoluteTime.now());
|
|
|
|
const secretSeed = encodeCrock(getRandomBytes(32));
|
|
|
|
const canonExchange = canonicalizeBaseUrl(args.exchangeBaseUrl);
|
|
|
|
const amount = args.amount;
|
|
|
|
|
2023-02-20 00:36:02 +01:00
|
|
|
let withdrawalGroupId;
|
|
|
|
|
|
|
|
if (args.forcedWithdrawalGroupId) {
|
|
|
|
withdrawalGroupId = args.forcedWithdrawalGroupId;
|
2023-02-20 21:26:08 +01:00
|
|
|
const wgId = withdrawalGroupId;
|
|
|
|
const existingWg = await ws.db
|
|
|
|
.mktx((x) => [x.withdrawalGroups])
|
|
|
|
.runReadOnly(async (tx) => {
|
|
|
|
return tx.withdrawalGroups.get(wgId);
|
|
|
|
});
|
|
|
|
if (existingWg) {
|
|
|
|
return existingWg;
|
|
|
|
}
|
2023-02-20 00:36:02 +01:00
|
|
|
} else {
|
|
|
|
withdrawalGroupId = encodeCrock(getRandomBytes(32));
|
|
|
|
}
|
|
|
|
|
2022-08-09 15:00:45 +02:00
|
|
|
await updateWithdrawalDenoms(ws, canonExchange);
|
|
|
|
const denoms = await getCandidateWithdrawalDenoms(ws, canonExchange);
|
|
|
|
|
|
|
|
let initialDenomSel: DenomSelectionState;
|
|
|
|
const denomSelUid = encodeCrock(getRandomBytes(16));
|
|
|
|
if (args.forcedDenomSel) {
|
|
|
|
logger.warn("using forced denom selection");
|
|
|
|
initialDenomSel = selectForcedWithdrawalDenominations(
|
|
|
|
amount,
|
|
|
|
denoms,
|
|
|
|
args.forcedDenomSel,
|
2023-04-19 17:42:47 +02:00
|
|
|
ws.config.testing.denomselAllowLate,
|
2022-08-09 15:00:45 +02:00
|
|
|
);
|
|
|
|
} else {
|
2023-04-19 17:42:47 +02:00
|
|
|
initialDenomSel = selectWithdrawalDenominations(
|
|
|
|
amount,
|
|
|
|
denoms,
|
|
|
|
ws.config.testing.denomselAllowLate,
|
|
|
|
);
|
2022-08-09 15:00:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
const withdrawalGroup: WithdrawalGroupRecord = {
|
|
|
|
denomSelUid,
|
|
|
|
denomsSel: initialDenomSel,
|
|
|
|
exchangeBaseUrl: canonExchange,
|
2022-11-02 17:42:14 +01:00
|
|
|
instructedAmount: Amounts.stringify(amount),
|
2022-08-09 15:00:45 +02:00
|
|
|
timestampStart: now,
|
|
|
|
rawWithdrawalAmount: initialDenomSel.totalWithdrawCost,
|
2022-10-16 22:18:24 +02:00
|
|
|
effectiveWithdrawalAmount: initialDenomSel.totalCoinValue,
|
2022-08-09 15:00:45 +02:00
|
|
|
secretSeed,
|
|
|
|
reservePriv: reserveKeyPair.priv,
|
|
|
|
reservePub: reserveKeyPair.pub,
|
2022-09-21 20:46:45 +02:00
|
|
|
status: args.reserveStatus,
|
2022-08-09 15:00:45 +02:00
|
|
|
withdrawalGroupId,
|
|
|
|
restrictAge: args.restrictAge,
|
|
|
|
senderWire: undefined,
|
|
|
|
timestampFinish: undefined,
|
2022-08-24 22:42:30 +02:00
|
|
|
wgInfo: args.wgInfo,
|
2022-08-09 15:00:45 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
const exchangeInfo = await updateExchangeFromUrl(ws, canonExchange);
|
|
|
|
const exchangeDetails = exchangeInfo.exchangeDetails;
|
|
|
|
if (!exchangeDetails) {
|
|
|
|
logger.trace(exchangeDetails);
|
|
|
|
throw Error("exchange not updated");
|
|
|
|
}
|
|
|
|
const { isAudited, isTrusted } = await getExchangeTrust(
|
|
|
|
ws,
|
|
|
|
exchangeInfo.exchange,
|
|
|
|
);
|
|
|
|
|
|
|
|
await ws.db
|
2022-09-13 13:25:41 +02:00
|
|
|
.mktx((x) => [
|
|
|
|
x.withdrawalGroups,
|
|
|
|
x.reserves,
|
|
|
|
x.exchanges,
|
|
|
|
x.exchangeDetails,
|
|
|
|
x.exchangeTrust,
|
|
|
|
])
|
2022-08-09 15:00:45 +02:00
|
|
|
.runReadWrite(async (tx) => {
|
|
|
|
await tx.withdrawalGroups.add(withdrawalGroup);
|
2022-08-26 01:18:01 +02:00
|
|
|
await tx.reserves.put({
|
|
|
|
reservePub: withdrawalGroup.reservePub,
|
|
|
|
reservePriv: withdrawalGroup.reservePriv,
|
|
|
|
});
|
2022-08-09 15:00:45 +02:00
|
|
|
|
2023-02-19 23:13:44 +01:00
|
|
|
const exchange = await tx.exchanges.get(withdrawalGroup.exchangeBaseUrl);
|
|
|
|
if (exchange) {
|
|
|
|
exchange.lastWithdrawal = TalerProtocolTimestamp.now();
|
|
|
|
await tx.exchanges.put(exchange);
|
|
|
|
}
|
|
|
|
|
2022-08-09 15:00:45 +02:00
|
|
|
if (!isAudited && !isTrusted) {
|
|
|
|
await tx.exchangeTrust.put({
|
|
|
|
currency: amount.currency,
|
|
|
|
exchangeBaseUrl: canonExchange,
|
|
|
|
exchangeMasterPub: exchangeDetails.masterPublicKey,
|
|
|
|
uids: [encodeCrock(getRandomBytes(32))],
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
return withdrawalGroup;
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function acceptWithdrawalFromUri(
|
|
|
|
ws: InternalWalletState,
|
|
|
|
req: {
|
|
|
|
talerWithdrawUri: string;
|
|
|
|
selectedExchange: string;
|
|
|
|
forcedDenomSel?: ForcedDenomSel;
|
|
|
|
restrictAge?: number;
|
|
|
|
},
|
|
|
|
): Promise<AcceptWithdrawalResponse> {
|
2022-10-05 11:11:51 +02:00
|
|
|
const selectedExchange = canonicalizeBaseUrl(req.selectedExchange);
|
|
|
|
logger.info(
|
|
|
|
`accepting withdrawal via ${req.talerWithdrawUri}, canonicalized selected exchange ${selectedExchange}`,
|
|
|
|
);
|
2022-08-24 19:44:24 +02:00
|
|
|
const existingWithdrawalGroup = await ws.db
|
2022-09-13 13:25:41 +02:00
|
|
|
.mktx((x) => [x.withdrawalGroups])
|
2022-08-24 19:44:24 +02:00
|
|
|
.runReadOnly(async (tx) => {
|
|
|
|
return await tx.withdrawalGroups.indexes.byTalerWithdrawUri.get(
|
|
|
|
req.talerWithdrawUri,
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
if (existingWithdrawalGroup) {
|
2022-08-24 22:42:30 +02:00
|
|
|
let url: string | undefined;
|
|
|
|
if (
|
|
|
|
existingWithdrawalGroup.wgInfo.withdrawalType ===
|
|
|
|
WithdrawalRecordType.BankIntegrated
|
|
|
|
) {
|
|
|
|
url = existingWithdrawalGroup.wgInfo.bankInfo.confirmUrl;
|
|
|
|
}
|
2022-08-24 19:44:24 +02:00
|
|
|
return {
|
|
|
|
reservePub: existingWithdrawalGroup.reservePub,
|
2022-08-24 22:42:30 +02:00
|
|
|
confirmTransferUrl: url,
|
2022-10-14 22:47:11 +02:00
|
|
|
transactionId: makeTransactionId(
|
2022-09-16 16:06:55 +02:00
|
|
|
TransactionType.Withdrawal,
|
|
|
|
existingWithdrawalGroup.withdrawalGroupId,
|
2022-09-16 19:27:24 +02:00
|
|
|
),
|
2022-08-24 19:44:24 +02:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2022-10-05 11:11:51 +02:00
|
|
|
await updateExchangeFromUrl(ws, selectedExchange);
|
2022-08-09 15:00:45 +02:00
|
|
|
const withdrawInfo = await getBankWithdrawalInfo(
|
|
|
|
ws.http,
|
|
|
|
req.talerWithdrawUri,
|
|
|
|
);
|
|
|
|
const exchangePaytoUri = await getExchangePaytoUri(
|
|
|
|
ws,
|
2022-10-05 11:11:51 +02:00
|
|
|
selectedExchange,
|
2022-08-09 15:00:45 +02:00
|
|
|
withdrawInfo.wireTypes,
|
|
|
|
);
|
|
|
|
|
|
|
|
const withdrawalGroup = await internalCreateWithdrawalGroup(ws, {
|
|
|
|
amount: withdrawInfo.amount,
|
|
|
|
exchangeBaseUrl: req.selectedExchange,
|
2022-08-24 22:42:30 +02:00
|
|
|
wgInfo: {
|
|
|
|
withdrawalType: WithdrawalRecordType.BankIntegrated,
|
|
|
|
bankInfo: {
|
|
|
|
exchangePaytoUri,
|
|
|
|
talerWithdrawUri: req.talerWithdrawUri,
|
|
|
|
confirmUrl: withdrawInfo.confirmTransferUrl,
|
2022-09-21 21:13:31 +02:00
|
|
|
timestampBankConfirmed: undefined,
|
|
|
|
timestampReserveInfoPosted: undefined,
|
2022-08-24 22:42:30 +02:00
|
|
|
},
|
|
|
|
},
|
2022-09-01 22:26:22 +02:00
|
|
|
restrictAge: req.restrictAge,
|
2022-08-09 15:00:45 +02:00
|
|
|
forcedDenomSel: req.forcedDenomSel,
|
2022-09-21 20:46:45 +02:00
|
|
|
reserveStatus: WithdrawalGroupStatus.RegisteringBank,
|
2022-08-09 15:00:45 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
const withdrawalGroupId = withdrawalGroup.withdrawalGroupId;
|
|
|
|
|
|
|
|
// We do this here, as the reserve should be registered before we return,
|
|
|
|
// so that we can redirect the user to the bank's status page.
|
|
|
|
await processReserveBankStatus(ws, withdrawalGroupId);
|
|
|
|
const processedWithdrawalGroup = await getWithdrawalGroupRecordTx(ws.db, {
|
|
|
|
withdrawalGroupId,
|
|
|
|
});
|
2022-09-21 20:46:45 +02:00
|
|
|
if (processedWithdrawalGroup?.status === WithdrawalGroupStatus.BankAborted) {
|
2022-08-09 15:00:45 +02:00
|
|
|
throw TalerError.fromDetail(
|
|
|
|
TalerErrorCode.WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK,
|
|
|
|
{},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-10-05 18:31:56 +02:00
|
|
|
// Start withdrawal in the background
|
|
|
|
processWithdrawalGroup(ws, withdrawalGroupId, {
|
|
|
|
forceNow: true,
|
|
|
|
}).catch((err) => {
|
|
|
|
logger.error("Processing withdrawal (after creation) failed:", err);
|
|
|
|
});
|
2022-08-09 15:00:45 +02:00
|
|
|
|
|
|
|
return {
|
|
|
|
reservePub: withdrawalGroup.reservePub,
|
|
|
|
confirmTransferUrl: withdrawInfo.confirmTransferUrl,
|
2022-10-14 23:01:41 +02:00
|
|
|
transactionId: makeTransactionId(
|
|
|
|
TransactionType.Withdrawal,
|
|
|
|
withdrawalGroupId,
|
|
|
|
),
|
2022-08-09 15:00:45 +02:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create a manual withdrawal operation.
|
|
|
|
*
|
|
|
|
* Adds the corresponding exchange as a trusted exchange if it is neither
|
|
|
|
* audited nor trusted already.
|
|
|
|
*
|
|
|
|
* Asynchronously starts the withdrawal.
|
|
|
|
*/
|
|
|
|
export async function createManualWithdrawal(
|
|
|
|
ws: InternalWalletState,
|
|
|
|
req: {
|
|
|
|
exchangeBaseUrl: string;
|
|
|
|
amount: AmountLike;
|
|
|
|
restrictAge?: number;
|
|
|
|
forcedDenomSel?: ForcedDenomSel;
|
|
|
|
},
|
|
|
|
): Promise<AcceptManualWithdrawalResult> {
|
|
|
|
const withdrawalGroup = await internalCreateWithdrawalGroup(ws, {
|
|
|
|
amount: Amounts.jsonifyAmount(req.amount),
|
2022-08-24 22:42:30 +02:00
|
|
|
wgInfo: {
|
|
|
|
withdrawalType: WithdrawalRecordType.BankManual,
|
|
|
|
},
|
2022-08-09 15:00:45 +02:00
|
|
|
exchangeBaseUrl: req.exchangeBaseUrl,
|
|
|
|
forcedDenomSel: req.forcedDenomSel,
|
|
|
|
restrictAge: req.restrictAge,
|
2022-09-21 20:46:45 +02:00
|
|
|
reserveStatus: WithdrawalGroupStatus.QueryingStatus,
|
2022-08-09 15:00:45 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
const withdrawalGroupId = withdrawalGroup.withdrawalGroupId;
|
|
|
|
|
|
|
|
const exchangePaytoUris = await ws.db
|
2022-09-13 13:25:41 +02:00
|
|
|
.mktx((x) => [
|
|
|
|
x.withdrawalGroups,
|
|
|
|
x.exchanges,
|
|
|
|
x.exchangeDetails,
|
|
|
|
x.exchangeTrust,
|
|
|
|
])
|
2023-02-10 19:47:59 +01:00
|
|
|
.runReadOnly(async (tx) => {
|
2022-08-09 15:00:45 +02:00
|
|
|
return await getFundingPaytoUris(tx, withdrawalGroup.withdrawalGroupId);
|
|
|
|
});
|
|
|
|
|
2022-09-23 18:56:21 +02:00
|
|
|
// Start withdrawal in the background (do not await!)
|
|
|
|
// FIXME: We could also interrupt the task look if it is waiting and
|
|
|
|
// rely on retry handling to re-process the withdrawal group.
|
|
|
|
runOperationWithErrorReporting(
|
|
|
|
ws,
|
2023-02-20 20:14:37 +01:00
|
|
|
TaskIdentifiers.forWithdrawal(withdrawalGroup),
|
2022-09-23 18:56:21 +02:00
|
|
|
async () => {
|
|
|
|
return await processWithdrawalGroup(ws, withdrawalGroupId, {
|
|
|
|
forceNow: true,
|
|
|
|
});
|
2022-08-09 15:00:45 +02:00
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
return {
|
|
|
|
reservePub: withdrawalGroup.reservePub,
|
|
|
|
exchangePaytoUris: exchangePaytoUris,
|
2022-10-14 23:01:41 +02:00
|
|
|
transactionId: makeTransactionId(
|
|
|
|
TransactionType.Withdrawal,
|
|
|
|
withdrawalGroupId,
|
|
|
|
),
|
2022-08-09 15:00:45 +02:00
|
|
|
};
|
|
|
|
}
|