taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit 16712a2c4530602277cb2ceaa1b2987420abb1ce
parent b627aa39c88575c51cb3f388aeab77353a0f5983
Author: Florian Dold <florian@dold.me>
Date:   Tue, 26 May 2026 23:53:48 +0200

basic support for prepared wire transfers

There will still be some small protocol changes, so this is just a
prototype version.

The harness test only tests with libeufin-bank, not with libeufin-nexus,
thus CH-QR-Bill isn't fully tested yet.

Diffstat:
Mpackages/taler-harness/src/harness/harness.ts | 5+++++
Apackages/taler-harness/src/integrationtests/test-withdrawal-shorten.ts | 146+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-harness/src/integrationtests/testrunner.ts | 2++
Mpackages/taler-util/src/qr.ts | 10++++++++--
Mpackages/taler-util/src/types-taler-prepared-transfer.ts | 2+-
Mpackages/taler-util/src/types-taler-wallet.ts | 46++++++++++++++++++++++++++++++++++++++++------
Mpackages/taler-wallet-core/src/dev-experiments.ts | 1+
Mpackages/taler-wallet-core/src/exchanges.ts | 1+
Mpackages/taler-wallet-core/src/wallet-api-types.ts | 194+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
Mpackages/taler-wallet-core/src/withdraw.ts | 129++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
10 files changed, 401 insertions(+), 135 deletions(-)

