taler-typescript-core

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

commit 33aafb0260a6c57fb13087c6a8cf30649e108004
parent 07d01ff9d0043b233ca5bfc7de3b93c654572fb7
Author: Florian Dold <florian@dold.me>
Date:   Mon,  9 Dec 2024 12:12:55 +0100

wallet-core: improve requests for handling bank accounts

Bank accounts known to the wallet now have a stable identifier.  The
"known" suffix has been dropped from requests. The wallet not emits
notifications when bank accounts change.

Diffstat:
Mpackages/bank-ui/src/hooks/bank-state.ts | 10+++++++---
Mpackages/bank-ui/src/stories.test.ts | 6+++---
Mpackages/taler-harness/src/integrationtests/test-known-accounts.ts | 28++++++++++++++--------------
Mpackages/taler-util/src/bank-api-client.ts | 13++++++-------
Mpackages/taler-util/src/index.ts | 7+++----
Mpackages/taler-util/src/notifications.ts | 14++++++++++++++
Mpackages/taler-util/src/types-taler-corebank.ts | 10++++++++--
Mpackages/taler-util/src/types-taler-wallet.ts | 68++++++++++++++++++++++++++++++++++++++++++++------------------------
Mpackages/taler-wallet-core/src/db.ts | 27++++++++++++++++++++++-----
Mpackages/taler-wallet-core/src/wallet-api-types.ts | 53++++++++++++++++++++++++++++++++---------------------
Mpackages/taler-wallet-core/src/wallet.ts | 139+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Mpackages/taler-wallet-core/src/withdraw.ts | 25++++++++++++++++++-------
Mpackages/taler-wallet-webextension/src/components/WalletActivity.tsx | 4+++-
Mpackages/taler-wallet-webextension/src/wallet/DepositPage/state.ts | 8++++----
Mpackages/taler-wallet-webextension/src/wallet/DepositPage/test.ts | 16+++++++++-------
Mpackages/taler-wallet-webextension/src/wallet/DestinationSelection/index.ts | 4++--
Mpackages/taler-wallet-webextension/src/wallet/DestinationSelection/state.ts | 6+++---
Mpackages/taler-wallet-webextension/src/wallet/DestinationSelection/views.tsx | 8++++----
Mpackages/taler-wallet-webextension/src/wallet/ManageAccount/index.ts | 6+++---
Mpackages/taler-wallet-webextension/src/wallet/ManageAccount/state.ts | 22+++++++++++-----------
Mpackages/taler-wallet-webextension/src/wallet/ManageAccount/stories.tsx | 21++++++++++++++-------
Mpackages/taler-wallet-webextension/src/wallet/ManageAccount/views.tsx | 30+++++++++++++++---------------
Mpackages/web-util/src/context/bank-api.ts | 14++++++++------
23 files changed, 348 insertions(+), 191 deletions(-)

