taler-typescript-core

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

commit 1a2737fd98b1ab7a9314394c9f1b04870b1a3b14
parent a2b7bf60c4ce56422eb908b1a5574696d58da0dd
Author: Florian Dold <florian@dold.me>
Date:   Fri, 22 Nov 2024 21:06:10 +0100

wallet-core,harness: test bank account management, allow multiple currencies per bank

Diffstat:
Apackages/taler-harness/src/integrationtests/test-known-accounts.ts | 89+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-harness/src/integrationtests/testrunner.ts | 18+++++++-----------
Mpackages/taler-util/src/types-taler-wallet.ts | 29+++++++++++++++++++----------
Mpackages/taler-wallet-core/src/db.ts | 13+++++++++++--
Mpackages/taler-wallet-core/src/wallet-api-types.ts | 22+++++++++++-----------
Mpackages/taler-wallet-core/src/wallet.ts | 45++++++++++++++++-----------------------------
Mpackages/taler-wallet-core/src/withdraw.ts | 28++++++++++++++++++++++++++++
Mpackages/taler-wallet-webextension/src/wallet/DepositPage/state.ts | 2+-
Mpackages/taler-wallet-webextension/src/wallet/DepositPage/test.ts | 4++--
Mpackages/taler-wallet-webextension/src/wallet/ManageAccount/state.ts | 6+++---
Mpackages/taler-wallet-webextension/src/wallet/ManageAccount/stories.tsx | 14+++++++-------
11 files changed, 194 insertions(+), 76 deletions(-)