diff --git a/packages/taler-harness/src/harness/harness.ts b/packages/taler-harness/src/harness/harness.ts @@ -698,6 +698,7 @@ export interface HarnessExchangeBankAccount { wireGatewayApiBaseUrl: string; wireGatewayAuth: BasicAuth; conversionUrl?: string; + preparedTransferUrl?: string; /** * If set, the harness will not automatically configure the wire fee for this account. */ @@ -1801,6 +1802,10 @@ export class ExchangeService implements ExchangeServiceInterface { if (acct.conversionUrl != null) { optArgs.push("conversion-url", acct.conversionUrl); } + if (acct.preparedTransferUrl) { + // FIXME: The exchange tooling has the wrong name for this option! + optArgs.push("wire-transfer-gateway", acct.preparedTransferUrl); + } if (acct.accountRestrictions != null) { optArgs.push(...acct.accountRestrictions.flat(1)); } diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-shorten.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-shorten.ts @@ -0,0 +1,146 @@ +/* + This file is part of GNU Taler + (C) 2020 Taler Systems S.A. + + 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/> + */ + +/** + * Imports. + */ +import { + AmountString, + j2s, + Logger, + succeedOrThrow, + TalerCorebankApiClient, + TalerExchangeHttpClient, +} from "@gnu-taler/taler-util"; +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { defaultCoinConfig } from "../harness/denomStructures.js"; +import { createWalletDaemonWithClient } from "../harness/environments.js"; +import { + BankService, + ExchangeService, + getTestHarnessPaytoForLabel, + GlobalTestState, + HarnessExchangeBankAccount, + LibeufinBankService, + setupDb, +} from "../harness/harness.js"; + +const logger = new Logger("test-withdrawal-shorten.ts"); + +export async function runWithdrawalShortenTest(t: GlobalTestState) { + // Set up test environment + + const db = await setupDb(t); + + const bc = { + allowRegistrations: true, + currency: "TESTKUDOS", + database: db.connStr, + httpPort: 8082, + }; + + const bank: BankService = await LibeufinBankService.create(t, bc); + + const exchange = ExchangeService.create(t, { + name: "testexchange-1", + currency: "TESTKUDOS", + httpPort: 8081, + database: db.connStr, + }); + + const receiverName = "Exchange"; + const exchangeBankUsername = "exchange"; + const exchangeBankPassword = "mypw-password"; + const exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername); + const wireGatewayApiBaseUrl = new URL( + `accounts/${exchangeBankUsername}/taler-wire-gateway/`, + bank.corebankApiBaseUrl, + ).href; + + const prepBaseUrl = + bank.corebankApiBaseUrl + "/accounts/exchange/taler-prepared-transfer/"; + + const exchangeBankAccount: HarnessExchangeBankAccount = { + wireGatewayApiBaseUrl, + wireGatewayAuth: { + type: "basic", + username: exchangeBankUsername, + password: exchangeBankPassword, + }, + accountPaytoUri: exchangePaytoUri, + preparedTransferUrl: prepBaseUrl, + }; + + await exchange.addBankAccount("1", exchangeBankAccount); + + bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri); + + await bank.start(); + + const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl, { + auth: { + username: "admin", + password: "admin-password", + }, + }); + + await bankClient.registerAccountExtended({ + name: receiverName, + password: exchangeBankPassword, + username: exchangeBankUsername, + is_taler_exchange: true, + payto_uri: exchangePaytoUri, + }); + + exchange.addOfferedCoins(defaultCoinConfig); + + await exchange.start(); + + const exchangeClient = new TalerExchangeHttpClient(exchange.baseUrl); + + const keys = succeedOrThrow(await exchangeClient.getKeys()); + + console.log(j2s(keys.accounts)); + + const { walletClient, walletService } = await createWalletDaemonWithClient( + t, + { + name: "wallet", + persistent: true, + }, + ); + + await walletClient.call(WalletApiOperation.AddExchange, { + exchangeBaseUrl: exchange.baseUrl, + }); + + const wres = await walletClient.call( + WalletApiOperation.AcceptManualWithdrawal, + { + exchangeBaseUrl: exchange.baseUrl, + amount: "TESTKUDOS:10" as AmountString, + }, + ); + + console.log(j2s(wres)); + + const acc0 = wres.withdrawalAccountsList[0]; + t.assertTrue(!!acc0); + t.assertTrue(!!acc0.transferOptions.find((x) => x.type === "payto")); + t.assertTrue(!!acc0.transferOptions.find((x) => x.type === "uri")); +} + +runWithdrawalShortenTest.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts @@ -227,6 +227,7 @@ import { runWithdrawalHugeTest } from "./test-withdrawal-huge.js"; import { runWithdrawalIdempotentTest } from "./test-withdrawal-idempotent.js"; import { runWithdrawalManualTest } from "./test-withdrawal-manual.js"; import { runWithdrawalPrepareTest } from "./test-withdrawal-prepare.js"; +import { runWithdrawalShortenTest } from "./test-withdrawal-shorten.js"; /** * Test runner. @@ -438,6 +439,7 @@ const allTests: TestMainFunction[] = [ runPaivanaRepurchaseTest, runKycFormValidationTest, runKycMerchantWalletReuseTest, + runWithdrawalShortenTest, ]; export interface TestRunSpec { diff --git a/packages/taler-util/src/qr.ts b/packages/taler-util/src/qr.ts @@ -33,6 +33,12 @@ function encodePaytoAsSwissQrBill(paytoUri: string): EncodeResult { if (parsedPayto.targetType !== "iban") { return { type: "skip" }; } + let referenceType = "NON"; + let referenceValue = ""; + if (parsedPayto.params["ch-qrr"] != null) { + referenceType = "QRR"; + referenceValue = parsedPayto.params["ch-qrr"]; + } const amountStr = parsedPayto.params["amount"]; const targetPathParts = parsedPayto.targetPath.split("/"); const beneficiaryIban = targetPathParts[targetPathParts.length - 1]; @@ -74,8 +80,8 @@ function encodePaytoAsSwissQrBill(paytoUri: string): EncodeResult { "", // Debtor town "", // Debtor country // Group: Reference - "NON", // reference type - "", // Reference + referenceType, // reference type + referenceValue, // Reference // Group: Additional Information parsedPayto.params["message"], // Unstructured data "EPD", // End of payment data diff --git a/packages/taler-util/src/types-taler-prepared-transfer.ts b/packages/taler-util/src/types-taler-prepared-transfer.ts @@ -16,6 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later */ +import { codecForAmountString } from "./amounts.js"; import { Codec, buildCodecForObject, @@ -29,7 +30,6 @@ import { codecOptional, } from "./codec.js"; import { TalerPreparedTransferApi } from "./index.js"; -import { codecForAmountString } from "./amounts.js"; import { codecForTimestamp } from "./time.js"; import { AmountString, diff --git a/packages/taler-util/src/types-taler-wallet.ts b/packages/taler-util/src/types-taler-wallet.ts @@ -1943,20 +1943,20 @@ export const codecForExchangeFullDetails = (): Codec<ExchangeFullDetails> => export interface AcceptManualWithdrawalResult { /** - * Payto URIs that can be used to fund the withdrawal. - * - * @deprecated in favor of withdrawalAccountsList + * Transaction ID of the newly created withdrawal transaction. */ - exchangePaytoUris: string[]; + transactionId: TransactionIdStr; /** * Public key of the newly created reserve. */ reservePub: string; + /** + * Bank accounts of the exchange that can be used + * to fund the withdrawal. + */ withdrawalAccountsList: WithdrawalExchangeAccountDetails[]; - - transactionId: TransactionIdStr; } export interface WithdrawalDetailsForAmount { @@ -3964,6 +3964,40 @@ export interface WithdrawalExchangeAccountDetails { * Error that happened when attempting to request the conversion rate. */ conversionError?: TalerErrorDetail; + + /** + * Timestamp that indicates when the transfer options expire. + * + * If missing, options do not expire. + */ + transferExpiry?: TalerProtocolTimestamp; + + /** + * Options for transfering funds to the exchange for the withdrawal. + */ + transferOptions: TransferOption[]; +} + +export type TransferOption = + | TransferOptionPayto + | TransferOptionUri + | TransferOptionSwissQrBill; + +export interface TransferOptionPayto { + type: "payto"; + paytoUri: string; + qrCodes: QrCodeSpec[]; +} + +export interface TransferOptionUri { + type: "uri"; + uri: string; +} + +export interface TransferOptionSwissQrBill { + type: "ch-qr-bill"; + qrReferenceNumber: string; + qrCodes: QrCodeSpec[]; } export interface PrepareWithdrawExchangeRequest { diff --git a/packages/taler-wallet-core/src/dev-experiments.ts b/packages/taler-wallet-core/src/dev-experiments.ts @@ -479,6 +479,7 @@ async function addFakeTx( { paytoUri: "payto://x-taler-bank/test/test", status: "ok", + transferOptions: [], }, ], }, diff --git a/packages/taler-wallet-core/src/exchanges.ts b/packages/taler-wallet-core/src/exchanges.ts @@ -680,6 +680,7 @@ async function validateWireInfo( conversionUrl: a.conversion_url, creditRestrictions: a.credit_restrictions, debitRestrictions: a.debit_restrictions, + wireTransferGatewayUrl: a.wire_transfer_gateway, }); isValid = v; } diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts @@ -202,76 +202,100 @@ import { PaymentBalanceDetails } from "./balance.js"; import { WithdrawTestBalanceResult } from "./testing.js"; export enum WalletApiOperation { + // Initialization and wallet lifecycle InitWallet = "initWallet", SetWalletRunConfig = "setWalletRunConfig", - WithdrawTestkudos = "withdrawTestkudos", - WithdrawTestBalance = "withdrawTestBalance", - PreparePayForUri = "preparePayForUri", - SharePayment = "sharePayment", - CheckPayForTemplate = "checkPayForTemplate", - PreparePayForTemplate = "preparePayForTemplate", - RunIntegrationTest = "runIntegrationTest", - RunIntegrationTestV2 = "runIntegrationTestV2", - TestCrypto = "testCrypto", - TestPay = "testPay", - AddExchange = "addExchange", + GetVersion = "getVersion", + Shutdown = "shutdown", + + // Balances + GetBalances = "getBalances", + GetBalanceDetail = "getBalanceDetail", + + // Misc. + + GetActiveTasks = "getActiveTasks", + ValidateIban = "validateIban", + GetCurrencySpecification = "getCurrencySpecification", + ListGlobalCurrencyExchanges = "listGlobalCurrencyExchanges", + ListGlobalCurrencyAuditors = "listGlobalCurrencyAuditors", + AddGlobalCurrencyExchange = "addGlobalCurrencyExchange", + RemoveGlobalCurrencyExchange = "removeGlobalCurrencyExchange", + AddGlobalCurrencyAuditor = "addGlobalCurrencyAuditor", + RemoveGlobalCurrencyAuditor = "removeGlobalCurrencyAuditor", + CanonicalizeBaseUrl = "canonicalizeBaseUrl", + StartExchangeWalletKyc = "startExchangeWalletKyc", + GetBankingChoicesForPayto = "getBankingChoicesForPayto", + ConvertIbanAccountFieldToPayto = "convertIbanAccountFieldToPayto", + ConvertIbanPaytoToAccountField = "convertIbanPaytoToAccountField", + + // Generic transaction management + GetTransactions = "getTransactions", GetTransactionsV2 = "getTransactionsV2", GetTransactionById = "getTransactionById", - TestingGetSampleTransactions = "testingGetSampleTransactions", - ListExchanges = "listExchanges", - GetDefaultExchanges = "getDefaultExchanges", - GetExchangeEntryByUrl = "getExchangeEntryByUrl", + RetryPendingNow = "retryPendingNow", + AbortTransaction = "abortTransaction", + FailTransaction = "failTransaction", + SuspendTransaction = "suspendTransaction", + ResumeTransaction = "resumeTransaction", + DeleteTransaction = "deleteTransaction", + RetryTransaction = "retryTransaction", + ListAssociatedRefreshes = "listAssociatedRefreshes", + + // Bank account management + ListBankAccounts = "listBankAccounts", GetBankAccountById = "getBankAccountById", AddBankAccount = "addBankAccount", ForgetBankAccount = "forgetBankAccount", - GetWithdrawalDetailsForAmount = "getWithdrawalDetailsForAmount", - AcceptManualWithdrawal = "acceptManualWithdrawal", - GetBalances = "getBalances", - GetBalanceDetail = "getBalanceDetail", - ConvertDepositAmount = "convertDepositAmount", - GetMaxDepositAmount = "getMaxDepositAmount", - GetMaxPeerPushDebitAmount = "getMaxPeerPushDebitAmount", - GetActiveTasks = "getActiveTasks", + + // Exchange entry management + + AddExchange = "addExchange", + ListExchanges = "listExchanges", + GetDefaultExchanges = "getDefaultExchanges", + GetExchangeEntryByUrl = "getExchangeEntryByUrl", + UpdateExchangeEntry = "updateExchangeEntry", + GetExchangeResources = "getExchangeResources", + CompleteExchangeBaseUrl = "completeExchangeBaseUrl", + DeleteExchange = "deleteExchange", SetExchangeTosAccepted = "setExchangeTosAccepted", SetExchangeTosForgotten = "setExchangeTosForgotten", - StartRefundQueryForUri = "startRefundQueryForUri", - StartRefundQuery = "startRefundQuery", + GetExchangeTos = "getExchangeTos", + GetExchangeDetailedInfo = "getExchangeDetailedInfo", + + // Withdrawals + + PrepareWithdrawExchange = "prepareWithdrawExchange", PrepareBankIntegratedWithdrawal = "prepareBankIntegratedWithdrawal", ConfirmWithdrawal = "confirmWithdrawal", AcceptBankIntegratedWithdrawal = "acceptBankIntegratedWithdrawal", - GetExchangeTos = "getExchangeTos", - GetExchangeDetailedInfo = "getExchangeDetailedInfo", - AddContact = "addContact", - DeleteContact = "deleteContact", - GetContacts = "getContacts", - GetMailbox = "getMailbox", - InitializeMailbox = "initializeMailbox", - GetMailboxMessages = "getMailboxMessage", - AddMailboxMessage = "addMailboxMessage", - DeleteMailboxMessage = "deleteMailboxMessage", - SendTalerUriMailboxMessage = "sendTalerUriMailboxMessage", - RefreshMailbox = "refreshMailbox", - RetryPendingNow = "retryPendingNow", - AbortTransaction = "abortTransaction", - FailTransaction = "failTransaction", - SuspendTransaction = "suspendTransaction", - ResumeTransaction = "resumeTransaction", - DeleteTransaction = "deleteTransaction", - RetryTransaction = "retryTransaction", + GetWithdrawalDetailsForAmount = "getWithdrawalDetailsForAmount", + AcceptManualWithdrawal = "acceptManualWithdrawal", + + // Merchant Payments + GetChoicesForPayment = "getChoicesForPayment", + PreparePayForUri = "preparePayForUri", + SharePayment = "sharePayment", + CheckPayForTemplate = "checkPayForTemplate", + PreparePayForTemplate = "preparePayForTemplate", + StartRefundQueryForUri = "startRefundQueryForUri", + StartRefundQuery = "startRefundQuery", ConfirmPay = "confirmPay", - DumpCoins = "dumpCoins", - SetCoinSuspended = "setCoinSuspended", - ForceRefresh = "forceRefresh", + + // Deposits + CheckDeposit = "checkDeposit", - GetVersion = "getVersion", CreateDepositGroup = "createDepositGroup", - ImportDb = "importDb", - ExportDb = "exportDb", - ExportDbToFile = "exportDbToFile", - ImportDbFromFile = "importDbFromFile", + ConvertDepositAmount = "convertDepositAmount", + GetMaxDepositAmount = "getMaxDepositAmount", + GetDepositWireTypes = "getDepositWireTypes", + GetDepositWireTypesForCurrency = "getDepositWireTypesForCurrency", + + // P2P Payments + PreparePeerPushCredit = "preparePeerPushCredit", CheckPeerPushDebit = "checkPeerPushDebit", CheckPeerPushDebitV2 = "checkPeerPushDebitV2", @@ -281,32 +305,7 @@ export enum WalletApiOperation { InitiatePeerPullCredit = "initiatePeerPullCredit", PreparePeerPullDebit = "preparePeerPullDebit", ConfirmPeerPullDebit = "confirmPeerPullDebit", - ClearDb = "clearDb", - Recycle = "recycle", - ApplyDevExperiment = "applyDevExperiment", - ValidateIban = "validateIban", - GetCurrencySpecification = "getCurrencySpecification", - UpdateExchangeEntry = "updateExchangeEntry", - PrepareWithdrawExchange = "prepareWithdrawExchange", - GetExchangeResources = "getExchangeResources", - CompleteExchangeBaseUrl = "completeExchangeBaseUrl", - DeleteExchange = "deleteExchange", - ListGlobalCurrencyExchanges = "listGlobalCurrencyExchanges", - ListGlobalCurrencyAuditors = "listGlobalCurrencyAuditors", - AddGlobalCurrencyExchange = "addGlobalCurrencyExchange", - RemoveGlobalCurrencyExchange = "removeGlobalCurrencyExchange", - AddGlobalCurrencyAuditor = "addGlobalCurrencyAuditor", - RemoveGlobalCurrencyAuditor = "removeGlobalCurrencyAuditor", - ListAssociatedRefreshes = "listAssociatedRefreshes", - Shutdown = "shutdown", - CanonicalizeBaseUrl = "canonicalizeBaseUrl", - GetDepositWireTypes = "getDepositWireTypes", - GetDepositWireTypesForCurrency = "getDepositWireTypesForCurrency", - GetQrCodesForPayto = "getQrCodesForPayto", - StartExchangeWalletKyc = "startExchangeWalletKyc", - GetBankingChoicesForPayto = "getBankingChoicesForPayto", - ConvertIbanAccountFieldToPayto = "convertIbanAccountFieldToPayto", - ConvertIbanPaytoToAccountField = "convertIbanPaytoToAccountField", + GetMaxPeerPushDebitAmount = "getMaxPeerPushDebitAmount", // Tokens and token families ListDiscounts = "listDiscounts", @@ -319,6 +318,19 @@ export enum WalletApiOperation { GetDonau = "getDonau", GetDonauStatements = "getDonauStatements", + // Mailbox + + AddContact = "addContact", + DeleteContact = "deleteContact", + GetContacts = "getContacts", + GetMailbox = "getMailbox", + InitializeMailbox = "initializeMailbox", + GetMailboxMessages = "getMailboxMessage", + AddMailboxMessage = "addMailboxMessage", + DeleteMailboxMessage = "deleteMailboxMessage", + SendTalerUriMailboxMessage = "sendTalerUriMailboxMessage", + RefreshMailbox = "refreshMailbox", + // Stored backups ListStoredBackups = "listStoredBackups", @@ -326,8 +338,27 @@ export enum WalletApiOperation { DeleteStoredBackup = "deleteStoredBackup", RecoverStoredBackup = "recoverStoredBackup", - // Testing + // Wallet database management + ImportDb = "importDb", + ExportDb = "exportDb", + ExportDbToFile = "exportDbToFile", + ImportDbFromFile = "importDbFromFile", + ClearDb = "clearDb", + Recycle = "recycle", + + // Testing + ApplyDevExperiment = "applyDevExperiment", + TestingGetSampleTransactions = "testingGetSampleTransactions", + WithdrawTestkudos = "withdrawTestkudos", + WithdrawTestBalance = "withdrawTestBalance", + RunIntegrationTest = "runIntegrationTest", + RunIntegrationTestV2 = "runIntegrationTestV2", + DumpCoins = "dumpCoins", + TestCrypto = "testCrypto", + TestPay = "testPay", + SetCoinSuspended = "setCoinSuspended", + ForceRefresh = "forceRefresh", TestingWaitTransactionsFinal = "testingWaitTransactionsFinal", TestingWaitRefreshesFinal = "testingWaitRefreshesFinal", TestingWaitTransactionState = "testingWaitTransactionState", @@ -359,6 +390,11 @@ export enum WalletApiOperation { * Use {@link WalletApiOperation.PrepareBankIntegratedWithdrawal} instead. */ GetWithdrawalDetailsForUri = "getWithdrawalDetailsForUri", + + /** + * @deprecated(2026-05-26) Consult withdrawal transaction details instead. + */ + GetQrCodesForPayto = "getQrCodesForPayto", } // group: Initialization diff --git a/packages/taler-wallet-core/src/withdraw.ts b/packages/taler-wallet-core/src/withdraw.ts @@ -65,6 +65,7 @@ import { TalerErrorCode, TalerErrorDetail, TalerPreciseTimestamp, + TalerPreparedTransferHttpClient, TalerProtocolTimestamp, TalerUriAction, Transaction, @@ -75,6 +76,7 @@ import { TransactionState, TransactionType, TransactionWithdrawal, + TransferOption, URL, UnblindedDenominationSignature, WithdrawUriInfoResponse, @@ -95,8 +97,11 @@ import { codecForExchangeWithdrawResponse, codecForLegitimizationNeededResponse, codecForReserveStatus, + decodeCrock, + eddsaSign, encodeCrock, getErrorDetailFromException, + getQrCodesForPayto, getRandomBytes, j2s, makeErrorDetail, @@ -3071,46 +3076,6 @@ export function augmentPaytoUrisForKycTransfer( ); } -/** - * Get payto URIs that can be used to fund a withdrawal operation. - */ -export async function getFundingPaytoUris( - tx: WalletIndexedDbTransaction, - withdrawalGroupId: string, -): Promise<string[]> { - const withdrawalGroup = await tx.withdrawalGroups.get(withdrawalGroupId); - checkDbInvariant(!!withdrawalGroup, `no withdrawal for ${withdrawalGroupId}`); - checkDbInvariant( - withdrawalGroup.exchangeBaseUrl !== undefined, - "can't get funding uri from uninitialized wg", - ); - checkDbInvariant( - withdrawalGroup.instructedAmount !== undefined, - "can't get funding uri from uninitialized wg", - ); - const exchangeDetails = await getExchangeDetailsInTx( - tx, - withdrawalGroup.exchangeBaseUrl, - ); - if (!exchangeDetails) { - logger.error(`exchange ${withdrawalGroup.exchangeBaseUrl} not found`); - return []; - } - const plainPaytoUris = - exchangeDetails.wireInfo?.accounts.map((x) => x.payto_uri) ?? []; - if (!plainPaytoUris) { - logger.error( - `exchange ${withdrawalGroup.exchangeBaseUrl} has no wire info`, - ); - return []; - } - return augmentPaytoUrisForWithdrawal( - plainPaytoUris, - withdrawalGroup.reservePub, - withdrawalGroup.instructedAmount, - ); -} - async function getWithdrawalGroupRecordTx( wex: WalletExecutionContext, req: { @@ -4165,7 +4130,8 @@ async function fetchAccount( instructedAmount: AmountJson, scopeInfo: ScopeInfo, acct: ExchangeWireAccount, - reservePub: string | undefined, + reservePub?: string, + reservePriv?: string, ): Promise<WithdrawalExchangeAccountDetails> { let paytoUri: string; let transferAmount: AmountString | undefined; @@ -4186,6 +4152,7 @@ async function fetchAccount( status: "error", paytoUri: acct.payto_uri, conversionError: respOrErr.talerErrorResponse, + transferOptions: [], }; } const resp = respOrErr.response; @@ -4202,6 +4169,7 @@ async function fetchAccount( status: "error", paytoUri: acct.payto_uri, conversionError: configRespOrError.talerErrorResponse, + transferOptions: [], }; } const configParsed = configRespOrError.response; @@ -4227,6 +4195,74 @@ async function fetchAccount( message: `Taler ${reservePub}`, }); } + const options: TransferOption[] = []; + let transferExpiry: TalerProtocolTimestamp | undefined = undefined; + if ( + reservePub != null && + reservePriv != null && + acct.wire_transfer_gateway != null + ) { + const prepClient = new TalerPreparedTransferHttpClient( + acct.wire_transfer_gateway, + ); + const mySig = eddsaSign(decodeCrock(reservePub), decodeCrock(reservePriv)); + const resp = succeedOrThrow( + await prepClient.register({ + account_pub: reservePub, + alg: "EdDSA", + authorization_pub: reservePub, + authorization_sig: encodeCrock(mySig), + credit_amount: transferAmount, + recurrent: false, + type: "reserve", + }), + ); + for (const opt of resp.subjects) { + switch (opt.type) { + case "SIMPLE": { + const myPayto = addPaytoQueryParams(acct.payto_uri, { + message: opt.subject, + amount: transferAmount, + }); + options.push({ + type: "payto", + paytoUri: myPayto, + qrCodes: getQrCodesForPayto(myPayto), + }); + break; + } + case "URI": { + options.push({ + type: "uri", + uri: opt.uri, + }); + break; + } + case "CH_QR_BILL": { + const myPayto = addPaytoQueryParams(acct.payto_uri, { + "ch-qrr": opt.qr_reference_number, + }); + options.push({ + type: "ch-qr-bill", + qrReferenceNumber: opt.qr_reference_number, + qrCodes: getQrCodesForPayto(myPayto), + }); + break; + } + default: + assertUnreachable; + logger.warn(`Unsupported transfer subject: ${j2s(opt)}`); + break; + } + } + transferExpiry = resp.expiration; + } else { + options.push({ + type: "payto", + paytoUri, + qrCodes: getQrCodesForPayto(paytoUri), + }); + } const acctInfo: WithdrawalExchangeAccountDetails = { status: "ok", paytoUri, @@ -4235,6 +4271,8 @@ async function fetchAccount( priority: acct.priority, currencySpecification, creditRestrictions: acct.credit_restrictions, + transferOptions: options, + transferExpiry, }; acctInfo.transferAmount = transferAmount; return acctInfo; @@ -4251,6 +4289,7 @@ async function fetchWithdrawalAccountInfo( exchange: ReadyExchangeSummary; instructedAmount: AmountJson; reservePub?: string; + reservePriv?: string; withdrawalType: WithdrawalRecordType; }, ): Promise<WithdrawalExchangeAccountDetails[]> { @@ -4269,6 +4308,7 @@ async function fetchWithdrawalAccountInfo( req.exchange.scopeInfo, acct, req.reservePub, + req.reservePriv, ); withdrawalAccounts.push(acctInfo); } @@ -4331,6 +4371,7 @@ export async function createManualWithdrawal( exchange, instructedAmount: amount, reservePub: reserveKeyPair.pub, + reservePriv: reserveKeyPair.priv, withdrawalType: WithdrawalRecordType.BankManual, }); @@ -4352,11 +4393,6 @@ export async function createManualWithdrawal( withdrawalGroup.withdrawalGroupId, ); - const exchangePaytoUris = await wex.runLegacyWalletDbTx( - async (tx) => - await getFundingPaytoUris(tx, withdrawalGroup.withdrawalGroupId), - ); - wex.ws.notify({ type: NotificationType.BalanceChange, hintTransactionId: ctx.transactionId, @@ -4366,7 +4402,6 @@ export async function createManualWithdrawal( return { reservePub: withdrawalGroup.reservePub, - exchangePaytoUris: exchangePaytoUris, withdrawalAccountsList: withdrawalAccountsList, transactionId: ctx.transactionId, };