wallet-core/packages/taler-wallet-core/src/operations/reserves.ts

780 lines
23 KiB
TypeScript
Raw Normal View History

/*
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 {
CreateReserveRequest,
CreateReserveResponse,
2020-09-01 14:57:22 +02:00
TalerErrorDetails,
2019-12-16 16:59:09 +01:00
AcceptWithdrawalResponse,
} from "../types/walletTypes";
import { canonicalizeBaseUrl } from "../util/helpers";
import { InternalWalletState } from "./state";
import {
ReserveRecordStatus,
ReserveRecord,
CurrencyRecord,
Stores,
WithdrawalGroupRecord,
ReserveBankInfo,
} from "../types/dbTypes";
import { Logger } from "../util/logging";
import { Amounts } from "../util/amounts";
import {
updateExchangeFromUrl,
getExchangeTrust,
getExchangePaytoUri,
} from "./exchanges";
import {
codecForWithdrawOperationStatusResponse,
codecForBankWithdrawalOperationPostResponse,
} from "../types/talerTypes";
import { assertUnreachable } from "../util/assertUnreachable";
2019-12-16 12:53:22 +01:00
import { encodeCrock, getRandomBytes } from "../crypto/talerCrypto";
import { randomBytes } from "../crypto/primitives/nacl-fast";
import {
processWithdrawGroup,
2019-12-16 16:59:09 +01:00
getBankWithdrawalInfo,
denomSelectionInfoToState,
updateWithdrawalDenoms,
selectWithdrawalDenominations,
getPossibleWithdrawalDenoms,
} from "./withdraw";
import {
guardOperationException,
OperationFailedAndReportedError,
makeErrorDetails,
OperationFailedError,
} from "./errors";
import { NotificationType } from "../types/notifications";
2019-12-16 12:53:22 +01:00
import { codecForReserveStatus } from "../types/ReserveStatus";
import {
getTimestampNow,
Duration,
durationMin,
durationMax,
} from "../util/time";
2020-07-16 19:22:56 +02:00
import { TransactionHandle } from "../util/query";
import { addPaytoQueryParams } from "../util/payto";
import { TalerErrorCode } from "../TalerErrorCode";
import {
readSuccessResponseJsonOrErrorCode,
throwUnexpectedRequestError,
readSuccessResponseJsonOrThrow,
} from "../util/http";
import { codecForAny } from "../util/codec";
import { URL } from "../util/url";
2020-11-27 11:23:06 +01:00
import {
initRetryInfo,
getRetryDuration,
updateRetryInfoTimeout,
} from "../util/retries";
import { ReserveTransactionType } from "../types/ReserveTransaction";
const logger = new Logger("reserves.ts");
2020-04-07 10:07:32 +02:00
async function resetReserveRetry(
ws: InternalWalletState,
reservePub: string,
): Promise<void> {
2020-03-30 12:39:32 +02:00
await ws.db.mutate(Stores.reserves, reservePub, (x) => {
if (x.retryInfo.active) {
x.retryInfo = initRetryInfo();
}
return x;
});
}
/**
* 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> {
const keypair = await ws.cryptoApi.createEddsaKeypair();
const now = getTimestampNow();
const canonExchange = canonicalizeBaseUrl(req.exchange);
let reserveStatus;
if (req.bankWithdrawStatusUrl) {
reserveStatus = ReserveRecordStatus.REGISTERING_BANK;
} else {
2020-07-16 19:22:56 +02:00
reserveStatus = ReserveRecordStatus.QUERYING_STATUS;
}
let bankInfo: ReserveBankInfo | undefined;
if (req.bankWithdrawStatusUrl) {
2020-07-16 19:22:56 +02:00
if (!req.exchangePaytoUri) {
throw Error(
"Exchange payto URI must be specified for a bank-integrated withdrawal",
);
2020-07-16 19:22:56 +02:00
}
bankInfo = {
statusUrl: req.bankWithdrawStatusUrl,
2020-07-16 19:22:56 +02:00
exchangePaytoUri: req.exchangePaytoUri,
};
}
const initialWithdrawalGroupId = encodeCrock(getRandomBytes(32));
await updateWithdrawalDenoms(ws, canonExchange);
const denoms = await getPossibleWithdrawalDenoms(ws, canonExchange);
const denomSelInfo = selectWithdrawalDenominations(req.amount, denoms);
const initialDenomSel = denomSelectionInfoToState(denomSelInfo);
const reserveRecord: ReserveRecord = {
instructedAmount: req.amount,
initialWithdrawalGroupId,
initialDenomSel,
initialWithdrawalStarted: false,
2019-12-16 16:20:45 +01:00
timestampCreated: now,
exchangeBaseUrl: canonExchange,
reservePriv: keypair.priv,
reservePub: keypair.pub,
senderWire: req.senderWire,
2020-07-16 19:22:56 +02:00
timestampBankConfirmed: undefined,
timestampReserveInfoPosted: undefined,
bankInfo,
reserveStatus,
2019-12-05 19:38:19 +01:00
lastSuccessfulStatusQuery: undefined,
retryInfo: initRetryInfo(),
lastError: undefined,
currency: req.amount.currency,
requestedQuery: false,
};
const exchangeInfo = await updateExchangeFromUrl(ws, req.exchange);
const exchangeDetails = exchangeInfo.details;
if (!exchangeDetails) {
logger.trace(exchangeDetails);
throw Error("exchange not updated");
}
const { isAudited, isTrusted } = await getExchangeTrust(ws, exchangeInfo);
2019-12-12 22:39:45 +01:00
let currencyRecord = await ws.db.get(
Stores.currencies,
exchangeDetails.currency,
);
if (!currencyRecord) {
currencyRecord = {
auditors: [],
exchanges: [],
fractionalDigits: 2,
name: exchangeDetails.currency,
};
}
if (!isAudited && !isTrusted) {
currencyRecord.exchanges.push({
2020-09-08 16:59:47 +02:00
exchangeBaseUrl: req.exchange,
exchangeMasterPub: exchangeDetails.masterPublicKey,
});
}
const cr: CurrencyRecord = currencyRecord;
2019-12-12 22:39:45 +01:00
const resp = await ws.db.runWithWriteTransaction(
[Stores.currencies, Stores.reserves, Stores.bankWithdrawUris],
2020-03-30 12:39:32 +02:00
async (tx) => {
// Check if we have already created a reserve for that bankWithdrawStatusUrl
2020-05-12 10:38:58 +02:00
if (reserveRecord.bankInfo?.statusUrl) {
const bwi = await tx.get(
Stores.bankWithdrawUris,
2020-05-12 10:38:58 +02:00
reserveRecord.bankInfo.statusUrl,
);
if (bwi) {
const otherReserve = await tx.get(Stores.reserves, bwi.reservePub);
if (otherReserve) {
logger.trace(
"returning existing reserve for bankWithdrawStatusUri",
);
return {
exchange: otherReserve.exchangeBaseUrl,
reservePub: otherReserve.reservePub,
};
}
}
await tx.put(Stores.bankWithdrawUris, {
reservePub: reserveRecord.reservePub,
2020-05-12 10:38:58 +02:00
talerWithdrawUri: reserveRecord.bankInfo.statusUrl,
});
}
await tx.put(Stores.currencies, cr);
await tx.put(Stores.reserves, reserveRecord);
const r: CreateReserveResponse = {
exchange: canonExchange,
reservePub: keypair.pub,
};
return r;
},
);
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
// Asynchronously process the reserve, but return
// to the caller already.
2020-03-30 12:39:32 +02:00
processReserve(ws, resp.reservePub, true).catch((e) => {
2020-07-23 20:52:46 +02:00
logger.error("Processing reserve (after createReserve) failed:", e);
});
return resp;
}
/**
* Re-query the status of a reserve.
*/
export async function forceQueryReserve(
ws: InternalWalletState,
reservePub: string,
): Promise<void> {
2020-03-30 12:39:32 +02:00
await ws.db.runWithWriteTransaction([Stores.reserves], async (tx) => {
const reserve = await tx.get(Stores.reserves, reservePub);
if (!reserve) {
return;
}
// Only force status query where it makes sense
switch (reserve.reserveStatus) {
case ReserveRecordStatus.DORMANT:
reserve.reserveStatus = ReserveRecordStatus.QUERYING_STATUS;
break;
default:
reserve.requestedQuery = true;
break;
}
reserve.retryInfo = initRetryInfo();
await tx.put(Stores.reserves, reserve);
});
await processReserve(ws, reservePub, true);
}
/**
* First fetch information requred to withdraw from the reserve,
* then deplete the reserve, withdrawing coins until it is empty.
*
* The returned promise resolves once the reserve is set to the
* state DORMANT.
*/
export async function processReserve(
ws: InternalWalletState,
reservePub: string,
2020-04-06 17:45:41 +02:00
forceNow = false,
): Promise<void> {
2019-12-05 19:38:19 +01:00
return ws.memoProcessReserve.memo(reservePub, async () => {
2020-09-01 14:57:22 +02:00
const onOpError = (err: TalerErrorDetails): Promise<void> =>
2019-12-05 19:38:19 +01:00
incrementReserveRetry(ws, reservePub, err);
await guardOperationException(
() => processReserveImpl(ws, reservePub, forceNow),
onOpError,
);
2019-12-05 19:38:19 +01:00
});
}
async function registerReserveWithBank(
ws: InternalWalletState,
reservePub: string,
): Promise<void> {
2020-04-06 17:45:41 +02:00
const reserve = await ws.db.get(Stores.reserves, reservePub);
switch (reserve?.reserveStatus) {
case ReserveRecordStatus.WAIT_CONFIRM_BANK:
case ReserveRecordStatus.REGISTERING_BANK:
break;
default:
return;
}
const bankInfo = reserve.bankInfo;
if (!bankInfo) {
return;
}
const bankStatusUrl = bankInfo.statusUrl;
const httpResp = await ws.http.postJson(
bankStatusUrl,
{
reserve_pub: reservePub,
selected_exchange: bankInfo.exchangePaytoUri,
},
{
timeout: getReserveRequestTimeout(reserve),
},
);
await readSuccessResponseJsonOrThrow(
httpResp,
codecForBankWithdrawalOperationPostResponse(),
);
2020-03-30 12:39:32 +02:00
await ws.db.mutate(Stores.reserves, reservePub, (r) => {
switch (r.reserveStatus) {
case ReserveRecordStatus.REGISTERING_BANK:
case ReserveRecordStatus.WAIT_CONFIRM_BANK:
break;
default:
return;
}
r.timestampReserveInfoPosted = getTimestampNow();
r.reserveStatus = ReserveRecordStatus.WAIT_CONFIRM_BANK;
if (!r.bankInfo) {
throw Error("invariant failed");
}
2019-12-05 19:38:19 +01:00
r.retryInfo = initRetryInfo();
return r;
});
2020-07-20 12:50:32 +02:00
ws.notify({ type: NotificationType.ReserveRegisteredWithBank });
return processReserveBankStatus(ws, reservePub);
}
async function processReserveBankStatus(
ws: InternalWalletState,
reservePub: string,
2019-12-05 19:38:19 +01:00
): Promise<void> {
2020-09-01 14:57:22 +02:00
const onOpError = (err: TalerErrorDetails): Promise<void> =>
2019-12-05 19:38:19 +01:00
incrementReserveRetry(ws, reservePub, err);
await guardOperationException(
() => processReserveBankStatusImpl(ws, reservePub),
onOpError,
);
}
export function getReserveRequestTimeout(r: ReserveRecord): Duration {
return durationMax(
{ d_ms: 60000 },
durationMin({ d_ms: 5000 }, getRetryDuration(r.retryInfo)),
);
}
2019-12-05 19:38:19 +01:00
async function processReserveBankStatusImpl(
ws: InternalWalletState,
reservePub: string,
): Promise<void> {
2020-04-06 17:45:41 +02:00
const reserve = await ws.db.get(Stores.reserves, reservePub);
switch (reserve?.reserveStatus) {
case ReserveRecordStatus.WAIT_CONFIRM_BANK:
case ReserveRecordStatus.REGISTERING_BANK:
break;
default:
return;
}
2020-05-12 10:38:58 +02:00
const bankStatusUrl = reserve.bankInfo?.statusUrl;
if (!bankStatusUrl) {
return;
}
const statusResp = await ws.http.get(bankStatusUrl, {
timeout: getReserveRequestTimeout(reserve),
});
const status = await readSuccessResponseJsonOrThrow(
statusResp,
codecForWithdrawOperationStatusResponse(),
2020-04-07 10:07:32 +02:00
);
if (status.aborted) {
logger.trace("bank aborted the withdrawal");
await ws.db.mutate(Stores.reserves, reservePub, (r) => {
switch (r.reserveStatus) {
case ReserveRecordStatus.REGISTERING_BANK:
case ReserveRecordStatus.WAIT_CONFIRM_BANK:
break;
default:
return;
}
const now = getTimestampNow();
r.timestampBankConfirmed = now;
r.reserveStatus = ReserveRecordStatus.BANK_ABORTED;
r.retryInfo = initRetryInfo();
return r;
});
return;
}
if (status.selection_done) {
if (reserve.reserveStatus === ReserveRecordStatus.REGISTERING_BANK) {
await registerReserveWithBank(ws, reservePub);
return await processReserveBankStatus(ws, reservePub);
}
} else {
await registerReserveWithBank(ws, reservePub);
return await processReserveBankStatus(ws, reservePub);
}
if (status.transfer_done) {
2020-03-30 12:39:32 +02:00
await ws.db.mutate(Stores.reserves, reservePub, (r) => {
switch (r.reserveStatus) {
case ReserveRecordStatus.REGISTERING_BANK:
case ReserveRecordStatus.WAIT_CONFIRM_BANK:
break;
default:
return;
}
const now = getTimestampNow();
2020-07-16 19:22:56 +02:00
r.timestampBankConfirmed = now;
r.reserveStatus = ReserveRecordStatus.QUERYING_STATUS;
2019-12-05 19:38:19 +01:00
r.retryInfo = initRetryInfo();
return r;
});
2019-12-05 19:38:19 +01:00
await processReserveImpl(ws, reservePub, true);
} else {
2020-03-30 12:39:32 +02:00
await ws.db.mutate(Stores.reserves, reservePub, (r) => {
switch (r.reserveStatus) {
case ReserveRecordStatus.WAIT_CONFIRM_BANK:
break;
default:
return;
}
2020-05-12 10:38:58 +02:00
if (r.bankInfo) {
r.bankInfo.confirmUrl = status.confirm_transfer_url;
}
return r;
});
await incrementReserveRetry(ws, reservePub, undefined);
}
}
2019-12-05 19:38:19 +01:00
async function incrementReserveRetry(
ws: InternalWalletState,
reservePub: string,
2020-09-01 14:57:22 +02:00
err: TalerErrorDetails | undefined,
): Promise<void> {
2020-03-30 12:39:32 +02:00
await ws.db.runWithWriteTransaction([Stores.reserves], async (tx) => {
2019-12-05 19:38:19 +01:00
const r = await tx.get(Stores.reserves, reservePub);
if (!r) {
return;
}
if (!r.retryInfo) {
return;
}
r.retryInfo.retryCounter++;
updateRetryInfoTimeout(r.retryInfo);
r.lastError = err;
await tx.put(Stores.reserves, r);
});
if (err) {
ws.notify({
type: NotificationType.ReserveOperationError,
error: err,
});
}
}
/**
* Update the information about a reserve that is stored in the wallet
* by quering 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 updateReserve(
ws: InternalWalletState,
reservePub: string,
2020-07-24 11:22:14 +02:00
): Promise<{ ready: boolean }> {
2019-12-12 22:39:45 +01:00
const reserve = await ws.db.get(Stores.reserves, reservePub);
if (!reserve) {
throw Error("reserve not in db");
}
if (reserve.reserveStatus !== ReserveRecordStatus.QUERYING_STATUS) {
2020-07-24 11:22:14 +02:00
return { ready: true };
}
const resp = await ws.http.get(
new URL(`reserves/${reservePub}`, reserve.exchangeBaseUrl).href,
{
timeout: getReserveRequestTimeout(reserve),
},
);
const result = await readSuccessResponseJsonOrErrorCode(
resp,
codecForReserveStatus(),
);
if (result.isError) {
if (
resp.status === 404 &&
2020-11-27 11:23:06 +01:00
result.talerErrorResponse.code ===
TalerErrorCode.EXCHANGE_RESERVES_GET_STATUS_UNKNOWN
) {
ws.notify({
type: NotificationType.ReserveNotYetFound,
reservePub,
});
await incrementReserveRetry(ws, reservePub, undefined);
2020-07-24 11:22:14 +02:00
return { ready: false };
} else {
throwUnexpectedRequestError(resp, result.talerErrorResponse);
}
}
const reserveInfo = result.response;
const balance = Amounts.parseOrThrow(reserveInfo.balance);
const currency = balance.currency;
await updateWithdrawalDenoms(ws, reserve.exchangeBaseUrl);
const denoms = await getPossibleWithdrawalDenoms(ws, reserve.exchangeBaseUrl);
const newWithdrawalGroup = await ws.db.runWithWriteTransaction(
[Stores.coins, Stores.planchets, Stores.withdrawalGroups, Stores.reserves],
2020-03-30 12:39:32 +02:00
async (tx) => {
const newReserve = await tx.get(Stores.reserves, reserve.reservePub);
if (!newReserve) {
return;
}
let amountReservePlus = Amounts.getZero(currency);
let amountReserveMinus = Amounts.getZero(currency);
// Subtract withdrawal groups for this reserve from the available amount.
await tx
.iterIndexed(Stores.withdrawalGroups.byReservePub, reservePub)
.forEach((wg) => {
const cost = wg.denomsSel.totalWithdrawCost;
amountReserveMinus = Amounts.add(amountReserveMinus, cost).amount;
});
for (const entry of reserveInfo.history) {
switch (entry.type) {
case ReserveTransactionType.Credit:
amountReservePlus = Amounts.add(
amountReservePlus,
Amounts.parseOrThrow(entry.amount),
).amount;
break;
case ReserveTransactionType.Recoup:
amountReservePlus = Amounts.add(
amountReservePlus,
Amounts.parseOrThrow(entry.amount),
).amount;
break;
case ReserveTransactionType.Closing:
amountReserveMinus = Amounts.add(
amountReserveMinus,
Amounts.parseOrThrow(entry.amount),
).amount;
break;
case ReserveTransactionType.Withdraw: {
// Now we check if the withdrawal transaction
// is part of any withdrawal known to this wallet.
const planchet = await tx.getIndexed(
Stores.planchets.coinEvHashIndex,
entry.h_coin_envelope,
);
if (planchet) {
// Amount is already accounted in some withdrawal session
break;
}
const coin = await tx.getIndexed(
Stores.coins.coinEvHashIndex,
entry.h_coin_envelope,
);
if (coin) {
// Amount is already accounted in some withdrawal session
break;
}
// Amount has been claimed by some withdrawal we don't know about
amountReserveMinus = Amounts.add(
amountReserveMinus,
Amounts.parseOrThrow(entry.amount),
).amount;
break;
}
}
}
const remainingAmount = Amounts.sub(amountReservePlus, amountReserveMinus)
.amount;
const denomSelInfo = selectWithdrawalDenominations(
remainingAmount,
denoms,
);
logger.trace(
`Remaining unclaimed amount in reseve is ${Amounts.stringify(
remainingAmount,
)} and can be withdrawn with ${
denomSelInfo.selectedDenoms.length
} coins`,
);
if (denomSelInfo.selectedDenoms.length === 0) {
newReserve.reserveStatus = ReserveRecordStatus.DORMANT;
newReserve.lastError = undefined;
newReserve.retryInfo = initRetryInfo(false);
await tx.put(Stores.reserves, newReserve);
return;
}
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,
timestampStart: getTimestampNow(),
retryInfo: initRetryInfo(),
lastError: undefined,
denomsSel: denomSelectionInfoToState(denomSelInfo),
secretSeed: encodeCrock(getRandomBytes(64)),
};
newReserve.lastError = undefined;
newReserve.retryInfo = initRetryInfo(false);
newReserve.reserveStatus = ReserveRecordStatus.DORMANT;
await tx.put(Stores.reserves, newReserve);
await tx.put(Stores.withdrawalGroups, withdrawalRecord);
return withdrawalRecord;
},
);
if (newWithdrawalGroup) {
logger.trace("processing new withdraw group");
ws.notify({
type: NotificationType.WithdrawGroupCreated,
withdrawalGroupId: newWithdrawalGroup.withdrawalGroupId,
});
await processWithdrawGroup(ws, newWithdrawalGroup.withdrawalGroupId);
}
2020-07-24 11:22:14 +02:00
return { ready: true };
}
async function processReserveImpl(
ws: InternalWalletState,
reservePub: string,
2020-04-06 17:45:41 +02:00
forceNow = false,
): Promise<void> {
2019-12-12 22:39:45 +01:00
const reserve = await ws.db.get(Stores.reserves, reservePub);
if (!reserve) {
logger.trace("not processing reserve: reserve does not exist");
return;
}
2019-12-05 19:38:19 +01:00
if (!forceNow) {
const now = getTimestampNow();
if (reserve.retryInfo.nextRetry.t_ms > now.t_ms) {
logger.trace("processReserve retry not due yet");
return;
}
} else {
await resetReserveRetry(ws, reservePub);
2019-12-05 19:38:19 +01:00
}
logger.trace(
`Processing reserve ${reservePub} with status ${reserve.reserveStatus}`,
);
switch (reserve.reserveStatus) {
case ReserveRecordStatus.REGISTERING_BANK:
await processReserveBankStatus(ws, reservePub);
return await processReserveImpl(ws, reservePub, true);
2020-07-24 11:22:14 +02:00
case ReserveRecordStatus.QUERYING_STATUS: {
const res = await updateReserve(ws, reservePub);
if (res.ready) {
return await processReserveImpl(ws, reservePub, true);
} else {
break;
}
}
case ReserveRecordStatus.DORMANT:
// nothing to do
break;
case ReserveRecordStatus.WAIT_CONFIRM_BANK:
await processReserveBankStatus(ws, reservePub);
break;
case ReserveRecordStatus.BANK_ABORTED:
break;
default:
console.warn("unknown reserve record status:", reserve.reserveStatus);
assertUnreachable(reserve.reserveStatus);
break;
}
}
2019-12-16 16:59:09 +01:00
export async function createTalerWithdrawReserve(
ws: InternalWalletState,
talerWithdrawUri: string,
selectedExchange: string,
): Promise<AcceptWithdrawalResponse> {
const withdrawInfo = await getBankWithdrawalInfo(ws, talerWithdrawUri);
const exchangeWire = await getExchangePaytoUri(
ws,
selectedExchange,
withdrawInfo.wireTypes,
);
const reserve = await createReserve(ws, {
amount: withdrawInfo.amount,
bankWithdrawStatusUrl: withdrawInfo.extractedStatusUrl,
exchange: selectedExchange,
senderWire: withdrawInfo.senderWire,
2020-07-16 19:22:56 +02:00
exchangePaytoUri: exchangeWire,
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);
const processedReserve = await ws.db.get(Stores.reserves, reserve.reservePub);
if (processedReserve?.reserveStatus === ReserveRecordStatus.BANK_ABORTED) {
throw OperationFailedError.fromCode(
TalerErrorCode.WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK,
"withdrawal aborted by bank",
{},
);
}
2019-12-16 16:59:09 +01:00
return {
reservePub: reserve.reservePub,
confirmTransferUrl: withdrawInfo.confirmTransferUrl,
};
}
2020-07-16 19:22:56 +02:00
/**
* Get payto URIs needed to fund a reserve.
*/
export async function getFundingPaytoUris(
2020-11-27 11:23:06 +01:00
tx: TransactionHandle<typeof Stores.reserves | typeof Stores.exchanges>,
2020-07-16 19:22:56 +02:00
reservePub: string,
): Promise<string[]> {
const r = await tx.get(Stores.reserves, reservePub);
if (!r) {
logger.error(`reserve ${reservePub} not found (DB corrupted?)`);
return [];
}
const exchange = await tx.get(Stores.exchanges, r.exchangeBaseUrl);
if (!exchange) {
logger.error(`exchange ${r.exchangeBaseUrl} not found (DB corrupted?)`);
return [];
}
const plainPaytoUris =
exchange.wireInfo?.accounts.map((x) => x.payto_uri) ?? [];
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}`,
}),
);
}