diff --git a/packages/taler-harness/src/integrationtests/test-known-accounts.ts b/packages/taler-harness/src/integrationtests/test-known-accounts.ts @@ -0,0 +1,89 @@ +/* + This file is part of GNU Taler + (C) 2023 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 { j2s } from "@gnu-taler/taler-util"; +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { + useSharedTestkudosEnvironment, + withdrawViaBankV2, +} from "../harness/environments.js"; +import { GlobalTestState } from "../harness/harness.js"; + +/** + * Tests for management of known bank accounts in the wallet. + */ +export async function runKnownAccountsTest(t: GlobalTestState) { + // Set up test environment + + const { walletClient, bank, exchange, merchant } = + await useSharedTestkudosEnvironment(t); + + // Withdraw digital cash into the wallet. + + await withdrawViaBankV2(t, { + walletClient, + bank, + exchange, + amount: "TESTKUDOS:20", + }); + + const accts = await walletClient.call( + WalletApiOperation.ListKnownBankAccounts, + {}, + ); + + console.log(`accounts: ${j2s(accts)}`); + + t.assertDeepEqual(accts.accounts.length, 1); + + await walletClient.call(WalletApiOperation.AddKnownBankAccount, { + paytoUri: "payto://iban/FOOBAR", + alias: "Foo", + currencies: ["EUR"], + }); + + { + const accts2 = await walletClient.call( + WalletApiOperation.ListKnownBankAccounts, + {}, + ); + console.log(`accounts after add: ${j2s(accts2)}`); + t.assertDeepEqual(accts2.accounts.length, 2); + } + + // Can use add to update + await walletClient.call(WalletApiOperation.AddKnownBankAccount, { + paytoUri: "payto://iban/FOOBAR", + alias: "Foo", + currencies: ["CHF"], + }); + + { + const accts2 = await walletClient.call( + WalletApiOperation.ListKnownBankAccounts, + {}, + ); + 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"]); + } +} + +runKnownAccountsTest.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts @@ -14,13 +14,7 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { - CancellationToken, - Logger, - minimatch, - TalerKycAml, - TalerProtocolTimestamp, -} from "@gnu-taler/taler-util"; +import { CancellationToken, Logger, minimatch } from "@gnu-taler/taler-util"; import * as child_process from "child_process"; import { spawnSync } from "child_process"; import * as fs from "fs"; @@ -30,9 +24,9 @@ import url from "url"; import { getSharedTestDir } from "../harness/environments.js"; import { GlobalTestState, - TestRunResult, runTestWithState, shouldLingerInTest, + TestRunResult, } from "../harness/harness.js"; import { runAccountRestrictionsTest } from "./test-account-restrictions.js"; import { runAgeRestrictionsDepositTest } from "./test-age-restrictions-deposit.js"; @@ -54,17 +48,21 @@ import { runExchangePurseTest } from "./test-exchange-purse.js"; import { runExchangeTimetravelTest } from "./test-exchange-timetravel.js"; import { runFeeRegressionTest } from "./test-fee-regression.js"; import { runForcedSelectionTest } from "./test-forced-selection.js"; +import { runKnownAccountsTest } from "./test-known-accounts.js"; import { runKycBalanceWithdrawalTest } from "./test-kyc-balance-withdrawal.js"; +import { runKycDecisionsTest } from "./test-kyc-decisions.js"; import { runKycDepositAggregateTest } from "./test-kyc-deposit-aggregate.js"; import { runKycDepositDepositKyctransferTest } from "./test-kyc-deposit-deposit-kyctransfer.js"; import { runKycDepositDepositTest } from "./test-kyc-deposit-deposit.js"; import { runKycExchangeWalletTest } from "./test-kyc-exchange-wallet.js"; import { runKycFormWithdrawalTest } from "./test-kyc-form-withdrawal.js"; +import { runKycMerchantActivateBankAccountTest } from "./test-kyc-merchant-activate-bank-account.js"; import { runKycMerchantAggregateTest } from "./test-kyc-merchant-aggregate.js"; import { runKycMerchantDepositTest } from "./test-kyc-merchant-deposit.js"; import { runKycNewMeasureTest } from "./test-kyc-new-measure.js"; import { runKycPeerPullTest } from "./test-kyc-peer-pull.js"; import { runKycPeerPushTest } from "./test-kyc-peer-push.js"; +import { runKycSkipExpirationTest } from "./test-kyc-skip-expiration.js"; import { runKycThresholdWithdrawalTest } from "./test-kyc-threshold-withdrawal.js"; import { runKycWithdrawalVerbotenTest } from "./test-kyc-withdrawal-verboten.js"; import { runKycTest } from "./test-kyc.js"; @@ -150,9 +148,6 @@ 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 { runKycMerchantActivateBankAccountTest } from "./test-kyc-merchant-activate-bank-account.js"; -import { runKycSkipExpirationTest } from "./test-kyc-skip-expiration.js"; -import { runKycDecisionsTest } from "./test-kyc-decisions.js"; /** * Test runner. @@ -290,6 +285,7 @@ const allTests: TestMainFunction[] = [ runKycWithdrawalVerbotenTest, runKycMerchantActivateBankAccountTest, runKycDecisionsTest, + runKnownAccountsTest, ]; export interface TestRunSpec { diff --git a/packages/taler-util/src/types-taler-wallet.ts b/packages/taler-util/src/types-taler-wallet.ts @@ -1130,9 +1130,18 @@ export interface WalletCoreVersion { export interface KnownBankAccountsInfo { paytoUri: string; + + /** + * Did we previously complete a KYC process for this bank account? + */ kycCompleted: boolean; - currency: string; - alias: string; + + /** + * Currencies supported by the bank, if known. + */ + currencies: string[] | undefined; + + alias: string | undefined; } export interface ListKnownBankAccountsResponse { @@ -1995,26 +2004,26 @@ export const codecForListKnownBankAccounts = .property("currency", codecOptional(codecForString())) .build("ListKnownBankAccountsRequest"); -export interface AddKnownBankAccountsRequest { +export interface AddKnownBankAccountRequest { paytoUri: string; alias: string; - currency: string; + currencies?: string[] | undefined; } export const codecForAddKnownBankAccounts = - (): Codec<AddKnownBankAccountsRequest> => - buildCodecForObject<AddKnownBankAccountsRequest>() + (): Codec<AddKnownBankAccountRequest> => + buildCodecForObject<AddKnownBankAccountRequest>() .property("paytoUri", codecForString()) .property("alias", codecForString()) - .property("currency", codecForString()) + .property("currencies", codecOptional(codecForList(codecForString()))) .build("AddKnownBankAccountsRequest"); -export interface ForgetKnownBankAccountsRequest { +export interface ForgetKnownBankAccountRequest { paytoUri: string; } export const codecForForgetKnownBankAccounts = - (): Codec<ForgetKnownBankAccountsRequest> => - buildCodecForObject<ForgetKnownBankAccountsRequest>() + (): Codec<ForgetKnownBankAccountRequest> => + buildCodecForObject<ForgetKnownBankAccountRequest>() .property("paytoUri", codecForString()) .build("ForgetKnownBankAccountsRequest"); diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts @@ -3016,10 +3016,19 @@ export interface FixupRecord { * User accounts */ export interface BankAccountsRecord { + /** + * Payto URI of the bank account. + */ uri: string; - currency: string; + + currencies: string[] | undefined; + + /** + * FIXME: Provide more info here. + */ kycCompleted: boolean; - alias: string; + + 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 @@ -35,7 +35,7 @@ import { AddExchangeRequest, AddGlobalCurrencyAuditorRequest, AddGlobalCurrencyExchangeRequest, - AddKnownBankAccountsRequest, + AddKnownBankAccountRequest, AmountResponse, ApplyDevExperimentRequest, BackupRecovery, @@ -70,7 +70,7 @@ import { ExportDbToFileResponse, FailTransactionRequest, ForceRefreshRequest, - ForgetKnownBankAccountsRequest, + ForgetKnownBankAccountRequest, GetActiveTasksResponse, GetBalanceDetailRequest, GetBankingChoicesForPaytoRequest, @@ -105,13 +105,13 @@ import { InitiatePeerPushDebitResponse, IntegrationTestArgs, IntegrationTestV2Args, - ListKnownBankAccountsResponse, ListAssociatedRefreshesRequest, ListAssociatedRefreshesResponse, ListExchangesRequest, ListGlobalCurrencyAuditorsResponse, ListGlobalCurrencyExchangesResponse, ListKnownBankAccountsRequest, + ListKnownBankAccountsResponse, PrepareBankIntegratedWithdrawalRequest, PrepareBankIntegratedWithdrawalResponse, PreparePayRequest, @@ -195,8 +195,8 @@ export enum WalletApiOperation { ListExchanges = "listExchanges", GetExchangeEntryByUrl = "getExchangeEntryByUrl", ListKnownBankAccounts = "listKnownBankAccounts", - AddKnownBankAccounts = "addKnownBankAccounts", - ForgetKnownBankAccounts = "forgetKnownBankAccounts", + AddKnownBankAccount = "addKnownBankAccount", + ForgetKnownBankAccount = "forgetKnownBankAccount", GetWithdrawalDetailsForUri = "getWithdrawalDetailsForUri", GetWithdrawalDetailsForAmount = "getWithdrawalDetailsForAmount", AcceptManualWithdrawal = "acceptManualWithdrawal", @@ -707,14 +707,14 @@ export type ListKnownBankAccountsOp = { }; export type AddKnownBankAccountsOp = { - op: WalletApiOperation.AddKnownBankAccounts; - request: AddKnownBankAccountsRequest; + op: WalletApiOperation.AddKnownBankAccount; + request: AddKnownBankAccountRequest; response: EmptyObject; }; export type ForgetKnownBankAccountsOp = { - op: WalletApiOperation.ForgetKnownBankAccounts; - request: ForgetKnownBankAccountsRequest; + op: WalletApiOperation.ForgetKnownBankAccount; + request: ForgetKnownBankAccountRequest; response: EmptyObject; }; @@ -1351,8 +1351,8 @@ export type WalletOperations = { [WalletApiOperation.ListExchanges]: ListExchangesOp; [WalletApiOperation.AddExchange]: AddExchangeOp; [WalletApiOperation.ListKnownBankAccounts]: ListKnownBankAccountsOp; - [WalletApiOperation.AddKnownBankAccounts]: AddKnownBankAccountsOp; - [WalletApiOperation.ForgetKnownBankAccounts]: ForgetKnownBankAccountsOp; + [WalletApiOperation.AddKnownBankAccount]: AddKnownBankAccountsOp; + [WalletApiOperation.ForgetKnownBankAccount]: ForgetKnownBankAccountsOp; [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 @@ -38,7 +38,7 @@ import { AddExchangeRequest, AddGlobalCurrencyAuditorRequest, AddGlobalCurrencyExchangeRequest, - AddKnownBankAccountsRequest, + AddKnownBankAccountRequest, AmountJson, AmountString, Amounts, @@ -60,7 +60,7 @@ import { ExportDbToFileRequest, ExportDbToFileResponse, FailTransactionRequest, - ForgetKnownBankAccountsRequest, + ForgetKnownBankAccountRequest, GetActiveTasksResponse, GetBankingChoicesForPaytoRequest, GetBankingChoicesForPaytoResponse, @@ -511,7 +511,7 @@ async function handleListKnownBankAccounts( await wex.db.runReadOnlyTx({ storeNames: ["bankAccounts"] }, async (tx) => { const knownAccounts = await tx.bankAccounts.iter().toArray(); for (const r of knownAccounts) { - if (currency && currency !== r.currency) { + if (currency && r.currencies && !r.currencies.includes(currency)) { continue; } const payto = parsePaytoUri(r.uri); @@ -520,33 +520,13 @@ async function handleListKnownBankAccounts( paytoUri: r.uri, alias: r.alias, kycCompleted: r.kycCompleted, - currency: r.currency, + currencies: r.currencies, }); } } }); return { accounts }; } - -/** - */ -async function addKnownBankAccounts( - wex: WalletExecutionContext, - payto: string, - alias: string, - currency: string, -): Promise<void> { - await wex.db.runReadWriteTx({ storeNames: ["bankAccounts"] }, async (tx) => { - tx.bankAccounts.put({ - uri: payto, - alias: alias, - currency: currency, - kycCompleted: false, - }); - }); - return; -} - /** */ async function forgetKnownBankAccounts( @@ -963,15 +943,22 @@ async function handleTestingGetDenomStats( async function handleAddKnownBankAccount( wex: WalletExecutionContext, - req: AddKnownBankAccountsRequest, + req: AddKnownBankAccountRequest, ): Promise<EmptyObject> { - await addKnownBankAccounts(wex, req.paytoUri, req.alias, req.currency); + await wex.db.runReadWriteTx({ storeNames: ["bankAccounts"] }, async (tx) => { + tx.bankAccounts.put({ + uri: req.paytoUri, + alias: req.alias, + currencies: req.currencies, + kycCompleted: false, + }); + }); return {}; } async function handleForgetKnownBankAccounts( wex: WalletExecutionContext, - req: ForgetKnownBankAccountsRequest, + req: ForgetKnownBankAccountRequest, ): Promise<EmptyObject> { await forgetKnownBankAccounts(wex, req.paytoUri); return {}; @@ -1687,11 +1674,11 @@ const handlers: { [T in WalletApiOperation]: HandlerWithValidator<T> } = { codec: codecForListKnownBankAccounts(), handler: handleListKnownBankAccounts, }, - [WalletApiOperation.AddKnownBankAccounts]: { + [WalletApiOperation.AddKnownBankAccount]: { codec: codecForAddKnownBankAccounts(), handler: handleAddKnownBankAccount, }, - [WalletApiOperation.ForgetKnownBankAccounts]: { + [WalletApiOperation.ForgetKnownBankAccount]: { codec: codecForForgetKnownBankAccounts(), handler: handleForgetKnownBankAccounts, }, diff --git a/packages/taler-wallet-core/src/withdraw.ts b/packages/taler-wallet-core/src/withdraw.ts @@ -3485,6 +3485,34 @@ export async function confirmWithdrawal( if (!acceptable) { throw Error("bank account not acceptable by the exchange"); } + + await wex.db.runReadWriteTx( + { + storeNames: ["bankAccounts"], + }, + async (tx) => { + const existingAccount = await tx.bankAccounts.get(senderWire); + if (existingAccount) { + // Add currency for existing known bank account if necessary + if (existingAccount.currencies?.includes(instructedAmount.currency)) { + existingAccount.currencies = [ + instructedAmount.currency, + ...(existingAccount.currencies ?? []), + ]; + existingAccount.currencies.sort(); + await tx.bankAccounts.put(existingAccount); + } + return; + } + + await tx.bankAccounts.put({ + currencies: [instructedAmount.currency], + kycCompleted: false, + uri: senderWire, + alias: undefined, + }); + }, + ); } const ctx = new WithdrawTransactionContext( diff --git a/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts b/packages/taler-wallet-webextension/src/wallet/DepositPage/state.ts @@ -270,7 +270,7 @@ export function createLabelsForBankAccount( const initialList: Record<string, string> = {}; if (!knownBankAccounts.length) return initialList; return knownBankAccounts.reduce((prev, cur) => { - prev[cur.paytoUri] = cur.alias; + prev[cur.paytoUri] = cur.alias ?? "??"; 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 @@ -149,13 +149,13 @@ describe("DepositPage states", () => { const ibanPayto = { paytoUri: "payto://iban/ES8877998399652238", kycCompleted: false, - currency: "EUR", + currencies: ["EUR"], alias: "my iban account", }; const talerBankPayto = { paytoUri: "payto://x-taler-bank/ES8877998399652238", kycCompleted: false, - currency: "EUR", + currencies: ["EUR"], alias: "my taler account", }; diff --git a/packages/taler-wallet-webextension/src/wallet/ManageAccount/state.ts b/packages/taler-wallet-webextension/src/wallet/ManageAccount/state.ts @@ -114,9 +114,9 @@ export function useComponentState({ if (!uri || found) return; const normalizedPayto = stringifyPaytoUri(uri); - await api.wallet.call(WalletApiOperation.AddKnownBankAccounts, { + await api.wallet.call(WalletApiOperation.AddKnownBankAccount, { alias, - currency: scope.currency, + currencies: [scope.currency], paytoUri: normalizedPayto, }); onAccountAdded(normalizedPayto); @@ -140,7 +140,7 @@ export function useComponentState({ async function deleteAccount(account: KnownBankAccountsInfo): Promise<void> { const payto = account.paytoUri; - await api.wallet.call(WalletApiOperation.ForgetKnownBankAccounts, { + await api.wallet.call(WalletApiOperation.ForgetKnownBankAccount, { paytoUri: payto, }); hook?.retry(); diff --git a/packages/taler-wallet-webextension/src/wallet/ManageAccount/stories.tsx b/packages/taler-wallet-webextension/src/wallet/ManageAccount/stories.tsx @@ -53,7 +53,7 @@ export const JustTwoBitcoinAccounts = tests.createExample(ReadyView, { bitcoin: [ { alias: "my bitcoin addr", - currency: "BTC", + currencies: ["BTC"], kycCompleted: false, paytoUri: stringifyPaytoUri({ targetType: "bitcoin", @@ -66,7 +66,7 @@ export const JustTwoBitcoinAccounts = tests.createExample(ReadyView, { }, { alias: "my other addr", - currency: "BTC", + currencies: ["BTC"], kycCompleted: true, paytoUri: stringifyPaytoUri({ targetType: "bitcoin", @@ -106,7 +106,7 @@ export const WithAllTypeOfAccounts = tests.createExample(ReadyView, { iban: [ { alias: "my bank", - currency: "ARS", + currencies: ["ARS"], kycCompleted: true, paytoUri: stringifyPaytoUri({ targetType: "iban", @@ -120,7 +120,7 @@ export const WithAllTypeOfAccounts = tests.createExample(ReadyView, { "x-taler-bank": [ { alias: "my xtaler bank", - currency: "ARS", + currencies: ["ARS"], kycCompleted: true, paytoUri: stringifyPaytoUri({ targetType: "x-taler-bank", @@ -135,7 +135,7 @@ export const WithAllTypeOfAccounts = tests.createExample(ReadyView, { bitcoin: [ { alias: "my bitcoin addr", - currency: "BTC", + currencies: ["BTC"], kycCompleted: false, paytoUri: stringifyPaytoUri({ targetType: "bitcoin", @@ -148,7 +148,7 @@ export const WithAllTypeOfAccounts = tests.createExample(ReadyView, { }, { alias: "my other addr", - currency: "BTC", + currencies: ["BTC"], kycCompleted: true, paytoUri: stringifyPaytoUri({ targetType: "bitcoin", @@ -188,7 +188,7 @@ export const AddingIbanAccount = tests.createExample(ReadyView, { iban: [ { alias: "my bank", - currency: "ARS", + currencies: ["ARS"], kycCompleted: true, paytoUri: stringifyPaytoUri({ targetType: "iban",