From 98d7a238a615e7dc801bbfdf85188197b91f6083 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Sat, 29 Jun 2024 14:16:34 +0200 Subject: wallet-core: request handler refactoring WIP --- packages/taler-wallet-core/src/wallet.ts | 632 +++++++++++++++++++------------ 1 file changed, 387 insertions(+), 245 deletions(-) (limited to 'packages/taler-wallet-core/src/wallet.ts') diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index c2ec17f48..536f559d4 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -24,29 +24,48 @@ */ import { IDBDatabase, IDBFactory } from "@gnu-taler/idb-bridge"; import { + AbortTransactionRequest, AbsoluteTime, + AcceptManualWithdrawalRequest, + AcceptManualWithdrawalResult, ActiveTask, AddExchangeRequest, + AddKnownBankAccountsRequest, AmountJson, AmountString, Amounts, CancellationToken, CoinDumpJson, CoinStatus, + ConfirmPayRequest, + ConfirmPayResult, CoreApiResponse, CreateStoredBackupResponse, DeleteStoredBackupRequest, DenominationInfo, Duration, + EmptyObject, ExchangesShortListResponse, + FailTransactionRequest, + ForgetKnownBankAccountsRequest, + GetActiveTasksResponse, + GetContractTermsDetailsRequest, GetCurrencySpecificationRequest, GetCurrencySpecificationResponse, + GetDepositWireTypesForCurrencyRequest, + GetDepositWireTypesForCurrencyResponse, + GetExchangeTosRequest, + GetExchangeTosResult, + GetQrCodesForPaytoRequest, + GetQrCodesForPaytoResponse, + HintNetworkAvailabilityRequest, InitRequest, InitResponse, IntegrationTestArgs, IntegrationTestV2Args, KnownBankAccounts, KnownBankAccountsInfo, + ListExchangesForScopedCurrencyRequest, ListGlobalCurrencyAuditorsResponse, ListGlobalCurrencyExchangesResponse, Logger, @@ -61,22 +80,28 @@ import { RecoverStoredBackupRequest, SharePaymentRequest, SharePaymentResult, + StartRefundQueryRequest, StoredBackupList, + SuspendTransactionRequest, TalerBankIntegrationHttpClient, TalerError, TalerErrorCode, TalerProtocolTimestamp, TalerUriAction, + TestingGetDenomStatsRequest, TestingGetDenomStatsResponse, + TestingGetReserveHistoryRequest, TestingListTasksForTransactionRequest, TestingListTasksForTransactionsResponse, TestingWaitTransactionRequest, TimerAPI, TimerGroup, TransactionType, + TransactionsResponse, UpdateExchangeEntryRequest, ValidateIbanRequest, ValidateIbanResponse, + WalletContractData, WalletCoreVersion, WalletNotification, WalletRunConfig, @@ -106,6 +131,7 @@ import { codecForDeleteExchangeRequest, codecForDeleteStoredBackupRequest, codecForDeleteTransactionRequest, + codecForEmptyObject, codecForFailTransactionRequest, codecForForceRefreshRequest, codecForForgetKnownBankAccounts, @@ -167,7 +193,6 @@ import { parseTalerUri, performanceNow, safeStringifyException, - sampleWalletCoreTransactions, setDangerousTimetravel, validateIban, } from "@gnu-taler/taler-util"; @@ -181,6 +206,7 @@ import { markAttentionRequestAsRead, } from "./attention.js"; import { + RunBackupCycleRequest, addBackupProvider, codecForAddBackupProviderRequest, codecForRemoveBackupProvider, @@ -318,7 +344,6 @@ import { WALLET_MERCHANT_PROTOCOL_VERSION, } from "./versions.js"; import { - EmptyObject, WalletApiOperation, WalletCoreApiClient, WalletCoreResponseType, @@ -693,18 +718,6 @@ async function handlePrepareWithdrawExchange( }; } -/** - * Response returned from the pending operations API. - * - * @deprecated this is a placeholder for the response type of a deprecated wallet-core request. - */ -export interface PendingOperationsResponse { - /** - * List of pending operations. - */ - pendingOperations: any[]; -} - async function handleRetryPendingNow( wex: WalletExecutionContext, ): Promise { @@ -857,6 +870,343 @@ async function handleUpdateExchangeEntry( return {}; } +async function handleTestingGetDenomStats( + wex: WalletExecutionContext, + req: TestingGetDenomStatsRequest, +): Promise { + const denomStats: TestingGetDenomStatsResponse = { + numKnown: 0, + numLost: 0, + numOffered: 0, + }; + await wex.db.runReadOnlyTx({ storeNames: ["denominations"] }, async (tx) => { + const denoms = await tx.denominations.indexes.byExchangeBaseUrl.getAll( + req.exchangeBaseUrl, + ); + for (const d of denoms) { + denomStats.numKnown++; + if (d.isOffered) { + denomStats.numOffered++; + } + if (d.isLost) { + denomStats.numLost++; + } + } + }); + return denomStats; +} + +async function handleListExchangesForScopedCurrency( + wex: WalletExecutionContext, + req: ListExchangesForScopedCurrencyRequest, +): Promise { + const exchangesResp = await listExchanges(wex); + const result: ExchangesShortListResponse = { + exchanges: [], + }; + // Right now we only filter on the currency, as wallet-core doesn't + // fully support scoped currencies yet. + for (const exch of exchangesResp.exchanges) { + if (exch.currency === req.scope.currency) { + result.exchanges.push({ + exchangeBaseUrl: exch.exchangeBaseUrl, + }); + } + } + return result; +} + +async function handleAddKnownBankAccount( + wex: WalletExecutionContext, + req: AddKnownBankAccountsRequest, +): Promise { + await addKnownBankAccounts(wex, req.payto, req.alias, req.currency); + return {}; +} + +async function handleForgetKnownBankAccounts( + wex: WalletExecutionContext, + req: ForgetKnownBankAccountsRequest, +): Promise { + await forgetKnownBankAccounts(wex, req.payto); + return {}; +} + +// FIXME: Doesn't have proper type! +async function handleTestingGetReserveHistory( + wex: WalletExecutionContext, + req: TestingGetReserveHistoryRequest, +): Promise { + const reserve = await wex.db.runReadOnlyTx( + { storeNames: ["reserves"] }, + async (tx) => { + return tx.reserves.indexes.byReservePub.get(req.reservePub); + }, + ); + if (!reserve) { + throw Error("no reserve pub found"); + } + const sigResp = await wex.cryptoApi.signReserveHistoryReq({ + reservePriv: reserve.reservePriv, + startOffset: 0, + }); + const exchangeBaseUrl = req.exchangeBaseUrl; + const url = new URL(`reserves/${req.reservePub}/history`, exchangeBaseUrl); + const resp = await wex.http.fetch(url.href, { + headers: { ["Taler-Reserve-History-Signature"]: sigResp.sig }, + }); + const historyJson = await readSuccessResponseJsonOrThrow(resp, codecForAny()); + return historyJson; +} + +async function handleAcceptManualWithdrawal( + wex: WalletExecutionContext, + req: AcceptManualWithdrawalRequest, +): Promise { + const res = await createManualWithdrawal(wex, { + amount: Amounts.parseOrThrow(req.amount), + exchangeBaseUrl: req.exchangeBaseUrl, + restrictAge: req.restrictAge, + forceReservePriv: req.forceReservePriv, + }); + return res; +} + +async function handleGetExchangeTos( + wex: WalletExecutionContext, + req: GetExchangeTosRequest, +): Promise { + return getExchangeTos( + wex, + req.exchangeBaseUrl, + req.acceptedFormat, + req.acceptLanguage, + ); +} + +async function handleGetContractTermsDetails( + wex: WalletExecutionContext, + req: GetContractTermsDetailsRequest, +): Promise { + if (req.proposalId) { + // FIXME: deprecated path + return getContractTermsDetails(wex, req.proposalId); + } + if (req.transactionId) { + const parsedTx = parseTransactionIdentifier(req.transactionId); + if (parsedTx?.tag === TransactionType.Payment) { + return getContractTermsDetails(wex, parsedTx.proposalId); + } + throw Error("transactionId is not a payment transaction"); + } + throw Error("transactionId missing"); +} + +async function handleGetQrCodesForPayto( + wex: WalletExecutionContext, + req: GetQrCodesForPaytoRequest, +): Promise { + return { + codes: getQrCodesForPayto(req.paytoUri), + }; +} + +async function handleConfirmPay( + wex: WalletExecutionContext, + req: ConfirmPayRequest, +): Promise { + let transactionId; + if (req.proposalId) { + // legacy client support + transactionId = constructTransactionIdentifier({ + tag: TransactionType.Payment, + proposalId: req.proposalId, + }); + } else if (req.transactionId) { + transactionId = req.transactionId; + } else { + throw Error("transactionId or (deprecated) proposalId required"); + } + return await confirmPay(wex, transactionId, req.sessionId); +} + +async function handleAbortTransaction( + wex: WalletExecutionContext, + req: AbortTransactionRequest, +): Promise { + await abortTransaction(wex, req.transactionId); + return {}; +} + +async function handleSuspendTransaction( + wex: WalletExecutionContext, + req: SuspendTransactionRequest, +): Promise { + await suspendTransaction(wex, req.transactionId); + return {}; +} + +async function handleGetActiveTasks( + wex: WalletExecutionContext, + req: EmptyObject, +): Promise { + const allTasksId = (await getActiveTaskIds(wex.ws)).taskIds; + + const tasksInfo = await Promise.all( + allTasksId.map(async (id) => { + return await wex.db.runReadOnlyTx( + { storeNames: ["operationRetries"] }, + async (tx) => { + return tx.operationRetries.get(id); + }, + ); + }), + ); + + const tasks = allTasksId.map((taskId, i): ActiveTask => { + const transaction = convertTaskToTransactionId(taskId); + const d = tasksInfo[i]; + + const firstTry = !d + ? undefined + : timestampAbsoluteFromDb(d.retryInfo.firstTry); + const nextTry = !d + ? undefined + : timestampAbsoluteFromDb(d.retryInfo.nextRetry); + const counter = d?.retryInfo.retryCounter; + const lastError = d?.lastError; + + return { + taskId: taskId, + retryCounter: counter, + firstTry, + nextTry, + lastError, + transaction, + }; + }); + return { tasks }; +} + +async function handleFailTransaction( + wex: WalletExecutionContext, + req: FailTransactionRequest, +): Promise { + await failTransaction(wex, req.transactionId); + return {}; +} + +async function handleTestingGetSampleTransactions( + wex: WalletExecutionContext, + req: EmptyObject, +): Promise { + // FIXME! + return { transactions: [] }; + // These are out of date! + //return { transactions: sampleWalletCoreTransactions }; +} + +async function handleStartRefundQuery( + wex: WalletExecutionContext, + req: StartRefundQueryRequest, +): Promise { + const txIdParsed = parseTransactionIdentifier(req.transactionId); + if (!txIdParsed) { + throw Error("invalid transaction ID"); + } + if (txIdParsed.tag !== TransactionType.Payment) { + throw Error("expected payment transaction ID"); + } + await startQueryRefund(wex, txIdParsed.proposalId); + return {}; +} + +async function handleAddBackupProvider( + wex: WalletExecutionContext, + req: RunBackupCycleRequest, +): Promise { + await runBackupCycle(wex, req); + return {}; +} + +async function handleHintNetworkAvailability( + wex: WalletExecutionContext, + req: HintNetworkAvailabilityRequest, +): Promise { + wex.ws.networkAvailable = req.isNetworkAvailable; + // When network becomes available, restart tasks as they're blocked + // waiting for the network. + // When network goes down, restart tasks so they notice the network + // is down and wait. + await restartAllRunningTasks(wex); + return {}; +} + +async function handleGetDepositWireTypesForCurrency( + wex: WalletExecutionContext, + req: GetDepositWireTypesForCurrencyRequest, +): Promise { + const wtSet: Set = new Set(); + await wex.db.runReadOnlyTx( + { storeNames: ["exchanges", "exchangeDetails"] }, + async (tx) => { + const exchanges = await tx.exchanges.getAll(); + for (const exchange of exchanges) { + const det = await getExchangeWireDetailsInTx(tx, exchange.baseUrl); + if (!det) { + continue; + } + if (det.currency !== req.currency) { + continue; + } + for (const acc of det.wireInfo.accounts) { + let usable = true; + for (const dr of acc.debit_restrictions) { + if (dr.type === "deny") { + usable = false; + break; + } + } + if (!usable) { + break; + } + const parsedPayto = parsePaytoUri(acc.payto_uri); + if (!parsedPayto) { + continue; + } + wtSet.add(parsedPayto.targetType); + } + } + }, + ); + return { + wireTypes: [...wtSet], + }; +} + +async function handleListGlobalCurrencyExchanges( + wex: WalletExecutionContext, + req: EmptyObject, +): Promise { + const resp: ListGlobalCurrencyExchangesResponse = { + exchanges: [], + }; + await wex.db.runReadOnlyTx( + { storeNames: ["globalCurrencyExchanges"] }, + async (tx) => { + const gceList = await tx.globalCurrencyExchanges.iter().toArray(); + for (const gce of gceList) { + resp.exchanges.push({ + currency: gce.currency, + exchangeBaseUrl: gce.exchangeBaseUrl, + exchangeMasterPub: gce.exchangeMasterPub, + }); + } + }, + ); + return resp; +} + /** * Implementation of the "wallet-core" API. */ @@ -944,30 +1294,7 @@ async function dispatchRequestInternal( } case WalletApiOperation.TestingGetDenomStats: { const req = codecForTestingGetDenomStatsRequest().decode(payload); - const denomStats: TestingGetDenomStatsResponse = { - numKnown: 0, - numLost: 0, - numOffered: 0, - }; - await wex.db.runReadOnlyTx( - { storeNames: ["denominations"] }, - async (tx) => { - const denoms = - await tx.denominations.indexes.byExchangeBaseUrl.getAll( - req.exchangeBaseUrl, - ); - for (const d of denoms) { - denomStats.numKnown++; - if (d.isOffered) { - denomStats.numOffered++; - } - if (d.isLost) { - denomStats.numLost++; - } - } - }, - ); - return denomStats; + return handleTestingGetDenomStats(wex, req); } case WalletApiOperation.ListExchanges: { return await listExchanges(wex); @@ -979,20 +1306,7 @@ async function dispatchRequestInternal( case WalletApiOperation.ListExchangesForScopedCurrency: { const req = codecForListExchangesForScopedCurrencyRequest().decode(payload); - const exchangesResp = await listExchanges(wex); - const result: ExchangesShortListResponse = { - exchanges: [], - }; - // Right now we only filter on the currency, as wallet-core doesn't - // fully support scoped currencies yet. - for (const exch of exchangesResp.exchanges) { - if (exch.currency === req.scope.currency) { - result.exchanges.push({ - exchangeBaseUrl: exch.exchangeBaseUrl, - }); - } - } - return result; + return await handleListExchangesForScopedCurrency(wex, req); } case WalletApiOperation.GetExchangeDetailedInfo: { const req = codecForAddExchangeRequest().decode(payload); @@ -1004,13 +1318,11 @@ async function dispatchRequestInternal( } case WalletApiOperation.AddKnownBankAccounts: { const req = codecForAddKnownBankAccounts().decode(payload); - await addKnownBankAccounts(wex, req.payto, req.alias, req.currency); - return {}; + return await handleAddKnownBankAccount(wex, req); } case WalletApiOperation.ForgetKnownBankAccounts: { const req = codecForForgetKnownBankAccounts().decode(payload); - await forgetKnownBankAccounts(wex, req.payto); - return {}; + return await handleForgetKnownBankAccounts(wex, req); } case WalletApiOperation.GetWithdrawalDetailsForUri: { const req = codecForGetWithdrawalDetailsForUri().decode(payload); @@ -1018,48 +1330,16 @@ async function dispatchRequestInternal( } case WalletApiOperation.TestingGetReserveHistory: { const req = codecForTestingGetReserveHistoryRequest().decode(payload); - const reserve = await wex.db.runReadOnlyTx( - { storeNames: ["reserves"] }, - async (tx) => { - return tx.reserves.indexes.byReservePub.get(req.reservePub); - }, - ); - if (!reserve) { - throw Error("no reserve pub found"); - } - const sigResp = await wex.cryptoApi.signReserveHistoryReq({ - reservePriv: reserve.reservePriv, - startOffset: 0, - }); - const exchangeBaseUrl = req.exchangeBaseUrl; - const url = new URL( - `reserves/${req.reservePub}/history`, - exchangeBaseUrl, - ); - const resp = await wex.http.fetch(url.href, { - headers: { ["Taler-Reserve-History-Signature"]: sigResp.sig }, - }); - const historyJson = await readSuccessResponseJsonOrThrow( - resp, - codecForAny(), - ); - return historyJson; + return await handleTestingGetReserveHistory(wex, req); } case WalletApiOperation.AcceptManualWithdrawal: { const req = codecForAcceptManualWithdrawalRequest().decode(payload); - const res = await createManualWithdrawal(wex, { - amount: Amounts.parseOrThrow(req.amount), - exchangeBaseUrl: req.exchangeBaseUrl, - restrictAge: req.restrictAge, - forceReservePriv: req.forceReservePriv, - }); - return res; + return await handleAcceptManualWithdrawal(wex, req); } case WalletApiOperation.GetWithdrawalDetailsForAmount: { const req = codecForGetWithdrawalDetailsForAmountRequest().decode(payload); - const resp = await getWithdrawalDetailsForAmount(wex, cts, req); - return resp; + return await getWithdrawalDetailsForAmount(wex, cts, req); } case WalletApiOperation.GetBalances: { return await getBalances(wex); @@ -1080,12 +1360,6 @@ async function dispatchRequestInternal( const req = codecForUserAttentionsRequest().decode(payload); return await getUserAttentionsUnreadCount(wex, req); } - case WalletApiOperation.GetPendingOperations: { - // FIXME: Eventually remove the handler after deprecation period. - return { - pendingOperations: [], - } satisfies PendingOperationsResponse; - } case WalletApiOperation.SetExchangeTosAccepted: { const req = codecForAcceptExchangeTosRequest().decode(payload); await acceptExchangeTermsOfService(wex, req.exchangeBaseUrl); @@ -1118,27 +1392,11 @@ async function dispatchRequestInternal( } case WalletApiOperation.GetExchangeTos: { const req = codecForGetExchangeTosRequest().decode(payload); - return getExchangeTos( - wex, - req.exchangeBaseUrl, - req.acceptedFormat, - req.acceptLanguage, - ); + return await handleGetExchangeTos(wex, req); } case WalletApiOperation.GetContractTermsDetails: { const req = codecForGetContractTermsDetails().decode(payload); - if (req.proposalId) { - // FIXME: deprecated path - return getContractTermsDetails(wex, req.proposalId); - } - if (req.transactionId) { - const parsedTx = parseTransactionIdentifier(req.transactionId); - if (parsedTx?.tag === TransactionType.Payment) { - return getContractTermsDetails(wex, parsedTx.proposalId); - } - throw Error("transactionId is not a payment transaction"); - } - throw Error("transactionId missing"); + return handleGetContractTermsDetails(wex, req); } case WalletApiOperation.RetryPendingNow: { return handleRetryPendingNow(wex); @@ -1165,78 +1423,26 @@ async function dispatchRequestInternal( } case WalletApiOperation.GetQrCodesForPayto: { const req = codecForGetQrCodesForPaytoRequest().decode(payload); - return { - codes: getQrCodesForPayto(req.paytoUri), - }; + return handleGetQrCodesForPayto(wex, req); } case WalletApiOperation.ConfirmPay: { const req = codecForConfirmPayRequest().decode(payload); - let transactionId; - if (req.proposalId) { - // legacy client support - transactionId = constructTransactionIdentifier({ - tag: TransactionType.Payment, - proposalId: req.proposalId, - }); - } else if (req.transactionId) { - transactionId = req.transactionId; - } else { - throw Error("transactionId or (deprecated) proposalId required"); - } - return await confirmPay(wex, transactionId, req.sessionId); + return handleConfirmPay(wex, req); } case WalletApiOperation.AbortTransaction: { const req = codecForAbortTransaction().decode(payload); - await abortTransaction(wex, req.transactionId); - return {}; + return handleAbortTransaction(wex, req); } case WalletApiOperation.SuspendTransaction: { const req = codecForSuspendTransaction().decode(payload); - await suspendTransaction(wex, req.transactionId); - return {}; + return handleSuspendTransaction(wex, req); } case WalletApiOperation.GetActiveTasks: { - const allTasksId = (await getActiveTaskIds(wex.ws)).taskIds; - - const tasksInfo = await Promise.all( - allTasksId.map(async (id) => { - return await wex.db.runReadOnlyTx( - { storeNames: ["operationRetries"] }, - async (tx) => { - return tx.operationRetries.get(id); - }, - ); - }), - ); - - const tasks = allTasksId.map((taskId, i): ActiveTask => { - const transaction = convertTaskToTransactionId(taskId); - const d = tasksInfo[i]; - - const firstTry = !d - ? undefined - : timestampAbsoluteFromDb(d.retryInfo.firstTry); - const nextTry = !d - ? undefined - : timestampAbsoluteFromDb(d.retryInfo.nextRetry); - const counter = d?.retryInfo.retryCounter; - const lastError = d?.lastError; - - return { - taskId: taskId, - retryCounter: counter, - firstTry, - nextTry, - lastError, - transaction, - }; - }); - return { tasks }; + return await handleGetActiveTasks(wex, {}); } case WalletApiOperation.FailTransaction: { const req = codecForFailTransactionRequest().decode(payload); - await failTransaction(wex, req.transactionId); - return {}; + return await handleFailTransaction(wex, req); } case WalletApiOperation.ResumeTransaction: { const req = codecForResumeTransaction().decode(payload); @@ -1252,7 +1458,8 @@ async function dispatchRequestInternal( return {}; } case WalletApiOperation.TestingGetSampleTransactions: - return { transactions: sampleWalletCoreTransactions }; + const req = codecForEmptyObject().decode(payload); + return handleTestingGetSampleTransactions(wex, req); case WalletApiOperation.ForceRefresh: { const req = codecForForceRefreshRequest().decode(payload); return await forceRefresh(wex, req); @@ -1263,15 +1470,7 @@ async function dispatchRequestInternal( } case WalletApiOperation.StartRefundQuery: { const req = codecForStartRefundQueryRequest().decode(payload); - const txIdParsed = parseTransactionIdentifier(req.transactionId); - if (!txIdParsed) { - throw Error("invalid transaction ID"); - } - if (txIdParsed.tag !== TransactionType.Payment) { - throw Error("expected payment transaction ID"); - } - await startQueryRefund(wex, txIdParsed.proposalId); - return {}; + return handleStartRefundQuery(wex, req); } case WalletApiOperation.AddBackupProvider: { const req = codecForAddBackupProviderRequest().decode(payload); @@ -1279,8 +1478,7 @@ async function dispatchRequestInternal( } case WalletApiOperation.RunBackupCycle: { const req = codecForRunBackupCycle().decode(payload); - await runBackupCycle(wex, req); - return {}; + return handleAddBackupProvider(wex, req); } case WalletApiOperation.RemoveBackupProvider: { const req = codecForRemoveBackupProvider().decode(payload); @@ -1307,13 +1505,7 @@ async function dispatchRequestInternal( } case WalletApiOperation.HintNetworkAvailability: { const req = codecForHintNetworkAvailabilityRequest().decode(payload); - wex.ws.networkAvailable = req.isNetworkAvailable; - // When network becomes available, restart tasks as they're blocked - // waiting for the network. - // When network goes down, restart tasks so they notice the network - // is down and wait. - await restartAllRunningTasks(wex); - return {}; + return await handleHintNetworkAvailability(wex, req); } case WalletApiOperation.ConvertDepositAmount: { const req = codecForConvertAmountRequest.decode(payload); @@ -1385,61 +1577,11 @@ async function dispatchRequestInternal( case WalletApiOperation.GetDepositWireTypesForCurrency: { const req = codecForGetDepositWireTypesForCurrencyRequest().decode(payload); - const wtSet: Set = new Set(); - await wex.db.runReadOnlyTx( - { storeNames: ["exchanges", "exchangeDetails"] }, - async (tx) => { - const exchanges = await tx.exchanges.getAll(); - for (const exchange of exchanges) { - const det = await getExchangeWireDetailsInTx(tx, exchange.baseUrl); - if (!det) { - continue; - } - if (det.currency !== req.currency) { - continue; - } - for (const acc of det.wireInfo.accounts) { - let usable = true; - for (const dr of acc.debit_restrictions) { - if (dr.type === "deny") { - usable = false; - break; - } - } - if (!usable) { - break; - } - const parsedPayto = parsePaytoUri(acc.payto_uri); - if (!parsedPayto) { - continue; - } - wtSet.add(parsedPayto.targetType); - } - } - }, - ); - return { - wireTypes: [...wtSet], - }; + return handleGetDepositWireTypesForCurrency(wex, req); } case WalletApiOperation.ListGlobalCurrencyExchanges: { - const resp: ListGlobalCurrencyExchangesResponse = { - exchanges: [], - }; - await wex.db.runReadOnlyTx( - { storeNames: ["globalCurrencyExchanges"] }, - async (tx) => { - const gceList = await tx.globalCurrencyExchanges.iter().toArray(); - for (const gce of gceList) { - resp.exchanges.push({ - currency: gce.currency, - exchangeBaseUrl: gce.exchangeBaseUrl, - exchangeMasterPub: gce.exchangeMasterPub, - }); - } - }, - ); - return resp; + const req = codecForEmptyObject().decode(payload); + return await handleListGlobalCurrencyExchanges(wex, req); } case WalletApiOperation.ListGlobalCurrencyAuditors: { const resp: ListGlobalCurrencyAuditorsResponse = { -- cgit v1.2.3