diff --git a/packages/bank-ui/src/hooks/bank-state.ts b/packages/bank-ui/src/hooks/bank-state.ts @@ -24,11 +24,15 @@ import { codecForAny, codecForConstString, codecForString, - codecForTanTransmission, codecOptional, } from "@gnu-taler/taler-util"; -import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser"; -import { AppLocation } from "@gnu-taler/web-util/browser"; +import { + AppLocation, + buildStorageKey, + useLocalStorage, +} from "@gnu-taler/web-util/browser"; + +const { codecForTanTransmission } = TalerCorebankApi; export type ChallengeInProgess = | DeleteAccountChallenge diff --git a/packages/bank-ui/src/stories.test.ts b/packages/bank-ui/src/stories.test.ts @@ -20,8 +20,8 @@ */ import { AmountString, - TalerCorebankConfigResponse, setupI18n, + TalerCorebankApi, } from "@gnu-taler/taler-util"; import { BankApiProviderTesting, @@ -31,7 +31,7 @@ import * as tests from "@gnu-taler/web-util/testing"; import * as components from "./components/index.examples.js"; import * as pages from "./pages/index.stories.js"; -import { ComponentChildren, VNode, h as create } from "preact"; +import { ComponentChildren, h as create, VNode } from "preact"; // import { BankCoreApiProviderTesting } from "./context/config.js"; setupI18n("en", { en: {} }); @@ -54,7 +54,7 @@ describe("All the examples:", () => { }); function DefaultTestingContext(_props: { children: ComponentChildren }): VNode { - const cfg: TalerCorebankConfigResponse = { + const cfg: TalerCorebankApi.TalerCorebankConfigResponse = { name: "libeufin-bank", allow_deletions: true, bank_name: "taler bank", diff --git a/packages/taler-harness/src/integrationtests/test-known-accounts.ts b/packages/taler-harness/src/integrationtests/test-known-accounts.ts @@ -44,7 +44,7 @@ export async function runKnownAccountsTest(t: GlobalTestState) { }); const accts = await walletClient.call( - WalletApiOperation.ListKnownBankAccounts, + WalletApiOperation.ListBankAccounts, {}, ); @@ -52,15 +52,15 @@ export async function runKnownAccountsTest(t: GlobalTestState) { t.assertDeepEqual(accts.accounts.length, 1); - await walletClient.call(WalletApiOperation.AddKnownBankAccount, { + await walletClient.call(WalletApiOperation.AddBankAccount, { paytoUri: "payto://iban/FOOBAR", - alias: "Foo", + label: "Foo", currencies: ["EUR"], }); { const accts2 = await walletClient.call( - WalletApiOperation.ListKnownBankAccounts, + WalletApiOperation.ListBankAccounts, {}, ); console.log(`accounts after add: ${j2s(accts2)}`); @@ -68,40 +68,40 @@ export async function runKnownAccountsTest(t: GlobalTestState) { } // Can use add to update - await walletClient.call(WalletApiOperation.AddKnownBankAccount, { + const addResp1 = await walletClient.call(WalletApiOperation.AddBankAccount, { paytoUri: "payto://iban/FOOBAR", - alias: "Foo", + label: "Foo", currencies: ["CHF"], }); { const accts2 = await walletClient.call( - WalletApiOperation.ListKnownBankAccounts, + WalletApiOperation.ListBankAccounts, {}, ); console.log(`accounts after modify: ${j2s(accts2)}`); t.assertDeepEqual(accts2.accounts.length, 2); - const e = accts2.accounts.find((x) => x.alias == "Foo"); - t.assertDeepEqual(e?.currencies, ["CHF"]); + const e = accts2.accounts.find((x) => x.label == "Foo"); + t.assertDeepEqual(e?.currencies, ["CHF", "EUR"]); } // Test replacement - await walletClient.call(WalletApiOperation.AddKnownBankAccount, { - replacePaytoUri: "payto://iban/FOOBAR", + await walletClient.call(WalletApiOperation.AddBankAccount, { + replaceBankAccountId: addResp1.bankAccountId, paytoUri: "payto://iban/QUUX", - alias: "Foo", + label: "Foo", currencies: ["CHF"], }); { const accts2 = await walletClient.call( - WalletApiOperation.ListKnownBankAccounts, + WalletApiOperation.ListBankAccounts, {}, ); console.log(`accounts after replace: ${j2s(accts2)}`); t.assertDeepEqual(accts2.accounts.length, 2); - const e = accts2.accounts.find((x) => x.alias == "Foo"); + const e = accts2.accounts.find((x) => x.label == "Foo"); t.assertDeepEqual(e?.paytoUri, "payto://iban/QUUX"); } } diff --git a/packages/taler-util/src/bank-api-client.ts b/packages/taler-util/src/bank-api-client.ts @@ -22,7 +22,6 @@ * Imports. */ import { - AccountData, AmountString, base64FromArrayBuffer, buildCodecForObject, @@ -38,8 +37,8 @@ import { opKnownHttpFailure, opUnknownFailure, PaytoString, - RegisterAccountRequest, stringToBytes, + TalerCorebankApi, TalerError, TalerErrorCode, } from "@gnu-taler/taler-util"; @@ -259,7 +258,9 @@ export class TalerCorebankApiClient { logger.info(`result: ${j2s(res)}`); } - async registerAccountExtended(req: RegisterAccountRequest): Promise<void> { + async registerAccountExtended( + req: TalerCorebankApi.RegisterAccountRequest, + ): Promise<void> { const url = new URL("accounts", this.baseUrl); const resp = await this.httpLib.fetch(url.href, { method: "POST", @@ -324,10 +325,8 @@ export class TalerCorebankApiClient { }, }); // FIXME: Validate! - const acctInfo: AccountData = await readSuccessResponseJsonOrThrow( - infoResp, - codecForAny(), - ); + const acctInfo: TalerCorebankApi.AccountData = + await readSuccessResponseJsonOrThrow(infoResp, codecForAny()); return { password, username, diff --git a/packages/taler-util/src/index.ts b/packages/taler-util/src/index.ts @@ -23,9 +23,9 @@ export * from "./http-client/exchange.js"; export * from "./http-client/merchant.js"; export * from "./http-client/officer-account.js"; export { - CacheEvictor, - BasicOrTokenAuth, BasicAuth, + BasicOrTokenAuth, + CacheEvictor, TokenAuth, } from "./http-client/utils.js"; export * from "./http-status-codes.js"; @@ -64,7 +64,6 @@ export * from "./url.js"; // FIXME: remove all this, needs refactor export * from "./types-taler-bank-conversion.js"; export * from "./types-taler-bank-integration.js"; -export * from "./types-taler-corebank.js"; export * from "./types-taler-exchange.js"; export * from "./types-taler-merchant.js"; // end @@ -79,10 +78,10 @@ export * as TalerBankIntegrationApi from "./types-taler-bank-integration.js"; export * as ChallengerApi from "./types-taler-challenger.js"; export * as TalerCorebankApi from "./types-taler-corebank.js"; export * as TalerExchangeApi from "./types-taler-exchange.js"; +export * as TalerKycAml from "./types-taler-kyc-aml.js"; export * as TalerMerchantApi from "./types-taler-merchant.js"; export * as TalerRevenueApi from "./types-taler-revenue.js"; export * as TalerWireGatewayApi from "./types-taler-wire-gateway.js"; -export * as TalerKycAml from "./types-taler-kyc-aml.js"; export * from "./taler-signatures.js"; diff --git a/packages/taler-util/src/notifications.ts b/packages/taler-util/src/notifications.ts @@ -32,6 +32,7 @@ import { export enum NotificationType { BalanceChange = "balance-change", + BankAccountChange = "bank-account-change", BackupOperationError = "backup-error", TransactionStateTransition = "transaction-state-transition", ExchangeStateTransition = "exchange-state-transition", @@ -109,6 +110,18 @@ export interface ExchangeStateTransitionNotification { errorInfo?: ErrorInfoSummary; } +/** + * Transaction emitted when a bank account changes. + */ +export interface BankAccountChangeNotification { + type: NotificationType.BankAccountChange; + + /** + * ID of the affected bank account. + */ + bankAccountId: string; +} + export interface BalanceChangeNotification { type: NotificationType.BalanceChange; @@ -268,6 +281,7 @@ export interface IdleNotification { export type WalletNotification = | BalanceChangeNotification + | BankAccountChangeNotification | BackupOperationErrorNotification | ExchangeStateTransitionNotification | TransactionStateTransitionNotification diff --git a/packages/taler-util/src/types-taler-corebank.ts b/packages/taler-util/src/types-taler-corebank.ts @@ -147,14 +147,12 @@ export interface BankAccountCreateWithdrawalRequest { } export interface BankAccountConfirmWithdrawalRequest { - // Selected amount to be transferred. Optional if the // backend already knows the amount. // @since **v6** amount?: AmountString; } - export interface BankAccountCreateWithdrawalResponse { // ID of the withdrawal, can be used to view/modify the withdrawal operation. withdrawal_id: string; @@ -368,10 +366,16 @@ export interface PublicAccount { export interface ListBankAccountsResponse { accounts: AccountMinimalData[]; } + +export interface GetBankAccountByIdResponse { + account: AccountMinimalData; +} + export interface Balance { amount: AmountString; credit_debit_indicator: "credit" | "debit"; } + export interface AccountMinimalData { // Username username: string; @@ -576,6 +580,7 @@ export interface MonitorNoConversion { // exchange to another bank account. talerOutVolume: AmountString; } + // Monitoring stats when conversion is supported export interface MonitorWithConversion { type: "with-conversions"; @@ -620,6 +625,7 @@ export interface MonitorWithConversion { // exchange to another bank account. talerOutVolume: AmountString; } + export interface TanTransmission { // Channel of the last successful transmission of the TAN challenge. tan_channel: TanChannel; diff --git a/packages/taler-util/src/types-taler-wallet.ts b/packages/taler-util/src/types-taler-wallet.ts @@ -1146,7 +1146,9 @@ export interface WalletCoreVersion { devMode: boolean; } -export interface KnownBankAccountsInfo { +export interface KnownBankAccountInfo { + bankAccountId: string; + paytoUri: string; /** @@ -1159,13 +1161,25 @@ export interface KnownBankAccountsInfo { */ currencies: string[] | undefined; - alias: string | undefined; + label: string | undefined; +} + +export interface ListBankAccountsResponse { + accounts: KnownBankAccountInfo[]; } -export interface ListKnownBankAccountsResponse { - accounts: KnownBankAccountsInfo[]; +export type GetBankAccountByIdResponse = KnownBankAccountInfo; + +export interface GetBankAccountByIdRequest { + bankAccountId: string; } +export const codecForGetBankAccountByIdRequest = + (): Codec<GetBankAccountByIdRequest> => + buildCodecForObject<GetBankAccountByIdRequest>() + .property("bankAccountId", codecForString()) + .build("GetBankAccountByIdRequest"); + /** * Wire fee for one wire method */ @@ -2012,26 +2026,26 @@ export const codecForGetWithdrawalDetailsForUri = .property("restrictAge", codecOptional(codecForNumber())) .build("GetWithdrawalDetailsForUriRequest"); -export interface ListKnownBankAccountsRequest { +export interface ListBankAccountsRequest { currency?: string; } export const codecForListKnownBankAccounts = - (): Codec<ListKnownBankAccountsRequest> => - buildCodecForObject<ListKnownBankAccountsRequest>() + (): Codec<ListBankAccountsRequest> => + buildCodecForObject<ListBankAccountsRequest>() .property("currency", codecOptional(codecForString())) .build("ListKnownBankAccountsRequest"); -export interface AddKnownBankAccountRequest { +export interface AddBankAccountRequest { /** * Payto URI of the bank account that should be added. */ paytoUri: string; /** - * Human-readable alias / label for the account. + * Human-readable label for the account. */ - alias: string; + label: string; /** * Currencies supported by the bank (if known). @@ -2041,26 +2055,32 @@ export interface AddKnownBankAccountRequest { /** * Bank account that this new account should replace. */ - replacePaytoUri?: string; + replaceBankAccountId?: string; } -export const codecForAddKnownBankAccounts = - (): Codec<AddKnownBankAccountRequest> => - buildCodecForObject<AddKnownBankAccountRequest>() - .property("replacePaytoUri", codecOptional(codecForString())) - .property("paytoUri", codecForString()) - .property("alias", codecForString()) - .property("currencies", codecOptional(codecForList(codecForString()))) - .build("AddKnownBankAccountsRequest"); +export interface AddBankAccountResponse { + /** + * Identifier of the added bank account. + */ + bankAccountId: string; +} -export interface ForgetKnownBankAccountRequest { - paytoUri: string; +export const codecForAddKnownBankAccounts = (): Codec<AddBankAccountRequest> => + buildCodecForObject<AddBankAccountRequest>() + .property("replaceBankAccountId", codecOptional(codecForString())) + .property("paytoUri", codecForString()) + .property("label", codecForString()) + .property("currencies", codecOptional(codecForList(codecForString()))) + .build("AddKnownBankAccountsRequest"); + +export interface ForgetBankAccountRequest { + bankAccountId: string; } export const codecForForgetKnownBankAccounts = - (): Codec<ForgetKnownBankAccountRequest> => - buildCodecForObject<ForgetKnownBankAccountRequest>() - .property("paytoUri", codecForString()) + (): Codec<ForgetBankAccountRequest> => + buildCodecForObject<ForgetBankAccountRequest>() + .property("bankAccountId", codecForString()) .build("ForgetKnownBankAccountsRequest"); export interface GetContractTermsDetailsRequest { diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts @@ -2925,13 +2925,22 @@ export const WalletStoresV1 = { byStatus: describeIndex("byStatus", "status"), }, ), - bankAccounts: describeStore( + _obsolete_bankAccounts: describeStore( "bankAccounts", - describeContents<BankAccountsRecord>({ + describeContents<any>({ keyPath: "uri", }), {}, ), + bankAccountsV2: describeStore( + "bankAccountsV2", + describeContents<BankAccountsRecord>({ + keyPath: "bankAccountId", + }), + { + byPaytoUri: describeIndex("byPaytoUri", "paytoUri"), + }, + ), contractTerms: describeStore( "contractTerms", describeContents<ContractTermsRecord>({ @@ -3026,9 +3035,19 @@ export interface FixupRecord { */ export interface BankAccountsRecord { /** + * Opaque identifier for the bank account. + */ + bankAccountId: string; + + /** * Payto URI of the bank account. */ - uri: string; + paytoUri: string; + + /** + * User-defined label for the account. + */ + label: string | undefined; currencies: string[] | undefined; @@ -3036,8 +3055,6 @@ export interface BankAccountsRecord { * FIXME: Provide more info here. */ kycCompleted: boolean; - - alias: string | undefined; } export interface MetaConfigRecord { diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts @@ -32,10 +32,11 @@ import { AcceptPeerPullPaymentResponse, AcceptPeerPushPaymentResponse, AcceptWithdrawalResponse, + AddBankAccountRequest, + AddBankAccountResponse, AddExchangeRequest, AddGlobalCurrencyAuditorRequest, AddGlobalCurrencyExchangeRequest, - AddKnownBankAccountRequest, AmountResponse, ApplyDevExperimentRequest, BackupRecovery, @@ -70,9 +71,11 @@ import { ExportDbToFileResponse, FailTransactionRequest, ForceRefreshRequest, - ForgetKnownBankAccountRequest, + ForgetBankAccountRequest, GetActiveTasksResponse, GetBalanceDetailRequest, + GetBankAccountByIdRequest, + GetBankAccountByIdResponse, GetBankingChoicesForPaytoRequest, GetBankingChoicesForPaytoResponse, GetContractTermsDetailsRequest, @@ -107,11 +110,11 @@ import { IntegrationTestV2Args, ListAssociatedRefreshesRequest, ListAssociatedRefreshesResponse, + ListBankAccountsRequest, + ListBankAccountsResponse, ListExchangesRequest, ListGlobalCurrencyAuditorsResponse, ListGlobalCurrencyExchangesResponse, - ListKnownBankAccountsRequest, - ListKnownBankAccountsResponse, PrepareBankIntegratedWithdrawalRequest, PrepareBankIntegratedWithdrawalResponse, PreparePayRequest, @@ -193,9 +196,10 @@ export enum WalletApiOperation { TestingGetSampleTransactions = "testingGetSampleTransactions", ListExchanges = "listExchanges", GetExchangeEntryByUrl = "getExchangeEntryByUrl", - ListKnownBankAccounts = "listKnownBankAccounts", - AddKnownBankAccount = "addKnownBankAccount", - ForgetKnownBankAccount = "forgetKnownBankAccount", + ListBankAccounts = "listBankAccounts", + GetBankAccountById = "getBankAccountById", + AddBankAccount = "addBankAccount", + ForgetBankAccount = "forgetBankAccount", GetWithdrawalDetailsForUri = "getWithdrawalDetailsForUri", GetWithdrawalDetailsForAmount = "getWithdrawalDetailsForAmount", AcceptManualWithdrawal = "acceptManualWithdrawal", @@ -707,21 +711,27 @@ export type UpdateExchangeEntryOp = { response: EmptyObject; }; -export type ListKnownBankAccountsOp = { - op: WalletApiOperation.ListKnownBankAccounts; - request: ListKnownBankAccountsRequest; - response: ListKnownBankAccountsResponse; +export type ListBankAccountsOp = { + op: WalletApiOperation.ListBankAccounts; + request: ListBankAccountsRequest; + response: ListBankAccountsResponse; }; -export type AddKnownBankAccountsOp = { - op: WalletApiOperation.AddKnownBankAccount; - request: AddKnownBankAccountRequest; - response: EmptyObject; +export type GetBankAccountByIdOp = { + op: WalletApiOperation.GetBankAccountById; + request: GetBankAccountByIdRequest; + response: GetBankAccountByIdResponse; +}; + +export type AddBankAccountsOp = { + op: WalletApiOperation.AddBankAccount; + request: AddBankAccountRequest; + response: AddBankAccountResponse; }; -export type ForgetKnownBankAccountsOp = { - op: WalletApiOperation.ForgetKnownBankAccount; - request: ForgetKnownBankAccountRequest; +export type ForgetBankAccountsOp = { + op: WalletApiOperation.ForgetBankAccount; + request: ForgetBankAccountRequest; response: EmptyObject; }; @@ -1357,9 +1367,10 @@ export type WalletOperations = { [WalletApiOperation.AcceptManualWithdrawal]: AcceptManualWithdrawalOp; [WalletApiOperation.ListExchanges]: ListExchangesOp; [WalletApiOperation.AddExchange]: AddExchangeOp; - [WalletApiOperation.ListKnownBankAccounts]: ListKnownBankAccountsOp; - [WalletApiOperation.AddKnownBankAccount]: AddKnownBankAccountsOp; - [WalletApiOperation.ForgetKnownBankAccount]: ForgetKnownBankAccountsOp; + [WalletApiOperation.ListBankAccounts]: ListBankAccountsOp; + [WalletApiOperation.AddBankAccount]: AddBankAccountsOp; + [WalletApiOperation.ForgetBankAccount]: ForgetBankAccountsOp; + [WalletApiOperation.GetBankAccountById]: GetBankAccountByIdOp; [WalletApiOperation.SetExchangeTosAccepted]: SetExchangeTosAcceptedOp; [WalletApiOperation.SetExchangeTosForgotten]: SetExchangeTosForgottenOp; [WalletApiOperation.GetExchangeTos]: GetExchangeTosOp; diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts @@ -35,10 +35,11 @@ import { AcceptManualWithdrawalResult, AcceptWithdrawalResponse, ActiveTask, + AddBankAccountRequest, + AddBankAccountResponse, AddExchangeRequest, AddGlobalCurrencyAuditorRequest, AddGlobalCurrencyExchangeRequest, - AddKnownBankAccountRequest, AmountJson, AmountString, Amounts, @@ -60,8 +61,10 @@ import { ExportDbToFileRequest, ExportDbToFileResponse, FailTransactionRequest, - ForgetKnownBankAccountRequest, + ForgetBankAccountRequest, GetActiveTasksResponse, + GetBankAccountByIdRequest, + GetBankAccountByIdResponse, GetBankingChoicesForPaytoRequest, GetBankingChoicesForPaytoResponse, GetContractTermsDetailsRequest, @@ -78,11 +81,11 @@ import { InitResponse, IntegrationTestArgs, IntegrationTestV2Args, - KnownBankAccountsInfo, + KnownBankAccountInfo, + ListBankAccountsRequest, + ListBankAccountsResponse, ListGlobalCurrencyAuditorsResponse, ListGlobalCurrencyExchangesResponse, - ListKnownBankAccountsRequest, - ListKnownBankAccountsResponse, Logger, NotificationType, ObservabilityContext, @@ -155,6 +158,7 @@ import { codecForForceRefreshRequest, codecForForgetKnownBankAccounts, codecForGetBalanceDetailRequest, + codecForGetBankAccountByIdRequest, codecForGetBankingChoicesForPaytoRequest, codecForGetContractTermsDetails, codecForGetCurrencyInfoRequest, @@ -207,8 +211,10 @@ import { codecForUserAttentionsRequest, codecForValidateIbanRequest, codecForWithdrawTestBalance, + encodeCrock, getErrorDetailFromException, getQrCodesForPayto, + getRandomBytes, j2s, openPromise, parsePaytoUri, @@ -504,21 +510,22 @@ export async function getDenomInfo( */ async function handleListKnownBankAccounts( wex: WalletExecutionContext, - req: ListKnownBankAccountsRequest, -): Promise<ListKnownBankAccountsResponse> { - const accounts: KnownBankAccountsInfo[] = []; + req: ListBankAccountsRequest, +): Promise<ListBankAccountsResponse> { + const accounts: KnownBankAccountInfo[] = []; const currency = req.currency; - await wex.db.runReadOnlyTx({ storeNames: ["bankAccounts"] }, async (tx) => { - const knownAccounts = await tx.bankAccounts.iter().toArray(); + await wex.db.runReadOnlyTx({ storeNames: ["bankAccountsV2"] }, async (tx) => { + const knownAccounts = await tx.bankAccountsV2.iter().toArray(); for (const r of knownAccounts) { if (currency && r.currencies && !r.currencies.includes(currency)) { continue; } - const payto = parsePaytoUri(r.uri); + const payto = parsePaytoUri(r.paytoUri); if (payto) { accounts.push({ - paytoUri: r.uri, - alias: r.alias, + bankAccountId: r.bankAccountId, + paytoUri: r.paytoUri, + label: r.label, kycCompleted: r.kycCompleted, currencies: r.currencies, }); @@ -527,18 +534,43 @@ async function handleListKnownBankAccounts( }); return { accounts }; } + +async function handleGetBankAccountById( + wex: WalletExecutionContext, + req: GetBankAccountByIdRequest, +): Promise<GetBankAccountByIdResponse> { + const acct = await wex.db.runReadOnlyTx( + { storeNames: ["bankAccountsV2"] }, + async (tx) => { + return tx.bankAccountsV2.get(req.bankAccountId); + }, + ); + if (!acct) { + throw Error(`bank account ${req.bankAccountId} not found`); + } + return acct; +} + /** + * Remove a known bank account. */ async function forgetKnownBankAccounts( wex: WalletExecutionContext, - payto: string, + bankAccountId: string, ): Promise<void> { - await wex.db.runReadWriteTx({ storeNames: ["bankAccounts"] }, async (tx) => { - const account = await tx.bankAccounts.get(payto); - if (!account) { - throw Error(`account not found: ${payto}`); - } - tx.bankAccounts.delete(account.uri); + await wex.db.runReadWriteTx( + { storeNames: ["bankAccountsV2"] }, + async (tx) => { + const account = await tx.bankAccountsV2.get(bankAccountId); + if (!account) { + throw Error(`account not found: ${bankAccountId}`); + } + tx.bankAccountsV2.delete(account.bankAccountId); + }, + ); + wex.ws.notify({ + type: NotificationType.BankAccountChange, + bankAccountId, }); return; } @@ -943,27 +975,54 @@ async function handleTestingGetDenomStats( async function handleAddKnownBankAccount( wex: WalletExecutionContext, - req: AddKnownBankAccountRequest, -): Promise<EmptyObject> { - await wex.db.runReadWriteTx({ storeNames: ["bankAccounts"] }, async (tx) => { - if (req.replacePaytoUri) { - await tx.bankAccounts.delete(req.replacePaytoUri); - } - await tx.bankAccounts.put({ - uri: req.paytoUri, - alias: req.alias, - currencies: req.currencies, - kycCompleted: false, - }); + req: AddBankAccountRequest, +): Promise<AddBankAccountResponse> { + const acctId = await wex.db.runReadWriteTx( + { storeNames: ["bankAccountsV2"] }, + async (tx) => { + let currencies = req.currencies; + let myId: string; + const oldAcct = await tx.bankAccountsV2.indexes.byPaytoUri.get( + req.paytoUri, + ); + if (req.replaceBankAccountId) { + myId = req.replaceBankAccountId; + } else if (oldAcct) { + myId = oldAcct.bankAccountId; + currencies = [ + ...new Set([ + ...(req.currencies ?? []), + ...(oldAcct.currencies ?? []), + ]), + ]; + } else { + // New Account! + myId = `acct:${encodeCrock(getRandomBytes(32))}`; + } + await tx.bankAccountsV2.put({ + bankAccountId: myId, + paytoUri: req.paytoUri, + label: req.label, + currencies, + kycCompleted: false, + }); + return myId; + }, + ); + wex.ws.notify({ + type: NotificationType.BankAccountChange, + bankAccountId: acctId, }); - return {}; + return { + bankAccountId: acctId, + }; } async function handleForgetKnownBankAccounts( wex: WalletExecutionContext, - req: ForgetKnownBankAccountRequest, + req: ForgetBankAccountRequest, ): Promise<EmptyObject> { - await forgetKnownBankAccounts(wex, req.paytoUri); + await forgetKnownBankAccounts(wex, req.bankAccountId); return {}; } @@ -1673,15 +1732,19 @@ const handlers: { [T in WalletApiOperation]: HandlerWithValidator<T> } = { codec: codecForAddExchangeRequest(), handler: (wex, req) => getExchangeDetailedInfo(wex, req.exchangeBaseUrl), }, - [WalletApiOperation.ListKnownBankAccounts]: { + [WalletApiOperation.ListBankAccounts]: { codec: codecForListKnownBankAccounts(), handler: handleListKnownBankAccounts, }, - [WalletApiOperation.AddKnownBankAccount]: { + [WalletApiOperation.GetBankAccountById]: { + codec: codecForGetBankAccountByIdRequest(), + handler: handleGetBankAccountById, + }, + [WalletApiOperation.AddBankAccount]: { codec: codecForAddKnownBankAccounts(), handler: handleAddKnownBankAccount, }, - [WalletApiOperation.ForgetKnownBankAccount]: { + [WalletApiOperation.ForgetBankAccount]: { codec: codecForForgetKnownBankAccounts(), handler: handleForgetKnownBankAccounts, }, diff --git a/packages/taler-wallet-core/src/withdraw.ts b/packages/taler-wallet-core/src/withdraw.ts @@ -3491,12 +3491,13 @@ export async function confirmWithdrawal( throw Error("bank account not acceptable by the exchange"); } - await wex.db.runReadWriteTx( + const bankAccountId = await wex.db.runReadWriteTx( { - storeNames: ["bankAccounts"], + storeNames: ["bankAccountsV2"], }, async (tx) => { - const existingAccount = await tx.bankAccounts.get(senderWire); + const existingAccount = + await tx.bankAccountsV2.indexes.byPaytoUri.get(senderWire); if (existingAccount) { // Add currency for existing known bank account if necessary if (existingAccount.currencies?.includes(instructedAmount.currency)) { @@ -3505,19 +3506,29 @@ export async function confirmWithdrawal( ...(existingAccount.currencies ?? []), ]; existingAccount.currencies.sort(); - await tx.bankAccounts.put(existingAccount); + await tx.bankAccountsV2.put(existingAccount); } return; } - await tx.bankAccounts.put({ + const myId = `acct:${encodeCrock(getRandomBytes(32))}`; + await tx.bankAccountsV2.put({ currencies: [instructedAmount.currency], kycCompleted: false, - uri: senderWire, - alias: undefined, + paytoUri: senderWire, + bankAccountId: myId, + label: undefined, }); + return myId; }, ); + + if (bankAccountId) { + wex.ws.notify({ + type: NotificationType.BankAccountChange, + bankAccountId, + }); + } } const ctx = new WithdrawTransactionContext( diff --git a/packages/taler-wallet-webextension/src/components/WalletActivity.tsx b/packages/taler-wallet-webextension/src/components/WalletActivity.tsx @@ -37,9 +37,9 @@ import { TextField } from "../mui/TextField.js"; import { SafeHandler } from "../mui/handlers.js"; import { WxApiType } from "../wxApi.js"; import { WalletActivityTrack } from "../wxBackend.js"; +import { Checkbox } from "./Checkbox.js"; import { Modal } from "./Modal.js"; import { Time } from "./Time.js"; -import { Checkbox } from "./Checkbox.js"; const OPEN_ACTIVITY_HEIGHT_PX = 250; const CLOSE_ACTIVITY_HEIGHT_PX = 40; @@ -848,6 +848,8 @@ export function ObservabilityEventsTable(): VNode { (not.events[0] as RequestProgressNotification) .requestId })`; + case NotificationType.BankAccountChange: + return i18n.str`Bank account info changed`; default: { assertUnreachable(not.type); } diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts b/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts @@ -16,7 +16,7 @@ import { Amounts, - KnownBankAccountsInfo, + KnownBankAccountInfo, TransactionAmountMode, } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; @@ -46,7 +46,7 @@ export function useComponentState({ ); const { accounts } = await api.wallet.call( - WalletApiOperation.ListKnownBankAccounts, + WalletApiOperation.ListBankAccounts, { currency: scope.currency }, ); @@ -265,12 +265,12 @@ export function labelForAccountType(id: string): string { } export function createLabelsForBankAccount( - knownBankAccounts: Array<KnownBankAccountsInfo>, + knownBankAccounts: Array<KnownBankAccountInfo>, ): { [value: string]: string } { const initialList: Record<string, string> = {}; if (!knownBankAccounts.length) return initialList; return knownBankAccounts.reduce((prev, cur) => { - prev[cur.paytoUri] = cur.alias ?? "??"; + prev[cur.paytoUri] = cur.label ?? "??"; return prev; }, initialList); } diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts b/packages/taler-wallet-webextension/src/wallet/DepositPage/test.ts @@ -74,7 +74,7 @@ describe("DepositPage states", () => { ], }); handler.addWalletCallResponse( - WalletApiOperation.ListKnownBankAccounts, + WalletApiOperation.ListBankAccounts, undefined, { accounts: [], @@ -121,7 +121,7 @@ describe("DepositPage states", () => { ], }); handler.addWalletCallResponse( - WalletApiOperation.ListKnownBankAccounts, + WalletApiOperation.ListBankAccounts, undefined, { accounts: [], @@ -147,16 +147,18 @@ describe("DepositPage states", () => { }); const ibanPayto = { + bankAccountId: "acct:123", paytoUri: "payto://iban/ES8877998399652238", kycCompleted: false, currencies: ["EUR"], - alias: "my iban account", + label: "my iban account", }; const talerBankPayto = { + bankAccountId: "acct:456", paytoUri: "payto://x-taler-bank/ES8877998399652238", kycCompleted: false, currencies: ["EUR"], - alias: "my taler account", + label: "my taler account", }; it("should have status 'ready' but unable to deposit ", async () => { @@ -181,7 +183,7 @@ describe("DepositPage states", () => { ], }); handler.addWalletCallResponse( - WalletApiOperation.ListKnownBankAccounts, + WalletApiOperation.ListBankAccounts, undefined, { accounts: [ibanPayto], @@ -241,7 +243,7 @@ describe("DepositPage states", () => { ], }); handler.addWalletCallResponse( - WalletApiOperation.ListKnownBankAccounts, + WalletApiOperation.ListBankAccounts, undefined, { accounts: [talerBankPayto, ibanPayto], @@ -331,7 +333,7 @@ describe("DepositPage states", () => { ], }); handler.addWalletCallResponse( - WalletApiOperation.ListKnownBankAccounts, + WalletApiOperation.ListBankAccounts, undefined, { accounts: [talerBankPayto, ibanPayto], diff --git a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/index.ts b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/index.ts @@ -15,7 +15,7 @@ */ import { - KnownBankAccountsInfo, + KnownBankAccountInfo, PaytoUri, ScopeInfo, } from "@gnu-taler/taler-util"; @@ -72,7 +72,7 @@ export namespace State { error: undefined; type: Props["type"]; onSelectAccount: (p: string) => void; - previous: KnownBankAccountsInfo[]; + previous: KnownBankAccountInfo[]; goToBank: ButtonHandler; goToWallet: ButtonHandler; } diff --git a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/state.ts b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/state.ts @@ -16,7 +16,7 @@ import { ExchangeUpdateStatus, - KnownBankAccountsInfo, + KnownBankAccountInfo, PaytoUri, ScopeType, parseScopeInfoShort, @@ -40,13 +40,13 @@ export function useComponentState(props: Props): RecursiveState<State> { const hook = useAsyncAsHook(async () => { const resp = await api.wallet.call( - WalletApiOperation.ListKnownBankAccounts, + WalletApiOperation.ListBankAccounts, {}, ); return resp }); - const previous: KnownBankAccountsInfo[] = props.type === "send" && hook && !hook.hasError ? hook.response.accounts : []; + const previous: KnownBankAccountInfo[] = props.type === "send" && hook && !hook.hasError ? hook.response.accounts : []; if (!scope) { return () => { diff --git a/packages/taler-wallet-webextension/src/wallet/DestinationSelection/views.tsx b/packages/taler-wallet-webextension/src/wallet/DestinationSelection/views.tsx @@ -15,7 +15,7 @@ */ import { - KnownBankAccountsInfo, + KnownBankAccountInfo, parsePaytoUri, stringifyPaytoUri, } from "@gnu-taler/taler-util"; @@ -355,7 +355,7 @@ function RowExample({ disabled, onClick, }: { - info: KnownBankAccountsInfo; + info: KnownBankAccountInfo; disabled?: boolean; onClick?: () => void; }): VNode { @@ -364,7 +364,7 @@ function RowExample({ <MediaLeft> <CircleDiv> <SvgIcon - title={info.alias} + title={info.label} dangerouslySetInnerHTML={{ __html: bankIcon, }} @@ -373,7 +373,7 @@ function RowExample({ </CircleDiv> </MediaLeft> <MediaBody> - <span>{info.alias}</span> + <span>{info.label}</span> <LightText>{describeAccount(info.paytoUri)}</LightText> </MediaBody> <MediaRight> diff --git a/packages/taler-wallet-webextension/src/wallet/ManageAccount/index.ts b/packages/taler-wallet-webextension/src/wallet/ManageAccount/index.ts @@ -14,7 +14,7 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { KnownBankAccountsInfo, ScopeInfo } from "@gnu-taler/taler-util"; +import { KnownBankAccountInfo, ScopeInfo } from "@gnu-taler/taler-util"; import { ErrorAlertView } from "../../components/CurrentAlerts.js"; import { Loading } from "../../components/Loading.js"; import { ErrorAlert } from "../../context/alert.js"; @@ -59,12 +59,12 @@ export namespace State { onAccountAdded: ButtonHandler; onCancel: ButtonHandler; accountByType: AccountByType; - deleteAccount: (a: KnownBankAccountsInfo) => Promise<void>; + deleteAccount: (a: KnownBankAccountInfo) => Promise<void>; } } export type AccountByType = { - [key: string]: KnownBankAccountsInfo[]; + [key: string]: KnownBankAccountInfo[]; }; const viewMapping: StateViewMap<State> = { diff --git a/packages/taler-wallet-webextension/src/wallet/ManageAccount/state.ts b/packages/taler-wallet-webextension/src/wallet/ManageAccount/state.ts @@ -15,7 +15,7 @@ */ import { - KnownBankAccountsInfo, + KnownBankAccountInfo, parsePaytoUri, stringifyPaytoUri, } from "@gnu-taler/taler-util"; @@ -37,7 +37,7 @@ export function useComponentState({ const { i18n } = useTranslationContext(); const hook = useAsyncAsHook(() => - api.wallet.call(WalletApiOperation.ListKnownBankAccounts, { + api.wallet.call(WalletApiOperation.ListBankAccounts, { currency: scope.currency, }), ); @@ -92,7 +92,7 @@ export function useComponentState({ } const [payto, setPayto] = useState(""); - const [alias, setAlias] = useState(""); + const [label, setLabel] = useState(""); const [type, setType] = useState(hook2.response.wireTypes[0]); const accountType: Record<string, string> = {}; @@ -114,8 +114,8 @@ export function useComponentState({ if (!uri || found) return; const normalizedPayto = stringifyPaytoUri(uri); - await api.wallet.call(WalletApiOperation.AddKnownBankAccount, { - alias, + await api.wallet.call(WalletApiOperation.AddBankAccount, { + label, currencies: [scope.currency], paytoUri: normalizedPayto, }); @@ -125,7 +125,7 @@ export function useComponentState({ const paytoUriError = found ? "that account is already present" : undefined; const unableToAdd = - !type || !alias || paytoUriError !== undefined || uri === undefined; + !type || !label || paytoUriError !== undefined || uri === undefined; const accountByType: AccountByType = { iban: [], @@ -138,10 +138,10 @@ export function useComponentState({ accountByType[p.targetType].push(acc); }); - async function deleteAccount(account: KnownBankAccountsInfo): Promise<void> { + async function deleteAccount(account: KnownBankAccountInfo): Promise<void> { const payto = account.paytoUri; - await api.wallet.call(WalletApiOperation.ForgetKnownBankAccount, { - paytoUri: payto, + await api.wallet.call(WalletApiOperation.ForgetBankAccount, { + bankAccountId: account.bankAccountId, }); hook?.retry(); } @@ -158,9 +158,9 @@ export function useComponentState({ }), }, alias: { - value: alias, + value: label, onInput: pushAlertOnError(async (v) => { - setAlias(v); + setLabel(v); }), }, uri: { diff --git a/packages/taler-wallet-webextension/src/wallet/ManageAccount/stories.tsx b/packages/taler-wallet-webextension/src/wallet/ManageAccount/stories.tsx @@ -52,7 +52,8 @@ export const JustTwoBitcoinAccounts = tests.createExample(ReadyView, { "x-taler-bank": [], bitcoin: [ { - alias: "my bitcoin addr", + bankAccountId: "acct:1", + label: "my bitcoin addr", currencies: ["BTC"], kycCompleted: false, paytoUri: stringifyPaytoUri({ @@ -65,7 +66,8 @@ export const JustTwoBitcoinAccounts = tests.createExample(ReadyView, { }), }, { - alias: "my other addr", + bankAccountId: "acct:2", + label: "my other addr", currencies: ["BTC"], kycCompleted: true, paytoUri: stringifyPaytoUri({ @@ -105,7 +107,8 @@ export const WithAllTypeOfAccounts = tests.createExample(ReadyView, { accountByType: { iban: [ { - alias: "my bank", + bankAccountId: "acct:1", + label: "my bank", currencies: ["ARS"], kycCompleted: true, paytoUri: stringifyPaytoUri({ @@ -119,7 +122,8 @@ export const WithAllTypeOfAccounts = tests.createExample(ReadyView, { ], "x-taler-bank": [ { - alias: "my xtaler bank", + bankAccountId: "acct:2", + label: "my xtaler bank", currencies: ["ARS"], kycCompleted: true, paytoUri: stringifyPaytoUri({ @@ -134,7 +138,8 @@ export const WithAllTypeOfAccounts = tests.createExample(ReadyView, { ], bitcoin: [ { - alias: "my bitcoin addr", + bankAccountId: "acct:1", + label: "my bitcoin addr", currencies: ["BTC"], kycCompleted: false, paytoUri: stringifyPaytoUri({ @@ -147,7 +152,8 @@ export const WithAllTypeOfAccounts = tests.createExample(ReadyView, { }), }, { - alias: "my other addr", + bankAccountId: "acct:2", + label: "my other addr", currencies: ["BTC"], kycCompleted: true, paytoUri: stringifyPaytoUri({ @@ -187,7 +193,8 @@ export const AddingIbanAccount = tests.createExample(ReadyView, { accountByType: { iban: [ { - alias: "my bank", + bankAccountId: "acct:1", + label: "my bank", currencies: ["ARS"], kycCompleted: true, paytoUri: stringifyPaytoUri({ diff --git a/packages/taler-wallet-webextension/src/wallet/ManageAccount/views.tsx b/packages/taler-wallet-webextension/src/wallet/ManageAccount/views.tsx @@ -16,7 +16,7 @@ import { buildPayto, - KnownBankAccountsInfo, + KnownBankAccountInfo, parsePaytoUri, PaytoUriBitcoin, PaytoUriIBAN, @@ -45,8 +45,8 @@ type ComponentFormByAccountType = { type ComponentListByAccountType = { [type in AccountType]: (props: { - list: KnownBankAccountsInfo[]; - onDelete: (a: KnownBankAccountsInfo) => Promise<void>; + list: KnownBankAccountInfo[]; + onDelete: (a: KnownBankAccountInfo) => Promise<void>; }) => VNode; }; @@ -180,8 +180,8 @@ function IbanTable({ list, onDelete, }: { - list: KnownBankAccountsInfo[]; - onDelete: (ac: KnownBankAccountsInfo) => void; + list: KnownBankAccountInfo[]; + onDelete: (ac: KnownBankAccountInfo) => void; }): VNode { const { i18n } = useTranslationContext(); if (list.length === 0) return <Fragment />; @@ -215,8 +215,8 @@ function IbanTable({ {list.map((account) => { const p = parsePaytoUri(account.paytoUri) as PaytoUriIBAN; return ( - <tr key={account.alias}> - <td>{account.alias}</td> + <tr key={account.label}> + <td>{account.label}</td> <td>{p.bic}</td> <td>{p.iban}</td> <td>{p.params["receiver-name"]}</td> @@ -259,8 +259,8 @@ function TalerBankTable({ list, onDelete, }: { - list: KnownBankAccountsInfo[]; - onDelete: (ac: KnownBankAccountsInfo) => void; + list: KnownBankAccountInfo[]; + onDelete: (ac: KnownBankAccountInfo) => void; }): VNode { const { i18n } = useTranslationContext(); if (list.length === 0) return <Fragment />; @@ -291,8 +291,8 @@ function TalerBankTable({ {list.map((account) => { const p = parsePaytoUri(account.paytoUri) as PaytoUriTalerBank; return ( - <tr key={account.alias}> - <td>{account.alias}</td> + <tr key={account.label}> + <td>{account.label}</td> <td>{p.host}</td> <td>{p.account}</td> <td class="kyc"> @@ -334,8 +334,8 @@ function BitcoinTable({ list, onDelete, }: { - list: KnownBankAccountsInfo[]; - onDelete: (ac: KnownBankAccountsInfo) => void; + list: KnownBankAccountInfo[]; + onDelete: (ac: KnownBankAccountInfo) => void; }): VNode { const { i18n } = useTranslationContext(); if (list.length === 0) return <Fragment />; @@ -363,8 +363,8 @@ function BitcoinTable({ {list.map((account) => { const p = parsePaytoUri(account.paytoUri) as PaytoUriBitcoin; return ( - <tr key={account.alias}> - <td>{account.alias}</td> + <tr key={account.label}> + <td>{account.label}</td> <td>{p.targetPath}</td> <td class="kyc"> {account.kycCompleted ? ( diff --git a/packages/web-util/src/context/bank-api.ts b/packages/web-util/src/context/bank-api.ts @@ -16,7 +16,6 @@ import { CacheEvictor, - TalerCorebankConfigResponse, LibtoolVersion, ObservabilityEvent, ObservableHttpClientLibrary, @@ -25,6 +24,7 @@ import { TalerBankConversionHttpClient, TalerCoreBankCacheEviction, TalerCoreBankHttpClient, + TalerCorebankApi, TalerError, } from "@gnu-taler/taler-util"; import { @@ -46,7 +46,7 @@ import { useTranslationContext } from "./translation.js"; export type BankContextType = { url: URL; - config: TalerCorebankConfigResponse; + config: TalerCorebankApi.TalerCorebankConfigResponse; lib: BankLib; hints: VersionHint[]; onActivity: Subscriber<ObservabilityEvent>; @@ -88,7 +88,7 @@ export const BankApiProvider = ({ frameOnError: FunctionComponent<{ children: ComponentChildren }>; }): VNode => { const [checked, setChecked] = - useState<ConfigResult<TalerCorebankConfigResponse>>(); + useState<ConfigResult<TalerCorebankApi.TalerCorebankConfigResponse>>(); const { i18n } = useTranslationContext(); const { getRemoteConfig, VERSION, lib, cancelRequest, onActivity } = @@ -165,7 +165,7 @@ export const BankApiProvider = ({ function buildBankApiClient( url: URL, evictors: Evictors, -): APIClient<BankLib, TalerCorebankConfigResponse> { +): APIClient<BankLib, TalerCorebankApi.TalerCorebankConfigResponse> { const httpFetch = new BrowserFetchHttpLib({ enableThrottling: true, requireTls: false, @@ -189,13 +189,15 @@ function buildBankApiClient( httpLib, ); - async function getRemoteConfig(): Promise<TalerCorebankConfigResponse> { + async function getRemoteConfig(): Promise<TalerCorebankApi.TalerCorebankConfigResponse> { const resp = await bank.getConfig(); if (resp.type === "fail") { if (resp.detail) { throw TalerError.fromUncheckedDetail(resp.detail); } else { - throw TalerError.fromException(new Error("failed to get bank remote config")) + throw TalerError.fromException( + new Error("failed to get bank remote config"), + ); } } return resp.body;