aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2024-09-23 21:37:05 +0200
committerFlorian Dold <florian@dold.me>2024-09-23 21:37:12 +0200
commita0b89530827e4f7d477c02a5fabdb0d0d54c610d (patch)
tree3e097b0dbbaebebdc397ee7dd2e94bab68d1e3c0
parent8f0e2ca8eb70560a8616b8f1b9407738099825e3 (diff)
wallet-core: check credit/debit account restrictions, test
-rw-r--r--packages/taler-harness/src/harness/harness.ts15
-rw-r--r--packages/taler-harness/src/harness/helpers.ts4
-rw-r--r--packages/taler-harness/src/integrationtests/test-account-restrictions.ts168
-rw-r--r--packages/taler-harness/src/integrationtests/testrunner.ts2
-rw-r--r--packages/taler-wallet-core/src/coinSelection.ts14
-rw-r--r--packages/taler-wallet-core/src/db.ts11
-rw-r--r--packages/taler-wallet-core/src/deposits.ts3
-rw-r--r--packages/taler-wallet-core/src/withdraw.ts35
8 files changed, 238 insertions, 14 deletions
diff --git a/packages/taler-harness/src/harness/harness.ts b/packages/taler-harness/src/harness/harness.ts
index 9ed4e77ce..86ed98f1c 100644
--- a/packages/taler-harness/src/harness/harness.ts
+++ b/packages/taler-harness/src/harness/harness.ts
@@ -25,7 +25,6 @@
* Imports
*/
import {
- AccountRestriction,
AmountJson,
Amounts,
Configuration,
@@ -603,6 +602,12 @@ class BankServiceBase {
) {}
}
+export type RestrictionFlag = "credit-restriction" | "debit-restriction";
+
+export type HarnessAccountRestriction =
+ | [RestrictionFlag, "deny"]
+ | [RestrictionFlag, "regex", string, string, string];
+
export interface HarnessExchangeBankAccount {
accountName: string;
accountPassword: string;
@@ -611,13 +616,12 @@ export interface HarnessExchangeBankAccount {
conversionUrl?: string;
- debitRestrictions?: AccountRestriction[];
- creditRestrictions?: AccountRestriction[];
-
/**
* If set, the harness will not automatically configure the wire fee for this account.
*/
skipWireFeeCreation?: boolean;
+
+ accountRestrictions?: HarnessAccountRestriction[];
}
/**
@@ -1380,6 +1384,9 @@ export class ExchangeService implements ExchangeServiceInterface {
if (acct.conversionUrl != null) {
optArgs.push("conversion-url", acct.conversionUrl);
}
+ if (acct.accountRestrictions != null) {
+ optArgs.push(...acct.accountRestrictions.flat(1));
+ }
await runCommand(
this.globalState,
diff --git a/packages/taler-harness/src/harness/helpers.ts b/packages/taler-harness/src/harness/helpers.ts
index c6e9b2d35..73dcd1550 100644
--- a/packages/taler-harness/src/harness/helpers.ts
+++ b/packages/taler-harness/src/harness/helpers.ts
@@ -59,6 +59,7 @@ import {
FakebankService,
getTestHarnessPaytoForLabel,
GlobalTestState,
+ HarnessAccountRestriction,
HarnessExchangeBankAccount,
harnessHttpLib,
LibeufinBankService,
@@ -130,6 +131,8 @@ export interface EnvOptions {
walletTestObservability?: boolean;
+ accountRestrictions?: HarnessAccountRestriction[];
+
additionalExchangeConfig?(e: ExchangeService): void;
additionalMerchantConfig?(m: MerchantService): void;
additionalBankConfig?(b: BankService): void;
@@ -486,6 +489,7 @@ export async function createSimpleTestkudosEnvironmentV3(
accountPassword: exchangeBankPassword,
accountPaytoUri: exchangePaytoUri,
skipWireFeeCreation: opts.skipWireFeeCreation === true,
+ accountRestrictions: opts.accountRestrictions,
};
await exchange.addBankAccount("1", exchangeBankAccount);
diff --git a/packages/taler-harness/src/integrationtests/test-account-restrictions.ts b/packages/taler-harness/src/integrationtests/test-account-restrictions.ts
new file mode 100644
index 000000000..0b8e16fa2
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-account-restrictions.ts
@@ -0,0 +1,168 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ AmountString,
+ j2s,
+ Logger,
+ NotificationType,
+ TalerCorebankApiClient,
+ TransactionMajorState,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import {
+ ExchangeServiceInterface,
+ GlobalTestState,
+ WalletClient,
+} from "../harness/harness.js";
+import {
+ createSimpleTestkudosEnvironmentV3,
+ WithdrawViaBankResult,
+} from "../harness/helpers.js";
+
+const logger = new Logger("test-account-restrictions.ts");
+
+/**
+ * Test for credit/debit account restrictions.
+ */
+export async function runAccountRestrictionsTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { walletClient, bankClient, exchange } =
+ await createSimpleTestkudosEnvironmentV3(t, undefined, {
+ accountRestrictions: [
+ [
+ "debit-restriction",
+ "regex",
+ "payto://x-taler-bank/.*/foo-.*",
+ "bla",
+ "{}",
+ ],
+ [
+ "credit-restriction",
+ "regex",
+ "payto://x-taler-bank/.*/foo-.*",
+ "bla",
+ "{}",
+ ],
+ ],
+ });
+
+ // Withdraw digital cash into the wallet.
+
+ // The test is incomplete: The wallet can't check the account restrictions
+ // against the sender wire account, because fakebank doesn't report
+ // sender_wire yet.
+
+ const withdrawalResult = await myWithdrawViaBank(t, {
+ walletClient,
+ bankClient,
+ exchange,
+ amount: "TESTKUDOS:20",
+ acctname: "foo-123",
+ });
+
+ await withdrawalResult.withdrawalFinishedCond;
+
+ // Invalid account, does not start with "foo-"
+ const err = await t.assertThrowsTalerErrorAsync(async () => {
+ await walletClient.call(WalletApiOperation.CheckDeposit, {
+ amount: "TESTKUDOS:5",
+ depositPaytoUri: "payto://x-taler-bank/localhost/bar-42",
+ });
+ });
+
+ logger.info(`checkResp ${j2s(err)}`);
+
+ // Valid account
+ await walletClient.call(WalletApiOperation.CheckDeposit, {
+ amount: "TESTKUDOS:5",
+ depositPaytoUri: "payto://x-taler-bank/localhost/foo-42",
+ });
+}
+
+export async function myWithdrawViaBank(
+ t: GlobalTestState,
+ p: {
+ walletClient: WalletClient;
+ bankClient: TalerCorebankApiClient;
+ exchange: ExchangeServiceInterface;
+ amount: AmountString | string;
+ restrictAge?: number;
+ acctname: string;
+ },
+): Promise<WithdrawViaBankResult> {
+ const { walletClient: wallet, bankClient, exchange, amount } = p;
+
+ const user = await bankClient.createRandomBankUser();
+ await bankClient.registerAccountExtended({
+ name: p.acctname,
+ password: "test",
+ username: p.acctname,
+ });
+ const bankClient2 = new TalerCorebankApiClient(bankClient.baseUrl);
+ bankClient2.setAuth({
+ username: user.username,
+ password: user.password,
+ });
+
+ const wop = await bankClient2.createWithdrawalOperation(
+ user.username,
+ amount,
+ );
+
+ // Hand it to the wallet
+
+ await wallet.client.call(WalletApiOperation.GetWithdrawalDetailsForUri, {
+ talerWithdrawUri: wop.taler_withdraw_uri,
+ restrictAge: p.restrictAge,
+ });
+
+ // Withdraw (AKA select)
+
+ const acceptRes = await wallet.client.call(
+ WalletApiOperation.AcceptBankIntegratedWithdrawal,
+ {
+ exchangeBaseUrl: exchange.baseUrl,
+ talerWithdrawUri: wop.taler_withdraw_uri,
+ restrictAge: p.restrictAge,
+ },
+ );
+
+ const withdrawalFinishedCond = wallet.waitForNotificationCond(
+ (x) =>
+ x.type === NotificationType.TransactionStateTransition &&
+ x.newTxState.major === TransactionMajorState.Done &&
+ x.transactionId === acceptRes.transactionId,
+ );
+
+ // Confirm it
+
+ await bankClient2.confirmWithdrawalOperation(user.username, {
+ withdrawalOperationId: wop.withdrawal_id,
+ });
+
+ return {
+ accountPaytoUri: user.accountPaytoUri,
+ withdrawalFinishedCond,
+ transactionId: acceptRes.transactionId,
+ };
+}
+
+runAccountRestrictionsTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts
index 8ae179701..e0f6ee65b 100644
--- a/packages/taler-harness/src/integrationtests/testrunner.ts
+++ b/packages/taler-harness/src/integrationtests/testrunner.ts
@@ -28,6 +28,7 @@ import {
shouldLingerInTest,
} from "../harness/harness.js";
import { getSharedTestDir } from "../harness/helpers.js";
+import { runAccountRestrictionsTest } from "./test-account-restrictions.js";
import { runAgeRestrictionsDepositTest } from "./test-age-restrictions-deposit.js";
import { runAgeRestrictionsMerchantTest } from "./test-age-restrictions-merchant.js";
import { runAgeRestrictionsMixedMerchantTest } from "./test-age-restrictions-mixed-merchant.js";
@@ -270,6 +271,7 @@ const allTests: TestMainFunction[] = [
runKycMerchantAggregateTest,
runKycDepositDepositKyctransferTest,
runWithdrawalPrepareTest,
+ runAccountRestrictionsTest,
];
export interface TestRunSpec {
diff --git a/packages/taler-wallet-core/src/coinSelection.ts b/packages/taler-wallet-core/src/coinSelection.ts
index 01f8c49a9..bc9d51ec7 100644
--- a/packages/taler-wallet-core/src/coinSelection.ts
+++ b/packages/taler-wallet-core/src/coinSelection.ts
@@ -192,6 +192,20 @@ async function internalSelectPayCoins(
| { sel: SelResult; coinRes: SelectedCoin[]; tally: CoinSelectionTally }
| undefined
> {
+ let restrictWireMethod;
+ if (req.depositPaytoUri) {
+ const parsedPayto = parsePaytoUri(req.depositPaytoUri);
+ if (!parsedPayto) {
+ throw Error("invalid payto URI");
+ }
+ restrictWireMethod = parsedPayto.targetType;
+ if (restrictWireMethod !== req.restrictWireMethod) {
+ logger.warn(`conflicting payto URI and wire method restriction`);
+ }
+ } else {
+ restrictWireMethod = req.restrictWireMethod;
+ }
+
const { contractTermsAmount, depositFeeLimit } = req;
const candidateRes = await selectPayCandidates(wex, tx, {
currency: Amounts.currencyOf(req.contractTermsAmount),
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts
index 346e56c11..2b64d8d9d 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -416,6 +416,8 @@ export interface ReserveBankInfo {
currency: string | undefined;
externalConfirmation?: boolean;
+
+ senderWire?: string;
}
/**
@@ -1420,6 +1422,7 @@ export const enum WithdrawalRecordType {
export interface WgInfoBankIntegrated {
withdrawalType: WithdrawalRecordType.BankIntegrated;
+
/**
* Extra state for when this is a withdrawal involving
* a Taler-integrated bank.
@@ -1532,14 +1535,6 @@ export interface WithdrawalGroupRecord {
status: WithdrawalGroupStatus;
/**
- * Wire information (as payto URI) for the bank account that
- * transferred funds for this reserve.
- *
- * FIXME: Doesn't this belong to the bankAccounts object store?
- */
- senderWire?: string;
-
- /**
* Restrict withdrawals from this reserve to this age.
*/
restrictAge?: number;
diff --git a/packages/taler-wallet-core/src/deposits.ts b/packages/taler-wallet-core/src/deposits.ts
index a4dd09005..2e0eca8d3 100644
--- a/packages/taler-wallet-core/src/deposits.ts
+++ b/packages/taler-wallet-core/src/deposits.ts
@@ -1523,6 +1523,7 @@ async function processDepositGroupPendingDeposit(
exchanges: contractData.allowedExchanges,
},
restrictWireMethod: contractData.wireMethod,
+ depositPaytoUri: dg.wire.payto_uri,
contractTermsAmount: Amounts.parseOrThrow(contractData.amount),
depositFeeLimit: Amounts.parseOrThrow(contractData.maxDepositFee),
prevPayCoins: [],
@@ -1906,6 +1907,7 @@ export async function internalCheckDepositGroup(
exchanges: contractData.allowedExchanges,
},
restrictWireMethod: contractData.wireMethod,
+ depositPaytoUri: req.depositPaytoUri,
contractTermsAmount: Amounts.parseOrThrow(contractData.amount),
depositFeeLimit: Amounts.parseOrThrow(contractData.maxDepositFee),
prevPayCoins: [],
@@ -2007,6 +2009,7 @@ export async function createDepositGroup(
})),
},
restrictWireMethod: depositPayto.targetType,
+ depositPaytoUri: req.depositPaytoUri,
contractTermsAmount: amount,
depositFeeLimit: amount,
prevPayCoins: [],
diff --git a/packages/taler-wallet-core/src/withdraw.ts b/packages/taler-wallet-core/src/withdraw.ts
index 2739294df..add04f3dc 100644
--- a/packages/taler-wallet-core/src/withdraw.ts
+++ b/packages/taler-wallet-core/src/withdraw.ts
@@ -85,6 +85,7 @@ import {
WithdrawalType,
addPaytoQueryParams,
assertUnreachable,
+ checkAccountRestriction,
checkDbInvariant,
checkLogicInvariant,
codecForAccountKycStatus,
@@ -101,6 +102,7 @@ import {
getRandomBytes,
j2s,
makeErrorDetail,
+ parsePaytoUri,
parseTalerUri,
parseWithdrawUri,
} from "@gnu-taler/taler-util";
@@ -2439,7 +2441,7 @@ export async function processWithdrawalGroup(
case WithdrawalGroupStatus.PendingWaitConfirmBank:
return await processReserveBankStatus(wex, withdrawalGroupId);
case WithdrawalGroupStatus.PendingKyc:
- return processWithdrawalGroupPendingKyc(wex, withdrawalGroup);
+ return await processWithdrawalGroupPendingKyc(wex, withdrawalGroup);
case WithdrawalGroupStatus.PendingReady:
// Continue with the actual withdrawal!
return await processWithdrawalGroupPendingReady(wex, withdrawalGroup);
@@ -3123,7 +3125,6 @@ export async function internalPrepareCreateWithdrawalGroup(
status: args.reserveStatus,
withdrawalGroupId,
restrictAge: args.restrictAge,
- senderWire: undefined,
timestampFinish: undefined,
wgInfo: args.wgInfo,
};
@@ -3354,6 +3355,7 @@ export async function prepareBankIntegratedWithdrawal(
timestampReserveInfoPosted: undefined,
wireTypes: withdrawInfo.wireTypes,
currency: withdrawInfo.currency,
+ senderWire: withdrawInfo.senderWire,
externalConfirmation,
},
},
@@ -3426,6 +3428,10 @@ export async function confirmWithdrawal(
bankCurrency = withdrawalGroup.wgInfo.bankInfo.currency;
}
+ if (exchange.currency !== bankCurrency) {
+ throw Error("currency mismatch between exchange and bank");
+ }
+
const exchangePaytoUri = await getExchangePaytoUri(
wex,
selectedExchange,
@@ -3441,6 +3447,31 @@ export async function confirmWithdrawal(
wex.cancellationToken,
);
+ const senderWire = withdrawalGroup.wgInfo.bankInfo.senderWire;
+
+ if (senderWire) {
+ logger.info(`sender wire is ${senderWire}`);
+ const parsedSenderWire = parsePaytoUri(senderWire);
+ if (!parsedSenderWire) {
+ throw Error("invalid payto URI");
+ }
+ let acceptable = false;
+ for (const acc of withdrawalAccountList) {
+ const parsedExchangeWire = parsePaytoUri(acc.paytoUri);
+ if (!parsedExchangeWire) {
+ continue;
+ }
+ if (!checkAccountRestriction(senderWire, acc.creditRestrictions ?? [])) {
+ continue;
+ }
+ acceptable = true;
+ break;
+ }
+ if (!acceptable) {
+ throw Error("bank account not acceptable by the exchange");
+ }
+ }
+
const ctx = new WithdrawTransactionContext(
wex,
withdrawalGroup.withdrawalGroupId,