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:
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",