From ffd2a62c3f7df94365980302fef3bc3376b48182 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Mon, 3 Aug 2020 13:00:48 +0530 Subject: modularize repo, use pnpm, improve typechecking --- src/operations/reserves.ts | 840 --------------------------------------------- 1 file changed, 840 deletions(-) delete mode 100644 src/operations/reserves.ts (limited to 'src/operations/reserves.ts') diff --git a/src/operations/reserves.ts b/src/operations/reserves.ts deleted file mode 100644 index 405a02f9e..000000000 --- a/src/operations/reserves.ts +++ /dev/null @@ -1,840 +0,0 @@ -/* - 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 - */ - -import { - CreateReserveRequest, - CreateReserveResponse, - OperationErrorDetails, - AcceptWithdrawalResponse, -} from "../types/walletTypes"; -import { canonicalizeBaseUrl } from "../util/helpers"; -import { InternalWalletState } from "./state"; -import { - ReserveRecordStatus, - ReserveRecord, - CurrencyRecord, - Stores, - WithdrawalGroupRecord, - initRetryInfo, - updateRetryInfoTimeout, - ReserveUpdatedEventRecord, - WalletReserveHistoryItemType, - WithdrawalSourceType, - ReserveHistoryRecord, - 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"; -import { encodeCrock, getRandomBytes } from "../crypto/talerCrypto"; -import { randomBytes } from "../crypto/primitives/nacl-fast"; -import { - selectWithdrawalDenoms, - processWithdrawGroup, - getBankWithdrawalInfo, - denomSelectionInfoToState, -} from "./withdraw"; -import { - guardOperationException, - OperationFailedAndReportedError, - makeErrorDetails, -} from "./errors"; -import { NotificationType } from "../types/notifications"; -import { codecForReserveStatus } from "../types/ReserveStatus"; -import { getTimestampNow } from "../util/time"; -import { - reconcileReserveHistory, - summarizeReserveHistory, -} from "../util/reserveHistoryUtil"; -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"; - -const logger = new Logger("reserves.ts"); - -async function resetReserveRetry( - ws: InternalWalletState, - reservePub: string, -): Promise { - 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 { - const keypair = await ws.cryptoApi.createEddsaKeypair(); - const now = getTimestampNow(); - const canonExchange = canonicalizeBaseUrl(req.exchange); - - let reserveStatus; - if (req.bankWithdrawStatusUrl) { - reserveStatus = ReserveRecordStatus.REGISTERING_BANK; - } else { - reserveStatus = ReserveRecordStatus.QUERYING_STATUS; - } - - let bankInfo: ReserveBankInfo | undefined; - - if (req.bankWithdrawStatusUrl) { - if (!req.exchangePaytoUri) { - throw Error( - "Exchange payto URI must be specified for a bank-integrated withdrawal", - ); - } - bankInfo = { - statusUrl: req.bankWithdrawStatusUrl, - exchangePaytoUri: req.exchangePaytoUri, - }; - } - - const initialWithdrawalGroupId = encodeCrock(getRandomBytes(32)); - - const denomSelInfo = await selectWithdrawalDenoms( - ws, - canonExchange, - req.amount, - ); - const initialDenomSel = denomSelectionInfoToState(denomSelInfo); - - const reserveRecord: ReserveRecord = { - instructedAmount: req.amount, - initialWithdrawalGroupId, - initialDenomSel, - initialWithdrawalStarted: false, - timestampCreated: now, - exchangeBaseUrl: canonExchange, - reservePriv: keypair.priv, - reservePub: keypair.pub, - senderWire: req.senderWire, - timestampBankConfirmed: undefined, - timestampReserveInfoPosted: undefined, - bankInfo, - reserveStatus, - lastSuccessfulStatusQuery: undefined, - retryInfo: initRetryInfo(), - lastError: undefined, - currency: req.amount.currency, - }; - - const reserveHistoryRecord: ReserveHistoryRecord = { - reservePub: keypair.pub, - reserveTransactions: [], - }; - - reserveHistoryRecord.reserveTransactions.push({ - type: WalletReserveHistoryItemType.Credit, - expectedAmount: req.amount, - }); - - const senderWire = req.senderWire; - if (senderWire) { - const rec = { - paytoUri: senderWire, - }; - await ws.db.put(Stores.senderWires, rec); - } - - const exchangeInfo = await updateExchangeFromUrl(ws, req.exchange); - const exchangeDetails = exchangeInfo.details; - if (!exchangeDetails) { - console.log(exchangeDetails); - throw Error("exchange not updated"); - } - const { isAudited, isTrusted } = await getExchangeTrust(ws, exchangeInfo); - 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({ - baseUrl: req.exchange, - exchangePub: exchangeDetails.masterPublicKey, - }); - } - - const cr: CurrencyRecord = currencyRecord; - - const resp = await ws.db.runWithWriteTransaction( - [ - Stores.currencies, - Stores.reserves, - Stores.reserveHistory, - Stores.bankWithdrawUris, - ], - async (tx) => { - // Check if we have already created a reserve for that bankWithdrawStatusUrl - if (reserveRecord.bankInfo?.statusUrl) { - const bwi = await tx.get( - Stores.bankWithdrawUris, - 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, - talerWithdrawUri: reserveRecord.bankInfo.statusUrl, - }); - } - await tx.put(Stores.currencies, cr); - await tx.put(Stores.reserves, reserveRecord); - await tx.put(Stores.reserveHistory, reserveHistoryRecord); - const r: CreateReserveResponse = { - exchange: canonExchange, - reservePub: keypair.pub, - }; - return r; - }, - ); - - if (reserveRecord.reservePub === resp.reservePub) { - // Only emit notification when a new reserve was created. - ws.notify({ - type: NotificationType.ReserveCreated, - reservePub: reserveRecord.reservePub, - }); - } - - // Asynchronously process the reserve, but return - // to the caller already. - processReserve(ws, resp.reservePub, true).catch((e) => { - 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 { - 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: - case ReserveRecordStatus.WITHDRAWING: - case ReserveRecordStatus.QUERYING_STATUS: - break; - default: - return; - } - reserve.reserveStatus = ReserveRecordStatus.QUERYING_STATUS; - 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, - forceNow = false, -): Promise { - return ws.memoProcessReserve.memo(reservePub, async () => { - const onOpError = (err: OperationErrorDetails): Promise => - incrementReserveRetry(ws, reservePub, err); - await guardOperationException( - () => processReserveImpl(ws, reservePub, forceNow), - onOpError, - ); - }); -} - -async function registerReserveWithBank( - ws: InternalWalletState, - reservePub: string, -): Promise { - 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, - }); - await readSuccessResponseJsonOrThrow( - httpResp, - codecForBankWithdrawalOperationPostResponse(), - ); - 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"); - } - r.retryInfo = initRetryInfo(); - return r; - }); - ws.notify({ type: NotificationType.ReserveRegisteredWithBank }); - return processReserveBankStatus(ws, reservePub); -} - -export async function processReserveBankStatus( - ws: InternalWalletState, - reservePub: string, -): Promise { - const onOpError = (err: OperationErrorDetails): Promise => - incrementReserveRetry(ws, reservePub, err); - await guardOperationException( - () => processReserveBankStatusImpl(ws, reservePub), - onOpError, - ); -} - -async function processReserveBankStatusImpl( - ws: InternalWalletState, - reservePub: string, -): Promise { - 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 bankStatusUrl = reserve.bankInfo?.statusUrl; - if (!bankStatusUrl) { - return; - } - - const statusResp = await ws.http.get(bankStatusUrl); - const status = await readSuccessResponseJsonOrThrow( - statusResp, - codecForWithdrawOperationStatusResponse(), - ); - - 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) { - 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.QUERYING_STATUS; - r.retryInfo = initRetryInfo(); - return r; - }); - await processReserveImpl(ws, reservePub, true); - } else { - await ws.db.mutate(Stores.reserves, reservePub, (r) => { - switch (r.reserveStatus) { - case ReserveRecordStatus.WAIT_CONFIRM_BANK: - break; - default: - return; - } - if (r.bankInfo) { - r.bankInfo.confirmUrl = status.confirm_transfer_url; - } - return r; - }); - await incrementReserveRetry(ws, reservePub, undefined); - } -} - -async function incrementReserveRetry( - ws: InternalWalletState, - reservePub: string, - err: OperationErrorDetails | undefined, -): Promise { - await ws.db.runWithWriteTransaction([Stores.reserves], async (tx) => { - 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. - */ -async function updateReserve( - ws: InternalWalletState, - reservePub: string, -): Promise<{ ready: boolean }> { - const reserve = await ws.db.get(Stores.reserves, reservePub); - if (!reserve) { - throw Error("reserve not in db"); - } - - if (reserve.reserveStatus !== ReserveRecordStatus.QUERYING_STATUS) { - return { ready: true }; - } - - const resp = await ws.http.get( - new URL(`reserves/${reservePub}`, reserve.exchangeBaseUrl).href, - ); - - const result = await readSuccessResponseJsonOrErrorCode( - resp, - codecForReserveStatus(), - ); - if (result.isError) { - if ( - resp.status === 404 && - result.talerErrorResponse.code === TalerErrorCode.RESERVE_STATUS_UNKNOWN - ) { - ws.notify({ - type: NotificationType.ReserveNotYetFound, - reservePub, - }); - await incrementReserveRetry(ws, reservePub, undefined); - return { ready: false }; - } else { - throwUnexpectedRequestError(resp, result.talerErrorResponse); - } - } - - const reserveInfo = result.response; - - const balance = Amounts.parseOrThrow(reserveInfo.balance); - const currency = balance.currency; - await ws.db.runWithWriteTransaction( - [Stores.reserves, Stores.reserveUpdatedEvents, Stores.reserveHistory], - async (tx) => { - const r = await tx.get(Stores.reserves, reservePub); - if (!r) { - return; - } - if (r.reserveStatus !== ReserveRecordStatus.QUERYING_STATUS) { - return; - } - - const hist = await tx.get(Stores.reserveHistory, reservePub); - if (!hist) { - throw Error("inconsistent database"); - } - - const newHistoryTransactions = reserveInfo.history.slice( - hist.reserveTransactions.length, - ); - - const reserveUpdateId = encodeCrock(getRandomBytes(32)); - - const reconciled = reconcileReserveHistory( - hist.reserveTransactions, - reserveInfo.history, - ); - - const summary = summarizeReserveHistory( - reconciled.updatedLocalHistory, - currency, - ); - - if ( - reconciled.newAddedItems.length + reconciled.newMatchedItems.length != - 0 - ) { - const reserveUpdate: ReserveUpdatedEventRecord = { - reservePub: r.reservePub, - timestamp: getTimestampNow(), - amountReserveBalance: Amounts.stringify(balance), - amountExpected: Amounts.stringify(summary.awaitedReserveAmount), - newHistoryTransactions, - reserveUpdateId, - }; - await tx.put(Stores.reserveUpdatedEvents, reserveUpdate); - r.reserveStatus = ReserveRecordStatus.WITHDRAWING; - r.retryInfo = initRetryInfo(); - } else { - r.reserveStatus = ReserveRecordStatus.DORMANT; - r.retryInfo = initRetryInfo(false); - } - r.lastSuccessfulStatusQuery = getTimestampNow(); - hist.reserveTransactions = reconciled.updatedLocalHistory; - r.lastError = undefined; - await tx.put(Stores.reserves, r); - await tx.put(Stores.reserveHistory, hist); - }, - ); - ws.notify({ type: NotificationType.ReserveUpdated }); - return { ready: true }; -} - -async function processReserveImpl( - ws: InternalWalletState, - reservePub: string, - forceNow = false, -): Promise { - const reserve = await ws.db.get(Stores.reserves, reservePub); - if (!reserve) { - console.log("not processing reserve: reserve does not exist"); - return; - } - 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); - } - 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); - case ReserveRecordStatus.QUERYING_STATUS: { - const res = await updateReserve(ws, reservePub); - if (res.ready) { - return await processReserveImpl(ws, reservePub, true); - } else { - break; - } - } - case ReserveRecordStatus.WITHDRAWING: - await depleteReserve(ws, reservePub); - break; - case ReserveRecordStatus.DORMANT: - // nothing to do - break; - case ReserveRecordStatus.WAIT_CONFIRM_BANK: - await processReserveBankStatus(ws, reservePub); - break; - default: - console.warn("unknown reserve record status:", reserve.reserveStatus); - assertUnreachable(reserve.reserveStatus); - break; - } -} - -/** - * Withdraw coins from a reserve until it is empty. - * - * When finished, marks the reserve as depleted by setting - * the depleted timestamp. - */ -async function depleteReserve( - ws: InternalWalletState, - reservePub: string, -): Promise { - let reserve: ReserveRecord | undefined; - let hist: ReserveHistoryRecord | undefined; - await ws.db.runWithReadTransaction( - [Stores.reserves, Stores.reserveHistory], - async (tx) => { - reserve = await tx.get(Stores.reserves, reservePub); - hist = await tx.get(Stores.reserveHistory, reservePub); - }, - ); - - if (!reserve) { - return; - } - if (!hist) { - throw Error("inconsistent database"); - } - if (reserve.reserveStatus !== ReserveRecordStatus.WITHDRAWING) { - return; - } - logger.trace(`depleting reserve ${reservePub}`); - - const summary = summarizeReserveHistory( - hist.reserveTransactions, - reserve.currency, - ); - - const withdrawAmount = summary.unclaimedReserveAmount; - - const denomsForWithdraw = await selectWithdrawalDenoms( - ws, - reserve.exchangeBaseUrl, - withdrawAmount, - ); - if (!denomsForWithdraw) { - // Only complain about inability to withdraw if we - // didn't withdraw before. - if (Amounts.isZero(summary.withdrawnAmount)) { - const opErr = makeErrorDetails( - TalerErrorCode.WALLET_EXCHANGE_DENOMINATIONS_INSUFFICIENT, - `Unable to withdraw from reserve, no denominations are available to withdraw.`, - {}, - ); - await incrementReserveRetry(ws, reserve.reservePub, opErr); - throw new OperationFailedAndReportedError(opErr); - } - return; - } - - logger.trace( - `Selected coins total cost ${Amounts.stringify( - denomsForWithdraw.totalWithdrawCost, - )} for withdrawal of ${Amounts.stringify(withdrawAmount)}`, - ); - - logger.trace("selected denominations"); - - const newWithdrawalGroup = await ws.db.runWithWriteTransaction( - [ - Stores.withdrawalGroups, - Stores.reserves, - Stores.reserveHistory, - Stores.planchets, - ], - async (tx) => { - const newReserve = await tx.get(Stores.reserves, reservePub); - if (!newReserve) { - return false; - } - if (newReserve.reserveStatus !== ReserveRecordStatus.WITHDRAWING) { - return false; - } - const newHist = await tx.get(Stores.reserveHistory, reservePub); - if (!newHist) { - throw Error("inconsistent database"); - } - const newSummary = summarizeReserveHistory( - newHist.reserveTransactions, - newReserve.currency, - ); - if ( - Amounts.cmp( - newSummary.unclaimedReserveAmount, - denomsForWithdraw.totalWithdrawCost, - ) < 0 - ) { - // Something must have happened concurrently! - logger.error( - "aborting withdrawal session, likely concurrent withdrawal happened", - ); - logger.error( - `unclaimed reserve amount is ${newSummary.unclaimedReserveAmount}`, - ); - logger.error( - `withdrawal cost is ${denomsForWithdraw.totalWithdrawCost}`, - ); - return false; - } - for (let i = 0; i < denomsForWithdraw.selectedDenoms.length; i++) { - const sd = denomsForWithdraw.selectedDenoms[i]; - for (let j = 0; j < sd.count; j++) { - const amt = Amounts.add(sd.denom.value, sd.denom.feeWithdraw).amount; - newHist.reserveTransactions.push({ - type: WalletReserveHistoryItemType.Withdraw, - expectedAmount: amt, - }); - } - } - newReserve.reserveStatus = ReserveRecordStatus.DORMANT; - newReserve.retryInfo = initRetryInfo(false); - - let withdrawalGroupId: string; - - if (!newReserve.initialWithdrawalStarted) { - withdrawalGroupId = newReserve.initialWithdrawalGroupId; - newReserve.initialWithdrawalStarted = true; - } else { - withdrawalGroupId = encodeCrock(randomBytes(32)); - } - - const withdrawalRecord: WithdrawalGroupRecord = { - withdrawalGroupId: withdrawalGroupId, - exchangeBaseUrl: newReserve.exchangeBaseUrl, - source: { - type: WithdrawalSourceType.Reserve, - reservePub: newReserve.reservePub, - }, - rawWithdrawalAmount: withdrawAmount, - timestampStart: getTimestampNow(), - retryInfo: initRetryInfo(), - lastErrorPerCoin: {}, - lastError: undefined, - denomsSel: denomSelectionInfoToState(denomsForWithdraw), - }; - - await tx.put(Stores.reserves, newReserve); - await tx.put(Stores.reserveHistory, newHist); - 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); - } else { - console.trace("withdraw session already existed"); - } -} - -export async function createTalerWithdrawReserve( - ws: InternalWalletState, - talerWithdrawUri: string, - selectedExchange: string, -): Promise { - 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, - exchangePaytoUri: exchangeWire, - }); - // 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); - return { - reservePub: reserve.reservePub, - confirmTransferUrl: withdrawInfo.confirmTransferUrl, - }; -} - -/** - * Get payto URIs needed to fund a reserve. - */ -export async function getFundingPaytoUris( - tx: TransactionHandle, - reservePub: string, -): Promise { - 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}`, - }), - ); -} -- cgit v1.2.3