2019-12-02 00:42:40 +01:00
|
|
|
/*
|
|
|
|
This file is part of GNU Taler
|
|
|
|
(C) 2019 GNUnet e.V.
|
|
|
|
|
|
|
|
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/>
|
|
|
|
*/
|
|
|
|
|
|
|
|
import {
|
2021-12-13 11:28:15 +01:00
|
|
|
AcceptWithdrawalResponse,
|
|
|
|
addPaytoQueryParams,
|
|
|
|
Amounts,
|
|
|
|
canonicalizeBaseUrl,
|
|
|
|
codecForBankWithdrawalOperationPostResponse,
|
2021-03-17 17:56:37 +01:00
|
|
|
codecForReserveStatus,
|
2021-12-13 11:28:15 +01:00
|
|
|
codecForWithdrawOperationStatusResponse,
|
|
|
|
CreateReserveRequest,
|
|
|
|
CreateReserveResponse,
|
|
|
|
Duration,
|
2020-08-20 12:57:20 +02:00
|
|
|
durationMax,
|
2021-12-13 11:28:15 +01:00
|
|
|
durationMin,
|
|
|
|
encodeCrock,
|
|
|
|
getRandomBytes,
|
2022-01-11 12:48:32 +01:00
|
|
|
j2s,
|
2021-12-13 11:28:15 +01:00
|
|
|
Logger,
|
|
|
|
NotificationType,
|
|
|
|
randomBytes,
|
|
|
|
TalerErrorCode,
|
2022-03-22 21:16:38 +01:00
|
|
|
TalerErrorDetail,
|
2022-03-18 15:32:41 +01:00
|
|
|
AbsoluteTime,
|
2021-12-13 11:28:15 +01:00
|
|
|
URL,
|
2022-03-29 21:21:57 +02:00
|
|
|
AmountString,
|
|
|
|
ForcedDenomSel,
|
2021-03-17 17:56:37 +01:00
|
|
|
} from "@gnu-taler/taler-util";
|
2022-03-23 13:11:36 +01:00
|
|
|
import { InternalWalletState } from "../internal-wallet-state.js";
|
2021-04-07 19:29:51 +02:00
|
|
|
import {
|
2022-01-11 21:00:12 +01:00
|
|
|
OperationStatus,
|
2021-04-07 19:29:51 +02:00
|
|
|
ReserveBankInfo,
|
2021-12-13 11:28:15 +01:00
|
|
|
ReserveRecord,
|
|
|
|
ReserveRecordStatus,
|
|
|
|
WalletStoresV1,
|
|
|
|
WithdrawalGroupRecord,
|
2021-04-07 19:29:51 +02:00
|
|
|
} from "../db.js";
|
2022-03-23 13:11:36 +01:00
|
|
|
import { TalerError } from "../errors.js";
|
2021-03-17 17:56:37 +01:00
|
|
|
import { assertUnreachable } from "../util/assertUnreachable.js";
|
2021-04-07 19:29:51 +02:00
|
|
|
import {
|
2021-11-19 18:46:32 +01:00
|
|
|
readSuccessResponseJsonOrErrorCode,
|
|
|
|
readSuccessResponseJsonOrThrow,
|
2021-12-13 11:28:15 +01:00
|
|
|
throwUnexpectedRequestError,
|
2021-11-19 18:46:32 +01:00
|
|
|
} from "../util/http.js";
|
|
|
|
import { GetReadOnlyAccess } from "../util/query.js";
|
|
|
|
import {
|
2021-12-13 11:28:15 +01:00
|
|
|
getRetryDuration,
|
2022-03-29 13:47:32 +02:00
|
|
|
resetRetryInfo,
|
|
|
|
RetryInfo,
|
2021-04-07 19:29:51 +02:00
|
|
|
} from "../util/retries.js";
|
2021-06-02 13:23:51 +02:00
|
|
|
import {
|
2021-12-13 11:28:15 +01:00
|
|
|
getExchangeDetails,
|
|
|
|
getExchangePaytoUri,
|
|
|
|
getExchangeTrust,
|
|
|
|
updateExchangeFromUrl,
|
2021-06-02 13:23:51 +02:00
|
|
|
} from "./exchanges.js";
|
2021-04-07 19:29:51 +02:00
|
|
|
import {
|
2021-12-13 11:28:15 +01:00
|
|
|
getBankWithdrawalInfo,
|
|
|
|
getCandidateWithdrawalDenoms,
|
|
|
|
processWithdrawGroup,
|
|
|
|
selectWithdrawalDenominations,
|
|
|
|
updateWithdrawalDenoms,
|
2021-04-07 19:29:51 +02:00
|
|
|
} from "./withdraw.js";
|
2022-03-23 13:11:36 +01:00
|
|
|
import { guardOperationException } from "./common.js";
|
2019-12-02 00:42:40 +01:00
|
|
|
|
2022-03-08 20:39:52 +01:00
|
|
|
const logger = new Logger("taler-wallet-core:reserves.ts");
|
2019-12-02 00:42:40 +01:00
|
|
|
|
2022-03-08 20:39:52 +01:00
|
|
|
/**
|
2022-03-29 13:47:32 +02:00
|
|
|
* Set up the reserve's retry timeout in preparation for
|
|
|
|
* processing the reserve.
|
2022-03-08 20:39:52 +01:00
|
|
|
*/
|
2022-03-29 13:47:32 +02:00
|
|
|
async function setupReserveRetry(
|
2022-03-08 20:39:52 +01:00
|
|
|
ws: InternalWalletState,
|
|
|
|
reservePub: string,
|
2022-03-29 13:47:32 +02:00
|
|
|
options: {
|
|
|
|
reset: boolean;
|
|
|
|
},
|
2022-03-08 20:39:52 +01:00
|
|
|
): Promise<void> {
|
|
|
|
await ws.db
|
|
|
|
.mktx((x) => ({
|
|
|
|
reserves: x.reserves,
|
|
|
|
}))
|
|
|
|
.runReadWrite(async (tx) => {
|
|
|
|
const r = await tx.reserves.get(reservePub);
|
|
|
|
if (!r) {
|
|
|
|
return;
|
|
|
|
}
|
2022-03-29 13:47:32 +02:00
|
|
|
if (options.reset) {
|
|
|
|
r.retryInfo = resetRetryInfo();
|
2022-03-08 20:39:52 +01:00
|
|
|
} else {
|
2022-03-29 13:47:32 +02:00
|
|
|
r.retryInfo = RetryInfo.increment(r.retryInfo);
|
2022-03-08 20:39:52 +01:00
|
|
|
}
|
|
|
|
delete r.lastError;
|
|
|
|
await tx.reserves.put(r);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Report an error that happened while processing the reserve.
|
|
|
|
*
|
|
|
|
* Logs the error via a notification and by storing it in the database.
|
|
|
|
*/
|
|
|
|
async function reportReserveError(
|
|
|
|
ws: InternalWalletState,
|
|
|
|
reservePub: string,
|
2022-03-22 21:16:38 +01:00
|
|
|
err: TalerErrorDetail,
|
2022-03-08 20:39:52 +01:00
|
|
|
): Promise<void> {
|
|
|
|
await ws.db
|
|
|
|
.mktx((x) => ({
|
|
|
|
reserves: x.reserves,
|
|
|
|
}))
|
|
|
|
.runReadWrite(async (tx) => {
|
|
|
|
const r = await tx.reserves.get(reservePub);
|
|
|
|
if (!r) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (!r.retryInfo) {
|
|
|
|
logger.error(`got reserve error for inactive reserve (no retryInfo)`);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
r.lastError = err;
|
|
|
|
await tx.reserves.put(r);
|
|
|
|
});
|
|
|
|
ws.notify({
|
|
|
|
type: NotificationType.ReserveOperationError,
|
|
|
|
error: err,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2019-12-02 00:42:40 +01:00
|
|
|
/**
|
|
|
|
* Create a reserve, but do not flag it as confirmed yet.
|
|
|
|
*
|
|
|
|
* Adds the corresponding exchange as a trusted exchange if it is neither
|
|
|
|
* audited nor trusted already.
|
|
|
|
*/
|
|
|
|
export async function createReserve(
|
|
|
|
ws: InternalWalletState,
|
|
|
|
req: CreateReserveRequest,
|
|
|
|
): Promise<CreateReserveResponse> {
|
2022-03-23 21:24:23 +01:00
|
|
|
const keypair = await ws.cryptoApi.createEddsaKeypair({});
|
2022-03-18 15:32:41 +01:00
|
|
|
const now = AbsoluteTime.toTimestamp(AbsoluteTime.now());
|
2019-12-02 00:42:40 +01:00
|
|
|
const canonExchange = canonicalizeBaseUrl(req.exchange);
|
|
|
|
|
|
|
|
let reserveStatus;
|
|
|
|
if (req.bankWithdrawStatusUrl) {
|
2022-03-08 20:39:52 +01:00
|
|
|
reserveStatus = ReserveRecordStatus.RegisteringBank;
|
2019-12-02 00:42:40 +01:00
|
|
|
} else {
|
2022-03-08 20:39:52 +01:00
|
|
|
reserveStatus = ReserveRecordStatus.QueryingStatus;
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
|
|
|
|
2020-05-15 12:33:52 +02:00
|
|
|
let bankInfo: ReserveBankInfo | undefined;
|
|
|
|
|
|
|
|
if (req.bankWithdrawStatusUrl) {
|
2020-07-16 19:22:56 +02:00
|
|
|
if (!req.exchangePaytoUri) {
|
2020-07-22 10:52:03 +02:00
|
|
|
throw Error(
|
|
|
|
"Exchange payto URI must be specified for a bank-integrated withdrawal",
|
|
|
|
);
|
2020-07-16 19:22:56 +02:00
|
|
|
}
|
2020-05-15 12:33:52 +02:00
|
|
|
bankInfo = {
|
|
|
|
statusUrl: req.bankWithdrawStatusUrl,
|
2020-07-16 19:22:56 +02:00
|
|
|
exchangePaytoUri: req.exchangePaytoUri,
|
2020-05-15 12:33:52 +02:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2020-07-16 11:14:59 +02:00
|
|
|
const initialWithdrawalGroupId = encodeCrock(getRandomBytes(32));
|
|
|
|
|
2020-12-16 17:59:04 +01:00
|
|
|
await updateWithdrawalDenoms(ws, canonExchange);
|
2021-01-14 18:00:00 +01:00
|
|
|
const denoms = await getCandidateWithdrawalDenoms(ws, canonExchange);
|
2022-03-29 21:21:57 +02:00
|
|
|
const initialDenomSel = selectWithdrawalDenominations(req.amount, denoms);
|
2020-07-16 11:14:59 +02:00
|
|
|
|
2019-12-02 00:42:40 +01:00
|
|
|
const reserveRecord: ReserveRecord = {
|
2020-07-16 11:14:59 +02:00
|
|
|
instructedAmount: req.amount,
|
|
|
|
initialWithdrawalGroupId,
|
|
|
|
initialDenomSel,
|
|
|
|
initialWithdrawalStarted: false,
|
2019-12-16 16:20:45 +01:00
|
|
|
timestampCreated: now,
|
2019-12-02 00:42:40 +01:00
|
|
|
exchangeBaseUrl: canonExchange,
|
|
|
|
reservePriv: keypair.priv,
|
|
|
|
reservePub: keypair.pub,
|
|
|
|
senderWire: req.senderWire,
|
2020-07-16 19:22:56 +02:00
|
|
|
timestampBankConfirmed: undefined,
|
2019-12-02 00:42:40 +01:00
|
|
|
timestampReserveInfoPosted: undefined,
|
2020-05-15 12:33:52 +02:00
|
|
|
bankInfo,
|
2019-12-02 00:42:40 +01:00
|
|
|
reserveStatus,
|
2022-03-29 13:47:32 +02:00
|
|
|
retryInfo: resetRetryInfo(),
|
2019-12-05 19:38:19 +01:00
|
|
|
lastError: undefined,
|
2020-04-02 17:03:01 +02:00
|
|
|
currency: req.amount.currency,
|
2022-01-11 21:00:12 +01:00
|
|
|
operationStatus: OperationStatus.Pending,
|
2019-12-02 00:42:40 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
const exchangeInfo = await updateExchangeFromUrl(ws, req.exchange);
|
2021-06-02 13:23:51 +02:00
|
|
|
const exchangeDetails = exchangeInfo.exchangeDetails;
|
2019-12-02 00:42:40 +01:00
|
|
|
if (!exchangeDetails) {
|
2020-08-14 12:23:50 +02:00
|
|
|
logger.trace(exchangeDetails);
|
2019-12-02 00:42:40 +01:00
|
|
|
throw Error("exchange not updated");
|
|
|
|
}
|
2021-06-02 13:23:51 +02:00
|
|
|
const { isAudited, isTrusted } = await getExchangeTrust(
|
|
|
|
ws,
|
|
|
|
exchangeInfo.exchange,
|
|
|
|
);
|
2019-12-02 00:42:40 +01:00
|
|
|
|
2021-06-09 15:14:17 +02:00
|
|
|
const resp = await ws.db
|
|
|
|
.mktx((x) => ({
|
|
|
|
exchangeTrust: x.exchangeTrust,
|
|
|
|
reserves: x.reserves,
|
|
|
|
bankWithdrawUris: x.bankWithdrawUris,
|
|
|
|
}))
|
|
|
|
.runReadWrite(async (tx) => {
|
2019-12-02 00:42:40 +01:00
|
|
|
// Check if we have already created a reserve for that bankWithdrawStatusUrl
|
2020-05-12 10:38:58 +02:00
|
|
|
if (reserveRecord.bankInfo?.statusUrl) {
|
2021-06-09 15:14:17 +02:00
|
|
|
const bwi = await tx.bankWithdrawUris.get(
|
2020-05-12 10:38:58 +02:00
|
|
|
reserveRecord.bankInfo.statusUrl,
|
2019-12-02 00:42:40 +01:00
|
|
|
);
|
|
|
|
if (bwi) {
|
2021-06-09 15:14:17 +02:00
|
|
|
const otherReserve = await tx.reserves.get(bwi.reservePub);
|
2019-12-02 00:42:40 +01:00
|
|
|
if (otherReserve) {
|
|
|
|
logger.trace(
|
|
|
|
"returning existing reserve for bankWithdrawStatusUri",
|
|
|
|
);
|
|
|
|
return {
|
|
|
|
exchange: otherReserve.exchangeBaseUrl,
|
|
|
|
reservePub: otherReserve.reservePub,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
2021-06-09 15:14:17 +02:00
|
|
|
await tx.bankWithdrawUris.put({
|
2019-12-02 00:42:40 +01:00
|
|
|
reservePub: reserveRecord.reservePub,
|
2020-05-12 10:38:58 +02:00
|
|
|
talerWithdrawUri: reserveRecord.bankInfo.statusUrl,
|
2019-12-02 00:42:40 +01:00
|
|
|
});
|
|
|
|
}
|
2021-05-20 13:17:04 +02:00
|
|
|
if (!isAudited && !isTrusted) {
|
2021-06-09 15:14:17 +02:00
|
|
|
await tx.exchangeTrust.put({
|
2021-05-20 13:14:47 +02:00
|
|
|
currency: reserveRecord.currency,
|
|
|
|
exchangeBaseUrl: reserveRecord.exchangeBaseUrl,
|
|
|
|
exchangeMasterPub: exchangeDetails.masterPublicKey,
|
|
|
|
uids: [encodeCrock(getRandomBytes(32))],
|
|
|
|
});
|
|
|
|
}
|
2021-06-09 15:14:17 +02:00
|
|
|
await tx.reserves.put(reserveRecord);
|
2019-12-02 00:42:40 +01:00
|
|
|
const r: CreateReserveResponse = {
|
|
|
|
exchange: canonExchange,
|
|
|
|
reservePub: keypair.pub,
|
|
|
|
};
|
|
|
|
return r;
|
2021-06-09 15:14:17 +02:00
|
|
|
});
|
2019-12-02 00:42:40 +01:00
|
|
|
|
2020-07-23 20:52:46 +02:00
|
|
|
if (reserveRecord.reservePub === resp.reservePub) {
|
|
|
|
// Only emit notification when a new reserve was created.
|
|
|
|
ws.notify({
|
|
|
|
type: NotificationType.ReserveCreated,
|
|
|
|
reservePub: reserveRecord.reservePub,
|
|
|
|
});
|
|
|
|
}
|
2019-12-06 03:23:35 +01:00
|
|
|
|
2019-12-02 00:42:40 +01:00
|
|
|
// Asynchronously process the reserve, but return
|
|
|
|
// to the caller already.
|
2022-03-29 13:47:32 +02:00
|
|
|
processReserve(ws, resp.reservePub, { forceNow: true }).catch((e) => {
|
2020-07-23 20:52:46 +02:00
|
|
|
logger.error("Processing reserve (after createReserve) failed:", e);
|
2019-12-02 00:42:40 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
return resp;
|
|
|
|
}
|
|
|
|
|
2020-03-12 14:55:38 +01:00
|
|
|
/**
|
|
|
|
* Re-query the status of a reserve.
|
|
|
|
*/
|
|
|
|
export async function forceQueryReserve(
|
|
|
|
ws: InternalWalletState,
|
|
|
|
reservePub: string,
|
|
|
|
): Promise<void> {
|
2021-06-09 15:14:17 +02:00
|
|
|
await ws.db
|
|
|
|
.mktx((x) => ({
|
|
|
|
reserves: x.reserves,
|
|
|
|
}))
|
|
|
|
.runReadWrite(async (tx) => {
|
|
|
|
const reserve = await tx.reserves.get(reservePub);
|
|
|
|
if (!reserve) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// Only force status query where it makes sense
|
|
|
|
switch (reserve.reserveStatus) {
|
2022-03-08 20:39:52 +01:00
|
|
|
case ReserveRecordStatus.Dormant:
|
|
|
|
reserve.reserveStatus = ReserveRecordStatus.QueryingStatus;
|
2022-01-11 21:00:12 +01:00
|
|
|
reserve.operationStatus = OperationStatus.Pending;
|
2022-03-29 13:47:32 +02:00
|
|
|
reserve.retryInfo = resetRetryInfo();
|
2021-06-09 15:14:17 +02:00
|
|
|
break;
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
await tx.reserves.put(reserve);
|
|
|
|
});
|
2022-03-29 13:47:32 +02:00
|
|
|
await processReserve(ws, reservePub, { forceNow: true });
|
2020-03-12 14:55:38 +01:00
|
|
|
}
|
|
|
|
|
2019-12-02 00:42:40 +01:00
|
|
|
/**
|
2021-04-27 23:42:25 +02:00
|
|
|
* First fetch information required to withdraw from the reserve,
|
2019-12-02 00:42:40 +01:00
|
|
|
* then deplete the reserve, withdrawing coins until it is empty.
|
|
|
|
*
|
|
|
|
* The returned promise resolves once the reserve is set to the
|
2022-03-08 20:39:52 +01:00
|
|
|
* state "Dormant".
|
2019-12-02 00:42:40 +01:00
|
|
|
*/
|
|
|
|
export async function processReserve(
|
|
|
|
ws: InternalWalletState,
|
|
|
|
reservePub: string,
|
2022-03-29 13:47:32 +02:00
|
|
|
options: {
|
|
|
|
forceNow?: boolean;
|
|
|
|
} = {},
|
2019-12-02 00:42:40 +01:00
|
|
|
): Promise<void> {
|
2019-12-05 19:38:19 +01:00
|
|
|
return ws.memoProcessReserve.memo(reservePub, async () => {
|
2022-03-22 21:16:38 +01:00
|
|
|
const onOpError = (err: TalerErrorDetail): Promise<void> =>
|
2022-03-08 20:39:52 +01:00
|
|
|
reportReserveError(ws, reservePub, err);
|
2019-12-05 19:38:19 +01:00
|
|
|
await guardOperationException(
|
2022-03-29 13:47:32 +02:00
|
|
|
() => processReserveImpl(ws, reservePub, options),
|
2019-12-05 19:38:19 +01:00
|
|
|
onOpError,
|
2019-12-02 00:42:40 +01:00
|
|
|
);
|
2019-12-05 19:38:19 +01:00
|
|
|
});
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
async function registerReserveWithBank(
|
|
|
|
ws: InternalWalletState,
|
|
|
|
reservePub: string,
|
|
|
|
): Promise<void> {
|
2021-06-09 15:14:17 +02:00
|
|
|
const reserve = await ws.db
|
|
|
|
.mktx((x) => ({
|
|
|
|
reserves: x.reserves,
|
|
|
|
}))
|
|
|
|
.runReadOnly(async (tx) => {
|
|
|
|
return await tx.reserves.get(reservePub);
|
|
|
|
});
|
2019-12-02 00:42:40 +01:00
|
|
|
switch (reserve?.reserveStatus) {
|
2022-03-08 20:39:52 +01:00
|
|
|
case ReserveRecordStatus.WaitConfirmBank:
|
|
|
|
case ReserveRecordStatus.RegisteringBank:
|
2019-12-02 00:42:40 +01:00
|
|
|
break;
|
|
|
|
default:
|
|
|
|
return;
|
|
|
|
}
|
2020-05-15 12:33:52 +02:00
|
|
|
const bankInfo = reserve.bankInfo;
|
|
|
|
if (!bankInfo) {
|
2019-12-02 00:42:40 +01:00
|
|
|
return;
|
|
|
|
}
|
2020-05-15 12:33:52 +02:00
|
|
|
const bankStatusUrl = bankInfo.statusUrl;
|
2020-08-20 12:57:20 +02:00
|
|
|
const httpResp = await ws.http.postJson(
|
|
|
|
bankStatusUrl,
|
|
|
|
{
|
|
|
|
reserve_pub: reservePub,
|
|
|
|
selected_exchange: bankInfo.exchangePaytoUri,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
timeout: getReserveRequestTimeout(reserve),
|
|
|
|
},
|
|
|
|
);
|
2020-07-31 16:43:59 +02:00
|
|
|
await readSuccessResponseJsonOrThrow(
|
|
|
|
httpResp,
|
|
|
|
codecForBankWithdrawalOperationPostResponse(),
|
|
|
|
);
|
2021-06-09 15:14:17 +02:00
|
|
|
await ws.db
|
|
|
|
.mktx((x) => ({
|
|
|
|
reserves: x.reserves,
|
|
|
|
}))
|
|
|
|
.runReadWrite(async (tx) => {
|
|
|
|
const r = await tx.reserves.get(reservePub);
|
|
|
|
if (!r) {
|
2019-12-02 00:42:40 +01:00
|
|
|
return;
|
2021-06-09 15:14:17 +02:00
|
|
|
}
|
|
|
|
switch (r.reserveStatus) {
|
2022-03-08 20:39:52 +01:00
|
|
|
case ReserveRecordStatus.RegisteringBank:
|
|
|
|
case ReserveRecordStatus.WaitConfirmBank:
|
2021-06-09 15:14:17 +02:00
|
|
|
break;
|
|
|
|
default:
|
|
|
|
return;
|
|
|
|
}
|
2022-03-18 15:32:41 +01:00
|
|
|
r.timestampReserveInfoPosted = AbsoluteTime.toTimestamp(
|
|
|
|
AbsoluteTime.now(),
|
|
|
|
);
|
2022-03-08 20:39:52 +01:00
|
|
|
r.reserveStatus = ReserveRecordStatus.WaitConfirmBank;
|
2022-01-11 21:00:12 +01:00
|
|
|
r.operationStatus = OperationStatus.Pending;
|
2021-06-09 15:14:17 +02:00
|
|
|
if (!r.bankInfo) {
|
|
|
|
throw Error("invariant failed");
|
|
|
|
}
|
2022-03-29 13:47:32 +02:00
|
|
|
r.retryInfo = resetRetryInfo();
|
2021-06-09 15:14:17 +02:00
|
|
|
await tx.reserves.put(r);
|
|
|
|
});
|
2020-07-20 12:50:32 +02:00
|
|
|
ws.notify({ type: NotificationType.ReserveRegisteredWithBank });
|
2019-12-02 00:42:40 +01:00
|
|
|
return processReserveBankStatus(ws, reservePub);
|
|
|
|
}
|
|
|
|
|
2020-08-20 12:57:20 +02:00
|
|
|
export function getReserveRequestTimeout(r: ReserveRecord): Duration {
|
|
|
|
return durationMax(
|
|
|
|
{ d_ms: 60000 },
|
|
|
|
durationMin({ d_ms: 5000 }, getRetryDuration(r.retryInfo)),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-03-08 20:39:52 +01:00
|
|
|
async function processReserveBankStatus(
|
2019-12-05 19:38:19 +01:00
|
|
|
ws: InternalWalletState,
|
|
|
|
reservePub: string,
|
2019-12-02 00:42:40 +01:00
|
|
|
): Promise<void> {
|
2021-06-09 15:14:17 +02:00
|
|
|
const reserve = await ws.db
|
|
|
|
.mktx((x) => ({
|
|
|
|
reserves: x.reserves,
|
|
|
|
}))
|
|
|
|
.runReadOnly(async (tx) => {
|
|
|
|
return tx.reserves.get(reservePub);
|
|
|
|
});
|
2019-12-02 00:42:40 +01:00
|
|
|
switch (reserve?.reserveStatus) {
|
2022-03-08 20:39:52 +01:00
|
|
|
case ReserveRecordStatus.WaitConfirmBank:
|
|
|
|
case ReserveRecordStatus.RegisteringBank:
|
2019-12-02 00:42:40 +01:00
|
|
|
break;
|
|
|
|
default:
|
|
|
|
return;
|
|
|
|
}
|
2020-05-12 10:38:58 +02:00
|
|
|
const bankStatusUrl = reserve.bankInfo?.statusUrl;
|
2019-12-02 00:42:40 +01:00
|
|
|
if (!bankStatusUrl) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-08-20 12:57:20 +02:00
|
|
|
const statusResp = await ws.http.get(bankStatusUrl, {
|
|
|
|
timeout: getReserveRequestTimeout(reserve),
|
|
|
|
});
|
2020-07-31 16:43:59 +02:00
|
|
|
const status = await readSuccessResponseJsonOrThrow(
|
|
|
|
statusResp,
|
|
|
|
codecForWithdrawOperationStatusResponse(),
|
2020-04-07 10:07:32 +02:00
|
|
|
);
|
2019-12-02 00:42:40 +01:00
|
|
|
|
2020-08-20 11:04:56 +02:00
|
|
|
if (status.aborted) {
|
|
|
|
logger.trace("bank aborted the withdrawal");
|
2021-06-09 15:14:17 +02:00
|
|
|
await ws.db
|
|
|
|
.mktx((x) => ({
|
|
|
|
reserves: x.reserves,
|
|
|
|
}))
|
|
|
|
.runReadWrite(async (tx) => {
|
|
|
|
const r = await tx.reserves.get(reservePub);
|
|
|
|
if (!r) {
|
2020-08-20 11:04:56 +02:00
|
|
|
return;
|
2021-06-09 15:14:17 +02:00
|
|
|
}
|
|
|
|
switch (r.reserveStatus) {
|
2022-03-08 20:39:52 +01:00
|
|
|
case ReserveRecordStatus.RegisteringBank:
|
|
|
|
case ReserveRecordStatus.WaitConfirmBank:
|
2021-06-09 15:14:17 +02:00
|
|
|
break;
|
|
|
|
default:
|
|
|
|
return;
|
|
|
|
}
|
2022-03-18 15:32:41 +01:00
|
|
|
const now = AbsoluteTime.toTimestamp(AbsoluteTime.now());
|
2021-06-09 15:14:17 +02:00
|
|
|
r.timestampBankConfirmed = now;
|
2022-03-08 20:39:52 +01:00
|
|
|
r.reserveStatus = ReserveRecordStatus.BankAborted;
|
2022-01-11 21:00:12 +01:00
|
|
|
r.operationStatus = OperationStatus.Finished;
|
2022-03-29 13:47:32 +02:00
|
|
|
r.retryInfo = resetRetryInfo();
|
2021-06-09 15:14:17 +02:00
|
|
|
await tx.reserves.put(r);
|
|
|
|
});
|
2020-08-20 11:04:56 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-12-02 00:42:40 +01:00
|
|
|
if (status.selection_done) {
|
2022-03-08 20:39:52 +01:00
|
|
|
if (reserve.reserveStatus === ReserveRecordStatus.RegisteringBank) {
|
2019-12-02 00:42:40 +01:00
|
|
|
await registerReserveWithBank(ws, reservePub);
|
|
|
|
return await processReserveBankStatus(ws, reservePub);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
await registerReserveWithBank(ws, reservePub);
|
|
|
|
return await processReserveBankStatus(ws, reservePub);
|
|
|
|
}
|
|
|
|
|
2021-06-09 15:14:17 +02:00
|
|
|
await ws.db
|
|
|
|
.mktx((x) => ({
|
|
|
|
reserves: x.reserves,
|
|
|
|
}))
|
|
|
|
.runReadWrite(async (tx) => {
|
|
|
|
const r = await tx.reserves.get(reservePub);
|
|
|
|
if (!r) {
|
|
|
|
return;
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
2021-06-09 15:14:17 +02:00
|
|
|
if (status.transfer_done) {
|
|
|
|
switch (r.reserveStatus) {
|
2022-03-08 20:39:52 +01:00
|
|
|
case ReserveRecordStatus.RegisteringBank:
|
|
|
|
case ReserveRecordStatus.WaitConfirmBank:
|
2021-06-09 15:14:17 +02:00
|
|
|
break;
|
|
|
|
default:
|
|
|
|
return;
|
|
|
|
}
|
2022-03-18 15:32:41 +01:00
|
|
|
const now = AbsoluteTime.toTimestamp(AbsoluteTime.now());
|
2021-06-09 15:14:17 +02:00
|
|
|
r.timestampBankConfirmed = now;
|
2022-03-08 20:39:52 +01:00
|
|
|
r.reserveStatus = ReserveRecordStatus.QueryingStatus;
|
2022-01-11 21:00:12 +01:00
|
|
|
r.operationStatus = OperationStatus.Pending;
|
2022-03-29 13:47:32 +02:00
|
|
|
r.retryInfo = resetRetryInfo();
|
2021-06-09 15:14:17 +02:00
|
|
|
} else {
|
|
|
|
switch (r.reserveStatus) {
|
2022-03-08 20:39:52 +01:00
|
|
|
case ReserveRecordStatus.WaitConfirmBank:
|
2021-06-09 15:14:17 +02:00
|
|
|
break;
|
|
|
|
default:
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (r.bankInfo) {
|
|
|
|
r.bankInfo.confirmUrl = status.confirm_transfer_url;
|
|
|
|
}
|
2020-05-12 10:38:58 +02:00
|
|
|
}
|
2021-06-09 15:14:17 +02:00
|
|
|
await tx.reserves.put(r);
|
2019-12-02 00:42:40 +01:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Update the information about a reserve that is stored in the wallet
|
2021-04-27 23:42:25 +02:00
|
|
|
* by querying the reserve's exchange.
|
2020-12-16 17:59:04 +01:00
|
|
|
*
|
|
|
|
* 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.
|
2019-12-02 00:42:40 +01:00
|
|
|
*/
|
|
|
|
async function updateReserve(
|
|
|
|
ws: InternalWalletState,
|
|
|
|
reservePub: string,
|
2020-07-24 11:22:14 +02:00
|
|
|
): Promise<{ ready: boolean }> {
|
2021-06-09 15:14:17 +02:00
|
|
|
const reserve = await ws.db
|
|
|
|
.mktx((x) => ({
|
|
|
|
reserves: x.reserves,
|
|
|
|
}))
|
|
|
|
.runReadOnly(async (tx) => {
|
|
|
|
return tx.reserves.get(reservePub);
|
|
|
|
});
|
2019-12-02 00:42:40 +01:00
|
|
|
if (!reserve) {
|
|
|
|
throw Error("reserve not in db");
|
|
|
|
}
|
|
|
|
|
2022-03-08 20:39:52 +01:00
|
|
|
if (reserve.reserveStatus !== ReserveRecordStatus.QueryingStatus) {
|
2020-07-24 11:22:14 +02:00
|
|
|
return { ready: true };
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
|
|
|
|
2022-04-14 23:06:35 +02:00
|
|
|
const reserveUrl = new URL(`reserves/${reservePub}`, reserve.exchangeBaseUrl);
|
|
|
|
reserveUrl.searchParams.set("timeout_ms", "200");
|
|
|
|
|
2020-07-22 10:52:03 +02:00
|
|
|
const resp = await ws.http.get(
|
2022-04-14 23:06:35 +02:00
|
|
|
reserveUrl.href,
|
2020-08-20 12:57:20 +02:00
|
|
|
{
|
|
|
|
timeout: getReserveRequestTimeout(reserve),
|
|
|
|
},
|
2020-07-22 10:52:03 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
const result = await readSuccessResponseJsonOrErrorCode(
|
|
|
|
resp,
|
|
|
|
codecForReserveStatus(),
|
|
|
|
);
|
2022-01-11 12:48:32 +01:00
|
|
|
|
2020-07-22 10:52:03 +02:00
|
|
|
if (result.isError) {
|
|
|
|
if (
|
|
|
|
resp.status === 404 &&
|
2020-11-27 11:23:06 +01:00
|
|
|
result.talerErrorResponse.code ===
|
2022-03-22 21:16:38 +01:00
|
|
|
TalerErrorCode.EXCHANGE_RESERVES_STATUS_UNKNOWN
|
2020-07-22 10:52:03 +02:00
|
|
|
) {
|
|
|
|
ws.notify({
|
|
|
|
type: NotificationType.ReserveNotYetFound,
|
|
|
|
reservePub,
|
2019-12-19 21:22:29 +01:00
|
|
|
});
|
2020-07-24 11:22:14 +02:00
|
|
|
return { ready: false };
|
2020-07-22 10:52:03 +02:00
|
|
|
} else {
|
|
|
|
throwUnexpectedRequestError(resp, result.talerErrorResponse);
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
|
|
|
}
|
2020-07-22 10:52:03 +02:00
|
|
|
|
2022-01-11 12:48:32 +01:00
|
|
|
logger.trace(`got reserve status ${j2s(result.response)}`);
|
|
|
|
|
2020-07-22 10:52:03 +02:00
|
|
|
const reserveInfo = result.response;
|
2022-03-10 16:30:24 +01:00
|
|
|
const reserveBalance = Amounts.parseOrThrow(reserveInfo.balance);
|
|
|
|
const currency = reserveBalance.currency;
|
2020-12-16 17:59:04 +01:00
|
|
|
|
|
|
|
await updateWithdrawalDenoms(ws, reserve.exchangeBaseUrl);
|
2021-04-07 19:29:51 +02:00
|
|
|
const denoms = await getCandidateWithdrawalDenoms(
|
|
|
|
ws,
|
|
|
|
reserve.exchangeBaseUrl,
|
|
|
|
);
|
2020-12-16 17:59:04 +01:00
|
|
|
|
2021-06-09 15:14:17 +02:00
|
|
|
const newWithdrawalGroup = await ws.db
|
|
|
|
.mktx((x) => ({
|
|
|
|
planchets: x.planchets,
|
|
|
|
withdrawalGroups: x.withdrawalGroups,
|
|
|
|
reserves: x.reserves,
|
2022-03-10 16:30:24 +01:00
|
|
|
denominations: x.denominations,
|
2021-06-09 15:14:17 +02:00
|
|
|
}))
|
|
|
|
.runReadWrite(async (tx) => {
|
|
|
|
const newReserve = await tx.reserves.get(reserve.reservePub);
|
2020-12-16 17:59:04 +01:00
|
|
|
if (!newReserve) {
|
2019-12-02 00:42:40 +01:00
|
|
|
return;
|
|
|
|
}
|
2022-03-10 16:30:24 +01:00
|
|
|
let amountReservePlus = reserveBalance;
|
2020-12-16 17:59:04 +01:00
|
|
|
let amountReserveMinus = Amounts.getZero(currency);
|
|
|
|
|
2022-03-10 16:30:24 +01:00
|
|
|
// Subtract amount allocated in unfinished withdrawal groups
|
|
|
|
// for this reserve from the available amount.
|
2021-06-09 15:14:17 +02:00
|
|
|
await tx.withdrawalGroups.indexes.byReservePub
|
|
|
|
.iter(reservePub)
|
2022-03-10 16:30:24 +01:00
|
|
|
.forEachAsync(async (wg) => {
|
|
|
|
if (wg.timestampFinish) {
|
|
|
|
return;
|
2020-12-16 17:59:04 +01:00
|
|
|
}
|
2022-03-10 16:30:24 +01:00
|
|
|
await tx.planchets.indexes.byGroup
|
|
|
|
.iter(wg.withdrawalGroupId)
|
|
|
|
.forEachAsync(async (pr) => {
|
|
|
|
if (pr.withdrawalDone) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const denomInfo = await ws.getDenomInfo(
|
|
|
|
ws,
|
|
|
|
tx,
|
|
|
|
wg.exchangeBaseUrl,
|
|
|
|
pr.denomPubHash,
|
|
|
|
);
|
|
|
|
if (!denomInfo) {
|
|
|
|
logger.error(`no denom info found for ${pr.denomPubHash}`);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
amountReserveMinus = Amounts.add(
|
|
|
|
amountReserveMinus,
|
|
|
|
denomInfo.value,
|
|
|
|
denomInfo.feeWithdraw,
|
|
|
|
).amount;
|
|
|
|
});
|
|
|
|
});
|
2020-05-11 18:17:35 +02:00
|
|
|
|
2022-01-11 12:48:32 +01:00
|
|
|
const remainingAmount = Amounts.sub(
|
|
|
|
amountReservePlus,
|
|
|
|
amountReserveMinus,
|
|
|
|
).amount;
|
2022-03-29 21:21:57 +02:00
|
|
|
const denomSel = selectWithdrawalDenominations(
|
2020-12-16 17:59:04 +01:00
|
|
|
remainingAmount,
|
|
|
|
denoms,
|
2019-12-16 21:10:57 +01:00
|
|
|
);
|
|
|
|
|
2021-01-14 17:24:44 +01:00
|
|
|
logger.trace(
|
|
|
|
`Remaining unclaimed amount in reseve is ${Amounts.stringify(
|
|
|
|
remainingAmount,
|
2021-12-13 11:28:15 +01:00
|
|
|
)} and can be withdrawn with ${
|
2022-03-29 21:21:57 +02:00
|
|
|
denomSel.selectedDenoms.length
|
2021-01-14 17:24:44 +01:00
|
|
|
} coins`,
|
|
|
|
);
|
2020-12-16 17:59:04 +01:00
|
|
|
|
2022-03-29 21:21:57 +02:00
|
|
|
if (denomSel.selectedDenoms.length === 0) {
|
2022-03-08 20:39:52 +01:00
|
|
|
newReserve.reserveStatus = ReserveRecordStatus.Dormant;
|
2022-01-11 21:00:12 +01:00
|
|
|
newReserve.operationStatus = OperationStatus.Finished;
|
2022-03-08 20:39:52 +01:00
|
|
|
delete newReserve.lastError;
|
|
|
|
delete newReserve.retryInfo;
|
2021-06-09 15:14:17 +02:00
|
|
|
await tx.reserves.put(newReserve);
|
2021-01-14 17:24:44 +01:00
|
|
|
return;
|
2020-03-16 13:16:57 +01:00
|
|
|
}
|
2021-01-14 17:24:44 +01:00
|
|
|
|
|
|
|
let withdrawalGroupId: string;
|
|
|
|
|
|
|
|
if (!newReserve.initialWithdrawalStarted) {
|
|
|
|
withdrawalGroupId = newReserve.initialWithdrawalGroupId;
|
|
|
|
newReserve.initialWithdrawalStarted = true;
|
|
|
|
} else {
|
|
|
|
withdrawalGroupId = encodeCrock(randomBytes(32));
|
|
|
|
}
|
|
|
|
|
|
|
|
const withdrawalRecord: WithdrawalGroupRecord = {
|
|
|
|
withdrawalGroupId: withdrawalGroupId,
|
|
|
|
exchangeBaseUrl: reserve.exchangeBaseUrl,
|
|
|
|
reservePub: reserve.reservePub,
|
|
|
|
rawWithdrawalAmount: remainingAmount,
|
2022-03-18 15:32:41 +01:00
|
|
|
timestampStart: AbsoluteTime.toTimestamp(AbsoluteTime.now()),
|
2022-03-29 13:47:32 +02:00
|
|
|
retryInfo: resetRetryInfo(),
|
2021-01-14 17:24:44 +01:00
|
|
|
lastError: undefined,
|
2022-03-29 21:21:57 +02:00
|
|
|
denomsSel: denomSel,
|
2021-01-14 17:24:44 +01:00
|
|
|
secretSeed: encodeCrock(getRandomBytes(64)),
|
2021-05-12 13:34:49 +02:00
|
|
|
denomSelUid: encodeCrock(getRandomBytes(32)),
|
2022-01-11 21:00:12 +01:00
|
|
|
operationStatus: OperationStatus.Pending,
|
2021-01-14 17:24:44 +01:00
|
|
|
};
|
|
|
|
|
2022-03-08 20:39:52 +01:00
|
|
|
delete newReserve.lastError;
|
|
|
|
delete newReserve.retryInfo;
|
|
|
|
newReserve.reserveStatus = ReserveRecordStatus.Dormant;
|
2022-01-11 21:00:12 +01:00
|
|
|
newReserve.operationStatus = OperationStatus.Finished;
|
2021-01-14 17:24:44 +01:00
|
|
|
|
2021-06-09 15:14:17 +02:00
|
|
|
await tx.reserves.put(newReserve);
|
|
|
|
await tx.withdrawalGroups.put(withdrawalRecord);
|
2021-01-14 17:24:44 +01:00
|
|
|
return withdrawalRecord;
|
2021-06-09 15:14:17 +02:00
|
|
|
});
|
2020-12-16 17:59:04 +01:00
|
|
|
|
|
|
|
if (newWithdrawalGroup) {
|
|
|
|
logger.trace("processing new withdraw group");
|
|
|
|
ws.notify({
|
|
|
|
type: NotificationType.WithdrawGroupCreated,
|
|
|
|
withdrawalGroupId: newWithdrawalGroup.withdrawalGroupId,
|
|
|
|
});
|
|
|
|
await processWithdrawGroup(ws, newWithdrawalGroup.withdrawalGroupId);
|
2020-09-03 22:50:20 +02:00
|
|
|
}
|
2020-12-16 17:59:04 +01:00
|
|
|
|
2020-07-24 11:22:14 +02:00
|
|
|
return { ready: true };
|
2019-12-02 00:42:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
async function processReserveImpl(
|
|
|
|
ws: InternalWalletState,
|
|
|
|
reservePub: string,
|
2022-03-29 13:47:32 +02:00
|
|
|
options: {
|
|
|
|
forceNow?: boolean;
|
|
|
|
} = {},
|
2019-12-02 00:42:40 +01:00
|
|
|
): Promise<void> {
|
2022-03-29 13:47:32 +02:00
|
|
|
const forceNow = options.forceNow ?? false;
|
|
|
|
await setupReserveRetry(ws, reservePub, { reset: forceNow });
|
2021-06-09 15:14:17 +02:00
|
|
|
const reserve = await ws.db
|
|
|
|
.mktx((x) => ({
|
|
|
|
reserves: x.reserves,
|
|
|
|
}))
|
|
|
|
.runReadOnly(async (tx) => {
|
|
|
|
return tx.reserves.get(reservePub);
|
|
|
|
});
|
2019-12-02 00:42:40 +01:00
|
|
|
if (!reserve) {
|
2022-03-08 20:39:52 +01:00
|
|
|
logger.error(
|
|
|
|
`not processing reserve: reserve ${reservePub} does not exist`,
|
|
|
|
);
|
2019-12-02 00:42:40 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
logger.trace(
|
|
|
|
`Processing reserve ${reservePub} with status ${reserve.reserveStatus}`,
|
|
|
|
);
|
|
|
|
switch (reserve.reserveStatus) {
|
2022-03-08 20:39:52 +01:00
|
|
|
case ReserveRecordStatus.RegisteringBank:
|
2019-12-02 00:42:40 +01:00
|
|
|
await processReserveBankStatus(ws, reservePub);
|
2022-03-29 13:47:32 +02:00
|
|
|
return await processReserveImpl(ws, reservePub, { forceNow: true });
|
2022-03-08 20:39:52 +01:00
|
|
|
case ReserveRecordStatus.QueryingStatus:
|
2020-07-24 11:22:14 +02:00
|
|
|
const res = await updateReserve(ws, reservePub);
|
|
|
|
if (res.ready) {
|
2022-03-29 13:47:32 +02:00
|
|
|
return await processReserveImpl(ws, reservePub, { forceNow: true });
|
2020-07-24 11:22:14 +02:00
|
|
|
}
|
2021-06-17 18:14:56 +02:00
|
|
|
break;
|
2022-03-08 20:39:52 +01:00
|
|
|
case ReserveRecordStatus.Dormant:
|
2019-12-02 00:42:40 +01:00
|
|
|
// nothing to do
|
|
|
|
break;
|
2022-03-08 20:39:52 +01:00
|
|
|
case ReserveRecordStatus.WaitConfirmBank:
|
2019-12-02 00:42:40 +01:00
|
|
|
await processReserveBankStatus(ws, reservePub);
|
|
|
|
break;
|
2022-03-08 20:39:52 +01:00
|
|
|
case ReserveRecordStatus.BankAborted:
|
2020-08-20 11:04:56 +02:00
|
|
|
break;
|
2019-12-02 00:42:40 +01:00
|
|
|
default:
|
|
|
|
console.warn("unknown reserve record status:", reserve.reserveStatus);
|
|
|
|
assertUnreachable(reserve.reserveStatus);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2022-03-08 20:39:52 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Create a reserve for a bank-integrated withdrawal from
|
|
|
|
* a taler://withdraw URI.
|
|
|
|
*/
|
2019-12-16 16:59:09 +01:00
|
|
|
export async function createTalerWithdrawReserve(
|
|
|
|
ws: InternalWalletState,
|
|
|
|
talerWithdrawUri: string,
|
|
|
|
selectedExchange: string,
|
2022-03-29 21:21:57 +02:00
|
|
|
options: {
|
|
|
|
forcedDenomSel?: ForcedDenomSel;
|
|
|
|
} = {},
|
2019-12-16 16:59:09 +01:00
|
|
|
): Promise<AcceptWithdrawalResponse> {
|
2021-06-09 16:47:45 +02:00
|
|
|
await updateExchangeFromUrl(ws, selectedExchange);
|
2022-03-14 18:31:30 +01:00
|
|
|
const withdrawInfo = await getBankWithdrawalInfo(ws.http, talerWithdrawUri);
|
2021-06-09 16:47:45 +02:00
|
|
|
const exchangePaytoUri = await getExchangePaytoUri(
|
2019-12-16 16:59:09 +01:00
|
|
|
ws,
|
|
|
|
selectedExchange,
|
|
|
|
withdrawInfo.wireTypes,
|
|
|
|
);
|
|
|
|
const reserve = await createReserve(ws, {
|
|
|
|
amount: withdrawInfo.amount,
|
|
|
|
bankWithdrawStatusUrl: withdrawInfo.extractedStatusUrl,
|
|
|
|
exchange: selectedExchange,
|
|
|
|
senderWire: withdrawInfo.senderWire,
|
2021-06-09 16:47:45 +02:00
|
|
|
exchangePaytoUri: exchangePaytoUri,
|
2019-12-16 16:59:09 +01:00
|
|
|
});
|
|
|
|
// 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, reserve.reservePub);
|
2021-06-09 15:14:17 +02:00
|
|
|
const processedReserve = await ws.db
|
|
|
|
.mktx((x) => ({
|
|
|
|
reserves: x.reserves,
|
|
|
|
}))
|
|
|
|
.runReadOnly(async (tx) => {
|
|
|
|
return tx.reserves.get(reserve.reservePub);
|
|
|
|
});
|
2022-03-08 20:39:52 +01:00
|
|
|
if (processedReserve?.reserveStatus === ReserveRecordStatus.BankAborted) {
|
2022-03-22 21:16:38 +01:00
|
|
|
throw TalerError.fromDetail(
|
2020-08-20 11:04:56 +02:00
|
|
|
TalerErrorCode.WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK,
|
|
|
|
{},
|
|
|
|
);
|
|
|
|
}
|
2019-12-16 16:59:09 +01:00
|
|
|
return {
|
|
|
|
reservePub: reserve.reservePub,
|
|
|
|
confirmTransferUrl: withdrawInfo.confirmTransferUrl,
|
|
|
|
};
|
2019-12-16 21:10:57 +01:00
|
|
|
}
|
2020-07-16 19:22:56 +02:00
|
|
|
|
|
|
|
/**
|
2022-03-08 20:39:52 +01:00
|
|
|
* Get payto URIs that can be used to fund a reserve.
|
2020-07-16 19:22:56 +02:00
|
|
|
*/
|
|
|
|
export async function getFundingPaytoUris(
|
2021-06-09 15:14:17 +02:00
|
|
|
tx: GetReadOnlyAccess<{
|
|
|
|
reserves: typeof WalletStoresV1.reserves;
|
|
|
|
exchanges: typeof WalletStoresV1.exchanges;
|
|
|
|
exchangeDetails: typeof WalletStoresV1.exchangeDetails;
|
|
|
|
}>,
|
2020-07-16 19:22:56 +02:00
|
|
|
reservePub: string,
|
|
|
|
): Promise<string[]> {
|
2021-06-09 15:14:17 +02:00
|
|
|
const r = await tx.reserves.get(reservePub);
|
2020-07-16 19:22:56 +02:00
|
|
|
if (!r) {
|
|
|
|
logger.error(`reserve ${reservePub} not found (DB corrupted?)`);
|
|
|
|
return [];
|
|
|
|
}
|
2021-06-02 13:23:51 +02:00
|
|
|
const exchangeDetails = await getExchangeDetails(tx, r.exchangeBaseUrl);
|
|
|
|
if (!exchangeDetails) {
|
2020-07-16 19:22:56 +02:00
|
|
|
logger.error(`exchange ${r.exchangeBaseUrl} not found (DB corrupted?)`);
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
const plainPaytoUris =
|
2021-06-02 13:23:51 +02:00
|
|
|
exchangeDetails.wireInfo?.accounts.map((x) => x.payto_uri) ?? [];
|
2020-07-16 19:22:56 +02:00
|
|
|
if (!plainPaytoUris) {
|
|
|
|
logger.error(`exchange ${r.exchangeBaseUrl} has no wire info`);
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
return plainPaytoUris.map((x) =>
|
|
|
|
addPaytoQueryParams(x, {
|
|
|
|
amount: Amounts.stringify(r.instructedAmount),
|
|
|
|
message: `Taler Withdrawal ${r.reservePub}`,
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
}
|