diff options
Diffstat (limited to 'packages/taler-wallet-core/src/operations/reserves.ts')
-rw-r--r-- | packages/taler-wallet-core/src/operations/reserves.ts | 829 |
1 files changed, 0 insertions, 829 deletions
diff --git a/packages/taler-wallet-core/src/operations/reserves.ts b/packages/taler-wallet-core/src/operations/reserves.ts deleted file mode 100644 index 4b5862bef..000000000 --- a/packages/taler-wallet-core/src/operations/reserves.ts +++ /dev/null @@ -1,829 +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 <http://www.gnu.org/licenses/> - */ - -import { - CreateReserveRequest, - CreateReserveResponse, - TalerErrorDetails, - AcceptWithdrawalResponse, - Amounts, - codecForBankWithdrawalOperationPostResponse, - codecForReserveStatus, - codecForWithdrawOperationStatusResponse, - Duration, - durationMax, - durationMin, - getTimestampNow, - NotificationType, - ReserveTransactionType, - TalerErrorCode, - addPaytoQueryParams, -} from "@gnu-taler/taler-util"; -import { randomBytes } from "@gnu-taler/taler-util"; -import { - ReserveRecordStatus, - ReserveBankInfo, - ReserveRecord, - WithdrawalGroupRecord, - WalletStoresV1, -} from "../db.js"; -import { assertUnreachable } from "../util/assertUnreachable.js"; -import { canonicalizeBaseUrl } from "@gnu-taler/taler-util"; -import { - initRetryInfo, - getRetryDuration, - updateRetryInfoTimeout, -} from "../util/retries.js"; -import { guardOperationException, OperationFailedError } from "../errors.js"; -import { - updateExchangeFromUrl, - getExchangePaytoUri, - getExchangeDetails, - getExchangeTrust, -} from "./exchanges.js"; -import { InternalWalletState } from "../common.js"; -import { - updateWithdrawalDenoms, - getCandidateWithdrawalDenoms, - selectWithdrawalDenominations, - denomSelectionInfoToState, - processWithdrawGroup, - getBankWithdrawalInfo, -} from "./withdraw.js"; -import { encodeCrock, getRandomBytes } from "@gnu-taler/taler-util"; -import { Logger, URL } from "@gnu-taler/taler-util"; -import { - readSuccessResponseJsonOrErrorCode, - readSuccessResponseJsonOrThrow, - throwUnexpectedRequestError, -} from "../util/http.js"; -import { GetReadOnlyAccess } from "../util/query.js"; - -const logger = new Logger("reserves.ts"); - -async function resetReserveRetry( - ws: InternalWalletState, - reservePub: string, -): Promise<void> { - await ws.db - .mktx((x) => ({ - reserves: x.reserves, - })) - .runReadWrite(async (tx) => { - const x = await tx.reserves.get(reservePub); - if (x) { - x.retryInfo = initRetryInfo(); - await tx.reserves.put(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 { - 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)); - - await updateWithdrawalDenoms(ws, canonExchange); - const denoms = await getCandidateWithdrawalDenoms(ws, canonExchange); - const denomSelInfo = selectWithdrawalDenominations(req.amount, denoms); - 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, - requestedQuery: false, - }; - - const exchangeInfo = await updateExchangeFromUrl(ws, req.exchange); - const exchangeDetails = exchangeInfo.exchangeDetails; - if (!exchangeDetails) { - logger.trace(exchangeDetails); - throw Error("exchange not updated"); - } - const { isAudited, isTrusted } = await getExchangeTrust( - ws, - exchangeInfo.exchange, - ); - - const resp = await ws.db - .mktx((x) => ({ - exchangeTrust: x.exchangeTrust, - reserves: x.reserves, - bankWithdrawUris: x.bankWithdrawUris, - })) - .runReadWrite(async (tx) => { - // Check if we have already created a reserve for that bankWithdrawStatusUrl - if (reserveRecord.bankInfo?.statusUrl) { - const bwi = await tx.bankWithdrawUris.get( - reserveRecord.bankInfo.statusUrl, - ); - if (bwi) { - const otherReserve = await tx.reserves.get(bwi.reservePub); - if (otherReserve) { - logger.trace( - "returning existing reserve for bankWithdrawStatusUri", - ); - return { - exchange: otherReserve.exchangeBaseUrl, - reservePub: otherReserve.reservePub, - }; - } - } - await tx.bankWithdrawUris.put({ - reservePub: reserveRecord.reservePub, - talerWithdrawUri: reserveRecord.bankInfo.statusUrl, - }); - } - if (!isAudited && !isTrusted) { - await tx.exchangeTrust.put({ - currency: reserveRecord.currency, - exchangeBaseUrl: reserveRecord.exchangeBaseUrl, - exchangeMasterPub: exchangeDetails.masterPublicKey, - uids: [encodeCrock(getRandomBytes(32))], - }); - } - await tx.reserves.put(reserveRecord); - 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<void> { - 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) { - case ReserveRecordStatus.DORMANT: - reserve.reserveStatus = ReserveRecordStatus.QUERYING_STATUS; - break; - default: - reserve.requestedQuery = true; - break; - } - reserve.retryInfo = initRetryInfo(); - await tx.reserves.put(reserve); - }); - await processReserve(ws, reservePub, true); -} - -/** - * First fetch information required 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<void> { - return ws.memoProcessReserve.memo(reservePub, async () => { - const onOpError = (err: TalerErrorDetails): Promise<void> => - incrementReserveRetry(ws, reservePub, err); - await guardOperationException( - () => processReserveImpl(ws, reservePub, forceNow), - onOpError, - ); - }); -} - -async function registerReserveWithBank( - ws: InternalWalletState, - reservePub: string, -): Promise<void> { - const reserve = await ws.db - .mktx((x) => ({ - reserves: x.reserves, - })) - .runReadOnly(async (tx) => { - return await tx.reserves.get(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(), - ); - await ws.db - .mktx((x) => ({ - reserves: x.reserves, - })) - .runReadWrite(async (tx) => { - const r = await tx.reserves.get(reservePub); - if (!r) { - return; - } - 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(); - await tx.reserves.put(r); - }); - ws.notify({ type: NotificationType.ReserveRegisteredWithBank }); - return processReserveBankStatus(ws, reservePub); -} - -async function processReserveBankStatus( - ws: InternalWalletState, - reservePub: string, -): Promise<void> { - const onOpError = (err: TalerErrorDetails): Promise<void> => - 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)), - ); -} - -async function processReserveBankStatusImpl( - ws: InternalWalletState, - reservePub: string, -): Promise<void> { - const reserve = await ws.db - .mktx((x) => ({ - reserves: x.reserves, - })) - .runReadOnly(async (tx) => { - return tx.reserves.get(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, { - timeout: getReserveRequestTimeout(reserve), - }); - const status = await readSuccessResponseJsonOrThrow( - statusResp, - codecForWithdrawOperationStatusResponse(), - ); - - if (status.aborted) { - logger.trace("bank aborted the withdrawal"); - await ws.db - .mktx((x) => ({ - reserves: x.reserves, - })) - .runReadWrite(async (tx) => { - const r = await tx.reserves.get(reservePub); - if (!r) { - return; - } - 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(); - await tx.reserves.put(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); - } - - await ws.db - .mktx((x) => ({ - reserves: x.reserves, - })) - .runReadWrite(async (tx) => { - const r = await tx.reserves.get(reservePub); - if (!r) { - return; - } - if (status.transfer_done) { - 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(); - } else { - switch (r.reserveStatus) { - case ReserveRecordStatus.WAIT_CONFIRM_BANK: - break; - default: - return; - } - if (r.bankInfo) { - r.bankInfo.confirmUrl = status.confirm_transfer_url; - } - } - await tx.reserves.put(r); - }); -} - -async function incrementReserveRetry( - ws: InternalWalletState, - reservePub: string, - err: TalerErrorDetails | undefined, -): 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) { - return; - } - r.retryInfo.retryCounter++; - updateRetryInfoTimeout(r.retryInfo); - r.lastError = err; - await tx.reserves.put(r); - }); - if (err) { - ws.notify({ - type: NotificationType.ReserveOperationError, - error: err, - }); - } -} - -/** - * 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 updateReserve( - ws: InternalWalletState, - reservePub: string, -): Promise<{ ready: boolean }> { - const reserve = await ws.db - .mktx((x) => ({ - reserves: x.reserves, - })) - .runReadOnly(async (tx) => { - return tx.reserves.get(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, - { - timeout: getReserveRequestTimeout(reserve), - }, - ); - - const result = await readSuccessResponseJsonOrErrorCode( - resp, - codecForReserveStatus(), - ); - if (result.isError) { - if ( - resp.status === 404 && - result.talerErrorResponse.code === - TalerErrorCode.EXCHANGE_RESERVES_GET_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 updateWithdrawalDenoms(ws, reserve.exchangeBaseUrl); - const denoms = await getCandidateWithdrawalDenoms( - ws, - reserve.exchangeBaseUrl, - ); - - const newWithdrawalGroup = await ws.db - .mktx((x) => ({ - coins: x.coins, - planchets: x.planchets, - withdrawalGroups: x.withdrawalGroups, - reserves: x.reserves, - })) - .runReadWrite(async (tx) => { - const newReserve = await tx.reserves.get(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.withdrawalGroups.indexes.byReservePub - .iter(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.planchets.indexes.byCoinEvHash.get( - entry.h_coin_envelope, - ); - if (planchet) { - // Amount is already accounted in some withdrawal session - break; - } - const coin = await tx.coins.indexes.byCoinEvHash.get( - 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(); - await tx.reserves.put(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)), - denomSelUid: encodeCrock(getRandomBytes(32)), - }; - - newReserve.lastError = undefined; - newReserve.retryInfo = initRetryInfo(); - newReserve.reserveStatus = ReserveRecordStatus.DORMANT; - - await tx.reserves.put(newReserve); - await tx.withdrawalGroups.put(withdrawalRecord); - return withdrawalRecord; - }); - - if (newWithdrawalGroup) { - logger.trace("processing new withdraw group"); - ws.notify({ - type: NotificationType.WithdrawGroupCreated, - withdrawalGroupId: newWithdrawalGroup.withdrawalGroupId, - }); - await processWithdrawGroup(ws, newWithdrawalGroup.withdrawalGroupId); - } - - return { ready: true }; -} - -async function processReserveImpl( - ws: InternalWalletState, - reservePub: string, - forceNow = false, -): Promise<void> { - const reserve = await ws.db - .mktx((x) => ({ - reserves: x.reserves, - })) - .runReadOnly(async (tx) => { - return tx.reserves.get(reservePub); - }); - if (!reserve) { - logger.trace("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); - } - 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; - } -} -export async function createTalerWithdrawReserve( - ws: InternalWalletState, - talerWithdrawUri: string, - selectedExchange: string, -): Promise<AcceptWithdrawalResponse> { - await updateExchangeFromUrl(ws, selectedExchange); - const withdrawInfo = await getBankWithdrawalInfo(ws, talerWithdrawUri); - const exchangePaytoUri = await getExchangePaytoUri( - ws, - selectedExchange, - withdrawInfo.wireTypes, - ); - const reserve = await createReserve(ws, { - amount: withdrawInfo.amount, - bankWithdrawStatusUrl: withdrawInfo.extractedStatusUrl, - exchange: selectedExchange, - senderWire: withdrawInfo.senderWire, - exchangePaytoUri: exchangePaytoUri, - }); - // 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 - .mktx((x) => ({ - reserves: x.reserves, - })) - .runReadOnly(async (tx) => { - return tx.reserves.get(reserve.reservePub); - }); - if (processedReserve?.reserveStatus === ReserveRecordStatus.BANK_ABORTED) { - throw OperationFailedError.fromCode( - TalerErrorCode.WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK, - "withdrawal aborted by bank", - {}, - ); - } - return { - reservePub: reserve.reservePub, - confirmTransferUrl: withdrawInfo.confirmTransferUrl, - }; -} - -/** - * Get payto URIs needed to fund a reserve. - */ -export async function getFundingPaytoUris( - tx: GetReadOnlyAccess<{ - reserves: typeof WalletStoresV1.reserves; - exchanges: typeof WalletStoresV1.exchanges; - exchangeDetails: typeof WalletStoresV1.exchangeDetails; - }>, - reservePub: string, -): Promise<string[]> { - const r = await tx.reserves.get(reservePub); - if (!r) { - logger.error(`reserve ${reservePub} not found (DB corrupted?)`); - return []; - } - const exchangeDetails = await getExchangeDetails(tx, r.exchangeBaseUrl); - if (!exchangeDetails) { - logger.error(`exchange ${r.exchangeBaseUrl} not found (DB corrupted?)`); - return []; - } - const plainPaytoUris = - exchangeDetails.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}`, - }), - ); -} |