diff options
Diffstat (limited to 'packages/taler-harness/src/integrationtests')
54 files changed, 5133 insertions, 212 deletions
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..c0c85642c --- /dev/null +++ b/packages/taler-harness/src/integrationtests/test-account-restrictions.ts @@ -0,0 +1,179 @@ +/* + 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. + + const withdrawalResult = await myWithdrawViaBank(t, { + walletClient, + bankClient, + exchange, + amount: "TESTKUDOS:20", + acctname: "foo-123", + }); + + await withdrawalResult.withdrawalFinishedCond; + + // When withdrawing from an account that doesn't begin with "foo-", + // it fails. + await t.assertThrowsAsync(async () => { + await myWithdrawViaBank(t, { + walletClient, + bankClient, + exchange, + amount: "TESTKUDOS:20", + acctname: "bar-123", + }); + }); + + // 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; + await bankClient.registerAccountExtended({ + name: p.acctname, + password: "test", + username: p.acctname, + }); + const user = { + password: "test", + username: p.acctname, + }; + const accountPaytoUri = `payto://x-taler-bank/localhost/${p.acctname}?receiver-name=${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, + withdrawalFinishedCond, + transactionId: acceptRes.transactionId, + }; +} + +runAccountRestrictionsTest.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/test-age-restrictions-deposit.ts b/packages/taler-harness/src/integrationtests/test-age-restrictions-deposit.ts index a0e97c218..aa107696c 100644 --- a/packages/taler-harness/src/integrationtests/test-age-restrictions-deposit.ts +++ b/packages/taler-harness/src/integrationtests/test-age-restrictions-deposit.ts @@ -24,7 +24,7 @@ import { TransactionMinorState, } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { GlobalTestState, generateRandomPayto } from "../harness/harness.js"; +import { GlobalTestState, getTestHarnessPaytoForLabel } from "../harness/harness.js"; import { createSimpleTestkudosEnvironmentV3, withdrawViaBankV3, @@ -83,7 +83,7 @@ export async function runAgeRestrictionsDepositTest(t: GlobalTestState) { WalletApiOperation.CreateDepositGroup, { amount: "TESTKUDOS:10" as AmountString, - depositPaytoUri: generateRandomPayto("foo"), + depositPaytoUri: getTestHarnessPaytoForLabel("foo"), transactionId: depositTxId, }, ); diff --git a/packages/taler-harness/src/integrationtests/test-bank-api.ts b/packages/taler-harness/src/integrationtests/test-bank-api.ts index 58f8bb106..544957185 100644 --- a/packages/taler-harness/src/integrationtests/test-bank-api.ts +++ b/packages/taler-harness/src/integrationtests/test-bank-api.ts @@ -30,7 +30,7 @@ import { ExchangeService, GlobalTestState, MerchantService, - generateRandomPayto, + getTestHarnessPaytoForLabel, setupDb, } from "../harness/harness.js"; @@ -66,7 +66,7 @@ export async function runBankApiTest(t: GlobalTestState) { let receiverName = "Exchange"; let exchangeBankUsername = "exchange"; let exchangeBankPassword = "mypw"; - let exchangePaytoUri = generateRandomPayto(exchangeBankUsername); + let exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername); let wireGatewayApiBaseUrl = new URL("accounts/exchange/taler-wire-gateway/", bank.baseUrl).href; await exchange.addBankAccount("1", { @@ -95,13 +95,13 @@ export async function runBankApiTest(t: GlobalTestState) { await merchant.addInstanceWithWireAccount({ id: "default", name: "Default Instance", - paytoUris: [generateRandomPayto("merchant-default")], + paytoUris: [getTestHarnessPaytoForLabel("merchant-default")], }); await merchant.addInstanceWithWireAccount({ id: "minst1", name: "minst1", - paytoUris: [generateRandomPayto("minst1")], + paytoUris: [getTestHarnessPaytoForLabel("minst1")], }); console.log("setup done!"); diff --git a/packages/taler-harness/src/integrationtests/test-currency-scope.ts b/packages/taler-harness/src/integrationtests/test-currency-scope.ts index 34d18d87d..48502f6b7 100644 --- a/packages/taler-harness/src/integrationtests/test-currency-scope.ts +++ b/packages/taler-harness/src/integrationtests/test-currency-scope.ts @@ -26,7 +26,7 @@ import { GlobalTestState, HarnessExchangeBankAccount, MerchantService, - generateRandomPayto, + getTestHarnessPaytoForLabel, setupDb, } from "../harness/harness.js"; import { @@ -81,7 +81,7 @@ export async function runCurrencyScopeTest(t: GlobalTestState) { ).href, accountName: "myexchange", accountPassword: "x", - accountPaytoUri: generateRandomPayto("myexchange"), + accountPaytoUri: getTestHarnessPaytoForLabel("myexchange"), }; let exchangeTwoBankAccount: HarnessExchangeBankAccount = { @@ -91,7 +91,7 @@ export async function runCurrencyScopeTest(t: GlobalTestState) { ).href, accountName: "myexchange2", accountPassword: "x", - accountPaytoUri: generateRandomPayto("myexchange2"), + accountPaytoUri: getTestHarnessPaytoForLabel("myexchange2"), }; bank.setSuggestedExchange( @@ -151,7 +151,7 @@ export async function runCurrencyScopeTest(t: GlobalTestState) { await merchant.addInstanceWithWireAccount({ id: "default", name: "Default Instance", - paytoUris: [generateRandomPayto("merchant-default")], + paytoUris: [getTestHarnessPaytoForLabel("merchant-default")], defaultWireTransferDelay: Duration.toTalerProtocolDuration( Duration.fromSpec({ minutes: 1 }), ), @@ -160,7 +160,7 @@ export async function runCurrencyScopeTest(t: GlobalTestState) { await merchant.addInstanceWithWireAccount({ id: "minst1", name: "minst1", - paytoUris: [generateRandomPayto("minst1")], + paytoUris: [getTestHarnessPaytoForLabel("minst1")], defaultWireTransferDelay: Duration.toTalerProtocolDuration( Duration.fromSpec({ minutes: 1 }), ), diff --git a/packages/taler-harness/src/integrationtests/test-deposit.ts b/packages/taler-harness/src/integrationtests/test-deposit.ts index 0879c9e9f..654829c91 100644 --- a/packages/taler-harness/src/integrationtests/test-deposit.ts +++ b/packages/taler-harness/src/integrationtests/test-deposit.ts @@ -25,7 +25,7 @@ import { j2s, } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { GlobalTestState, generateRandomPayto } from "../harness/harness.js"; +import { GlobalTestState, getTestHarnessPaytoForLabel } from "../harness/harness.js"; import { createSimpleTestkudosEnvironmentV3, withdrawViaBankV3, @@ -51,6 +51,23 @@ export async function runDepositTest(t: GlobalTestState) { await withdrawalResult.withdrawalFinishedCond; + const depositPaytoUri = getTestHarnessPaytoForLabel("foo"); + + const bal = await walletClient.call(WalletApiOperation.GetBalances, {}); + + t.assertAmountEquals(bal.balances[0].available, "TESTKUDOS:19.53"); + + const maxDepositResp = await walletClient.call( + WalletApiOperation.GetMaxDepositAmount, + { + currency: "TESTKUDOS", + depositPaytoUri, + }, + ); + + t.assertAmountEquals(maxDepositResp.rawAmount, "TESTKUDOS:19.09"); + t.assertAmountEquals(maxDepositResp.effectiveAmount, "TESTKUDOS:19.53"); + const dgIdResp = await walletClient.client.call( WalletApiOperation.GenerateDepositGroupTxId, {}, @@ -77,7 +94,7 @@ export async function runDepositTest(t: GlobalTestState) { WalletApiOperation.CreateDepositGroup, { amount: "TESTKUDOS:10" as AmountString, - depositPaytoUri: generateRandomPayto("foo"), + depositPaytoUri, transactionId: depositTxId, }, ); diff --git a/packages/taler-harness/src/integrationtests/test-exchange-management-fault.ts b/packages/taler-harness/src/integrationtests/test-exchange-management-fault.ts index 801162ac8..9bdb0d93b 100644 --- a/packages/taler-harness/src/integrationtests/test-exchange-management-fault.ts +++ b/packages/taler-harness/src/integrationtests/test-exchange-management-fault.ts @@ -36,7 +36,7 @@ import { GlobalTestState, MerchantService, WalletCli, - generateRandomPayto, + getTestHarnessPaytoForLabel, setupDb, } from "../harness/harness.js"; @@ -74,7 +74,7 @@ export async function runExchangeManagementFaultTest( let receiverName = "Exchange"; let exchangeBankUsername = "exchange"; let exchangeBankPassword = "mypw"; - let exchangePaytoUri = generateRandomPayto(exchangeBankUsername); + let exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername); await exchange.addBankAccount("1", { accountName: exchangeBankUsername, @@ -111,13 +111,13 @@ export async function runExchangeManagementFaultTest( await merchant.addInstanceWithWireAccount({ id: "default", name: "Default Instance", - paytoUris: [generateRandomPayto("merchant-default")], + paytoUris: [getTestHarnessPaytoForLabel("merchant-default")], }); await merchant.addInstanceWithWireAccount({ id: "minst1", name: "minst1", - paytoUris: [generateRandomPayto("minst1")], + paytoUris: [getTestHarnessPaytoForLabel("minst1")], }); console.log("setup done!"); diff --git a/packages/taler-harness/src/integrationtests/test-exchange-purse.ts b/packages/taler-harness/src/integrationtests/test-exchange-purse.ts index 6666e2d0b..68dd58d3e 100644 --- a/packages/taler-harness/src/integrationtests/test-exchange-purse.ts +++ b/packages/taler-harness/src/integrationtests/test-exchange-purse.ts @@ -148,6 +148,7 @@ export async function runExchangePurseTest(t: GlobalTestState) { contribution: amount, denomPubHash: coin.denomPubHash, denomSig: coin.denomSig, + feeDeposit: d1.fees.feeDeposit, }; const depositSigsResp = await cryptoApi.signPurseDeposits({ diff --git a/packages/taler-harness/src/integrationtests/test-exchange-timetravel.ts b/packages/taler-harness/src/integrationtests/test-exchange-timetravel.ts index 4f2fb1ee4..828289373 100644 --- a/packages/taler-harness/src/integrationtests/test-exchange-timetravel.ts +++ b/packages/taler-harness/src/integrationtests/test-exchange-timetravel.ts @@ -35,7 +35,7 @@ import { makeNoFeeCoinConfig } from "../harness/denomStructures.js"; import { BankService, ExchangeService, - generateRandomPayto, + getTestHarnessPaytoForLabel, GlobalTestState, MerchantService, setupDb, @@ -128,7 +128,7 @@ export async function runExchangeTimetravelTest(t: GlobalTestState) { let receiverName = "Exchange"; let exchangeBankUsername = "exchange"; let exchangeBankPassword = "mypw"; - let exchangePaytoUri = generateRandomPayto(exchangeBankUsername); + let exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername); await exchange.addBankAccount("1", { accountName: exchangeBankUsername, @@ -171,13 +171,13 @@ export async function runExchangeTimetravelTest(t: GlobalTestState) { await merchant.addInstanceWithWireAccount({ id: "default", name: "Default Instance", - paytoUris: [generateRandomPayto("merchant-default")], + paytoUris: [getTestHarnessPaytoForLabel("merchant-default")], }); await merchant.addInstanceWithWireAccount({ id: "minst1", name: "minst1", - paytoUris: [generateRandomPayto("minst1")], + paytoUris: [getTestHarnessPaytoForLabel("minst1")], }); console.log("setup done!"); diff --git a/packages/taler-harness/src/integrationtests/test-fee-regression.ts b/packages/taler-harness/src/integrationtests/test-fee-regression.ts index 6ae7b5de8..b08fce4cb 100644 --- a/packages/taler-harness/src/integrationtests/test-fee-regression.ts +++ b/packages/taler-harness/src/integrationtests/test-fee-regression.ts @@ -27,7 +27,7 @@ import { ExchangeService, GlobalTestState, MerchantService, - generateRandomPayto, + getTestHarnessPaytoForLabel, setupDb, } from "../harness/harness.js"; import { @@ -70,7 +70,7 @@ export async function createMyTestkudosEnvironment( let receiverName = "Exchange"; let exchangeBankUsername = "exchange"; let exchangeBankPassword = "mypw"; - let exchangePaytoUri = generateRandomPayto(exchangeBankUsername); + let exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername); await exchange.addBankAccount("1", { accountName: exchangeBankUsername, @@ -170,7 +170,7 @@ export async function createMyTestkudosEnvironment( await merchant.addInstanceWithWireAccount({ id: "minst1", name: "minst1", - paytoUris: [generateRandomPayto("minst1")], + paytoUris: [getTestHarnessPaytoForLabel("minst1")], }); console.log("setup done!"); @@ -189,6 +189,7 @@ export async function createMyTestkudosEnvironment( walletClient, walletService, bankClient, + bank, exchangeBankAccount: { accountName: "", accountPassword: "", diff --git a/packages/taler-harness/src/integrationtests/test-kyc-balance-withdrawal.ts b/packages/taler-harness/src/integrationtests/test-kyc-balance-withdrawal.ts new file mode 100644 index 000000000..5e7a4756e --- /dev/null +++ b/packages/taler-harness/src/integrationtests/test-kyc-balance-withdrawal.ts @@ -0,0 +1,266 @@ +/* + 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 { + encodeCrock, + ExchangeWalletKycStatus, + hashPaytoUri, + j2s, + TalerCorebankApiClient, + TransactionIdStr, + TransactionMajorState, + TransactionMinorState, +} from "@gnu-taler/taler-util"; +import { + createSyncCryptoApi, + EddsaKeyPairStrings, + WalletApiOperation, +} from "@gnu-taler/taler-wallet-core"; +import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js"; +import { + BankService, + DbInfo, + ExchangeService, + getTestHarnessPaytoForLabel, + GlobalTestState, + HarnessExchangeBankAccount, + setupDb, + WalletClient, + WalletService, +} from "../harness/harness.js"; +import { + EnvOptions, + postAmlDecisionNoRules, + withdrawViaBankV3, +} from "../harness/helpers.js"; + +interface KycTestEnv { + commonDb: DbInfo; + bankClient: TalerCorebankApiClient; + exchange: ExchangeService; + exchangeBankAccount: HarnessExchangeBankAccount; + walletClient: WalletClient; + walletService: WalletService; + amlKeypair: EddsaKeyPairStrings; +} + +async function createKycTestkudosEnvironment( + t: GlobalTestState, + coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")), + opts: EnvOptions = {}, +): Promise<KycTestEnv> { + const db = await setupDb(t); + + const bank = await BankService.create(t, { + allowRegistrations: true, + currency: "TESTKUDOS", + database: db.connStr, + httpPort: 8082, + }); + + const exchange = ExchangeService.create(t, { + name: "testexchange-1", + currency: "TESTKUDOS", + httpPort: 8081, + database: db.connStr, + }); + + let receiverName = "Exchange"; + let exchangeBankUsername = "exchange"; + let exchangeBankPassword = "mypw"; + let exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername); + + await exchange.addBankAccount("1", { + accountName: exchangeBankUsername, + accountPassword: exchangeBankPassword, + wireGatewayApiBaseUrl: new URL( + "accounts/exchange/taler-wire-gateway/", + bank.baseUrl, + ).href, + accountPaytoUri: exchangePaytoUri, + }); + + bank.setSuggestedExchange(exchange, exchangePaytoUri); + + await bank.start(); + + await bank.pingUntilAvailable(); + + const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl, { + auth: { + username: "admin", + password: "adminpw", + }, + }); + + await bankClient.registerAccountExtended({ + name: receiverName, + password: exchangeBankPassword, + username: exchangeBankUsername, + is_taler_exchange: true, + payto_uri: exchangePaytoUri, + }); + + exchange.addCoinConfigList(coinConfig); + + await exchange.modifyConfig(async (config) => { + config.setString("exchange", "enable_kyc", "yes"); + + config.setString("KYC-RULE-R1", "operation_type", "balance"); + config.setString("KYC-RULE-R1", "enabled", "yes"); + config.setString("KYC-RULE-R1", "exposed", "yes"); + config.setString("KYC-RULE-R1", "is_and_combinator", "yes"); + config.setString("KYC-RULE-R1", "threshold", "TESTKUDOS:10"); + config.setString("KYC-RULE-R1", "timeframe", "forever"); + config.setString("KYC-RULE-R1", "next_measures", "M1"); + + config.setString("KYC-MEASURE-M1", "check_name", "C1"); + config.setString("KYC-MEASURE-M1", "context", "{}"); + config.setString("KYC-MEASURE-M1", "program", "P1"); + + config.setString("AML-PROGRAM-P1", "command", "/bin/true"); + config.setString("AML-PROGRAM-P1", "enabled", "true"); + config.setString("AML-PROGRAM-P1", "description", "this does nothing"); + config.setString("AML-PROGRAM-P1", "fallback", "M1"); + + config.setString("KYC-CHECK-C1", "type", "INFO"); + config.setString("KYC-CHECK-C1", "description", "my check!"); + config.setString("KYC-CHECK-C1", "fallback", "M1"); + }); + + await exchange.start(); + + const cryptoApi = createSyncCryptoApi(); + const amlKeypair = await cryptoApi.createEddsaKeypair({}); + + await exchange.enableAmlAccount(amlKeypair.pub, "Alice"); + + const walletService = new WalletService(t, { + name: "wallet", + useInMemoryDb: true, + }); + await walletService.start(); + await walletService.pingUntilAvailable(); + + const walletClient = new WalletClient({ + name: "wallet", + unixPath: walletService.socketPath, + onNotification(n) { + console.log("got notification", n); + }, + }); + await walletClient.connect(); + await walletClient.client.call(WalletApiOperation.InitWallet, { + config: { + testing: { + skipDefaults: true, + }, + }, + }); + + console.log("setup done!"); + + return { + commonDb: db, + exchange, + amlKeypair, + walletClient, + walletService, + bankClient, + exchangeBankAccount: { + accountName: "", + accountPassword: "", + accountPaytoUri: "", + wireGatewayApiBaseUrl: "", + }, + }; +} + +export async function runKycBalanceWithdrawalTest(t: GlobalTestState) { + // Set up test environment + + const { walletClient, bankClient, exchange, amlKeypair } = + await createKycTestkudosEnvironment(t); + + // Withdraw digital cash into the wallet. + + const wres = await withdrawViaBankV3(t, { + amount: "TESTKUDOS:20", + bankClient, + exchange, + walletClient, + }); + + await walletClient.call(WalletApiOperation.TestingWaitTransactionState, { + transactionId: wres.transactionId as TransactionIdStr, + txState: { + major: TransactionMajorState.Pending, + minor: TransactionMinorState.BalanceKycRequired, + }, + }); + + { + const exchangeEntry = await walletClient.call( + WalletApiOperation.GetExchangeEntryByUrl, + { + exchangeBaseUrl: exchange.baseUrl, + }, + ); + console.log(j2s(exchangeEntry)); + } + + await walletClient.call(WalletApiOperation.TestingWaitExchangeState, { + exchangeBaseUrl: exchange.baseUrl, + walletKycStatus: ExchangeWalletKycStatus.Legi, + }); + + { + const exchangeEntry = await walletClient.call( + WalletApiOperation.GetExchangeEntryByUrl, + { + exchangeBaseUrl: exchange.baseUrl, + }, + ); + console.log(j2s(exchangeEntry)); + t.assertDeepEqual( + exchangeEntry.walletKycStatus, + ExchangeWalletKycStatus.Legi, + ); + + const kycReservePub = exchangeEntry.walletKycReservePub; + t.assertTrue(!!kycReservePub); + + // FIXME: Create/user helper function for this! + const hPayto = hashPaytoUri( + `payto://taler-reserve-http/localhost:${exchange.port}/${kycReservePub}`, + ); + + await postAmlDecisionNoRules(t, { + amlPriv: amlKeypair.priv, + amlPub: amlKeypair.pub, + exchangeBaseUrl: exchange.baseUrl, + paytoHash: encodeCrock(hPayto), + }); + } + + // Now after KYC is done for the balance, the withdrawal should finish + await wres.withdrawalFinishedCond; +} + +runKycBalanceWithdrawalTest.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/test-kyc-deposit-aggregate.ts b/packages/taler-harness/src/integrationtests/test-kyc-deposit-aggregate.ts new file mode 100644 index 000000000..fb0e7d79e --- /dev/null +++ b/packages/taler-harness/src/integrationtests/test-kyc-deposit-aggregate.ts @@ -0,0 +1,291 @@ +/* + 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 { + TalerCorebankApiClient, + TransactionMajorState, + TransactionMinorState, +} from "@gnu-taler/taler-util"; +import { + createSyncCryptoApi, + EddsaKeyPairStrings, + WalletApiOperation, +} from "@gnu-taler/taler-wallet-core"; +import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js"; +import { + BankService, + DbInfo, + ExchangeService, + getTestHarnessPaytoForLabel, + GlobalTestState, + HarnessExchangeBankAccount, + setupDb, + WalletClient, + WalletService, +} from "../harness/harness.js"; +import { + EnvOptions, + postAmlDecisionNoRules, + withdrawViaBankV3, +} from "../harness/helpers.js"; + +interface KycTestEnv { + commonDb: DbInfo; + bankClient: TalerCorebankApiClient; + exchange: ExchangeService; + exchangeBankAccount: HarnessExchangeBankAccount; + walletClient: WalletClient; + walletService: WalletService; + amlKeypair: EddsaKeyPairStrings; +} + +async function createKycTestkudosEnvironment( + t: GlobalTestState, + coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")), + opts: EnvOptions = {}, +): Promise<KycTestEnv> { + const db = await setupDb(t); + + const bank = await BankService.create(t, { + allowRegistrations: true, + currency: "TESTKUDOS", + database: db.connStr, + httpPort: 8082, + }); + + const exchange = ExchangeService.create(t, { + name: "testexchange-1", + currency: "TESTKUDOS", + httpPort: 8081, + database: db.connStr, + }); + + let receiverName = "Exchange"; + let exchangeBankUsername = "exchange"; + let exchangeBankPassword = "mypw"; + let exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername); + + await exchange.addBankAccount("1", { + accountName: exchangeBankUsername, + accountPassword: exchangeBankPassword, + wireGatewayApiBaseUrl: new URL( + "accounts/exchange/taler-wire-gateway/", + bank.baseUrl, + ).href, + accountPaytoUri: exchangePaytoUri, + }); + + bank.setSuggestedExchange(exchange, exchangePaytoUri); + + await bank.start(); + + await bank.pingUntilAvailable(); + + const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl, { + auth: { + username: "admin", + password: "adminpw", + }, + }); + + await bankClient.registerAccountExtended({ + name: receiverName, + password: exchangeBankPassword, + username: exchangeBankUsername, + is_taler_exchange: true, + payto_uri: exchangePaytoUri, + }); + + const ageMaskSpec = opts.ageMaskSpec; + + if (ageMaskSpec) { + exchange.enableAgeRestrictions(ageMaskSpec); + // Enable age restriction for all coins. + exchange.addCoinConfigList( + coinConfig.map((x) => ({ + ...x, + name: `${x.name}-age`, + ageRestricted: true, + })), + ); + // For mixed age restrictions, we also offer coins without age restrictions + if (opts.mixedAgeRestriction) { + exchange.addCoinConfigList( + coinConfig.map((x) => ({ ...x, ageRestricted: false })), + ); + } + } else { + exchange.addCoinConfigList(coinConfig); + } + + await exchange.modifyConfig(async (config) => { + config.setString("exchange", "enable_kyc", "yes"); + + config.setString("KYC-RULE-R1", "operation_type", "aggregate"); + config.setString("KYC-RULE-R1", "enabled", "yes"); + config.setString("KYC-RULE-R1", "exposed", "yes"); + config.setString("KYC-RULE-R1", "is_and_combinator", "yes"); + config.setString("KYC-RULE-R1", "threshold", "TESTKUDOS:5"); + config.setString("KYC-RULE-R1", "timeframe", "1d"); + config.setString("KYC-RULE-R1", "next_measures", "M1"); + + config.setString("KYC-MEASURE-M1", "check_name", "C1"); + config.setString("KYC-MEASURE-M1", "context", "{}"); + config.setString("KYC-MEASURE-M1", "program", "P1"); + + config.setString("AML-PROGRAM-P1", "command", "/bin/true"); + config.setString("AML-PROGRAM-P1", "enabled", "true"); + config.setString("AML-PROGRAM-P1", "description", "this does nothing"); + config.setString("AML-PROGRAM-P1", "fallback", "M1"); + + config.setString("KYC-CHECK-C1", "type", "INFO"); + config.setString("KYC-CHECK-C1", "description", "my check!"); + config.setString("KYC-CHECK-C1", "fallback", "M1"); + }); + + await exchange.start(); + + const cryptoApi = createSyncCryptoApi(); + const amlKeypair = await cryptoApi.createEddsaKeypair({}); + + await exchange.enableAmlAccount(amlKeypair.pub, "Alice"); + + const walletService = new WalletService(t, { + name: "wallet", + useInMemoryDb: true, + }); + await walletService.start(); + await walletService.pingUntilAvailable(); + + const walletClient = new WalletClient({ + name: "wallet", + unixPath: walletService.socketPath, + onNotification(n) { + console.log("got notification", n); + }, + }); + await walletClient.connect(); + await walletClient.client.call(WalletApiOperation.InitWallet, { + config: { + testing: { + skipDefaults: true, + }, + }, + }); + + console.log("setup done!"); + + return { + commonDb: db, + exchange, + amlKeypair, + walletClient, + walletService, + bankClient, + exchangeBankAccount: { + accountName: "", + accountPassword: "", + accountPaytoUri: "", + wireGatewayApiBaseUrl: "", + }, + }; +} + +export async function runKycDepositAggregateTest(t: GlobalTestState) { + // Set up test environment + + const { walletClient, bankClient, exchange, amlKeypair } = + await createKycTestkudosEnvironment(t); + + // Withdraw digital cash into the wallet. + + const wres = await withdrawViaBankV3(t, { + bankClient, + amount: "TESTKUDOS:50", + exchange: exchange, + walletClient: walletClient, + }); + + await wres.withdrawalFinishedCond; + + const depositResp = await walletClient.call( + WalletApiOperation.CreateDepositGroup, + { + amount: "TESTKUDOS:10", + depositPaytoUri: wres.accountPaytoUri, + }, + ); + + await walletClient.call(WalletApiOperation.TestingWaitTransactionState, { + transactionId: depositResp.transactionId, + txState: { + major: TransactionMajorState.Pending, + minor: TransactionMinorState.Track, + }, + }); + + await exchange.runAggregatorOnceWithTimetravel({ + timetravelMicroseconds: 1000 * 1000 * 60 * 60 * 3, + }); + + console.log("waiting for kyc-required"); + + await walletClient.call(WalletApiOperation.TestingWaitTransactionState, { + transactionId: depositResp.transactionId, + txState: { + major: TransactionMajorState.Pending, + minor: TransactionMinorState.KycRequired, + }, + }); + + const txDetails = await walletClient.call( + WalletApiOperation.GetTransactionById, + { + transactionId: depositResp.transactionId, + }, + ); + + const kycPaytoHash = txDetails.kycPaytoHash; + + t.assertTrue(!!kycPaytoHash); + + await postAmlDecisionNoRules(t, { + amlPriv: amlKeypair.priv, + amlPub: amlKeypair.pub, + exchangeBaseUrl: exchange.baseUrl, + paytoHash: kycPaytoHash, + }); + + await exchange.runAggregatorOnceWithTimetravel({ + timetravelMicroseconds: 1000 * 1000 * 60 * 60 * 3, + }); + + await exchange.runTransferOnceWithTimetravel({ + timetravelMicroseconds: 1000 * 1000 * 60 * 60 * 3, + }); + + await walletClient.call(WalletApiOperation.TestingWaitTransactionState, { + transactionId: depositResp.transactionId, + txState: { + major: TransactionMajorState.Done, + }, + }); +} + +runKycDepositAggregateTest.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/test-kyc-deposit-deposit-kyctransfer.ts b/packages/taler-harness/src/integrationtests/test-kyc-deposit-deposit-kyctransfer.ts new file mode 100644 index 000000000..b91c32a71 --- /dev/null +++ b/packages/taler-harness/src/integrationtests/test-kyc-deposit-deposit-kyctransfer.ts @@ -0,0 +1,367 @@ +/* + 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 { + Logger, + parsePaytoUri, + TalerCorebankApiClient, + TransactionMajorState, + TransactionMinorState, + TransactionType, + WireGatewayApiClient, +} from "@gnu-taler/taler-util"; +import { + createSyncCryptoApi, + EddsaKeyPairStrings, + WalletApiOperation, +} from "@gnu-taler/taler-wallet-core"; +import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js"; +import { + BankService, + DbInfo, + ExchangeService, + getTestHarnessPaytoForLabel, + GlobalTestState, + HarnessExchangeBankAccount, + setupDb, + WalletClient, + WalletService, +} from "../harness/harness.js"; +import { + EnvOptions, + postAmlDecisionNoRules, + withdrawViaBankV3, +} from "../harness/helpers.js"; + +const logger = new Logger("test-kyc-deposit-deposit.ts"); + +interface KycTestEnv { + commonDb: DbInfo; + bankClient: TalerCorebankApiClient; + exchange: ExchangeService; + exchangeBankAccount: HarnessExchangeBankAccount; + walletClient: WalletClient; + walletService: WalletService; + amlKeypair: EddsaKeyPairStrings; +} + +async function createKycTestkudosEnvironment( + t: GlobalTestState, + coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")), + opts: EnvOptions = {}, +): Promise<KycTestEnv> { + const db = await setupDb(t); + + const bank = await BankService.create(t, { + allowRegistrations: true, + currency: "TESTKUDOS", + database: db.connStr, + httpPort: 8082, + }); + + const exchange = ExchangeService.create(t, { + name: "testexchange-1", + currency: "TESTKUDOS", + httpPort: 8081, + database: db.connStr, + }); + + let receiverName = "Exchange"; + let exchangeBankUsername = "exchange"; + let exchangeBankPassword = "mypw"; + let exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername); + + const wireGatewayApiBaseUrl = new URL( + `accounts/${exchangeBankUsername}/taler-wire-gateway/`, + bank.corebankApiBaseUrl, + ).href; + + await exchange.addBankAccount("1", { + accountName: exchangeBankUsername, + accountPassword: exchangeBankPassword, + wireGatewayApiBaseUrl, + accountPaytoUri: exchangePaytoUri, + }); + + bank.setSuggestedExchange(exchange, exchangePaytoUri); + + await bank.start(); + + await bank.pingUntilAvailable(); + + const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl, { + auth: { + username: "admin", + password: "adminpw", + }, + }); + + await bankClient.registerAccountExtended({ + name: receiverName, + password: exchangeBankPassword, + username: exchangeBankUsername, + is_taler_exchange: true, + payto_uri: exchangePaytoUri, + }); + + const ageMaskSpec = opts.ageMaskSpec; + + if (ageMaskSpec) { + exchange.enableAgeRestrictions(ageMaskSpec); + // Enable age restriction for all coins. + exchange.addCoinConfigList( + coinConfig.map((x) => ({ + ...x, + name: `${x.name}-age`, + ageRestricted: true, + })), + ); + // For mixed age restrictions, we also offer coins without age restrictions + if (opts.mixedAgeRestriction) { + exchange.addCoinConfigList( + coinConfig.map((x) => ({ ...x, ageRestricted: false })), + ); + } + } else { + exchange.addCoinConfigList(coinConfig); + } + + await exchange.modifyConfig(async (config) => { + config.setString("exchange", "enable_kyc", "yes"); + + config.setString("KYC-RULE-R1", "operation_type", "deposit"); + config.setString("KYC-RULE-R1", "enabled", "yes"); + config.setString("KYC-RULE-R1", "exposed", "yes"); + config.setString("KYC-RULE-R1", "is_and_combinator", "yes"); + config.setString("KYC-RULE-R1", "threshold", "TESTKUDOS:5"); + config.setString("KYC-RULE-R1", "timeframe", "1d"); + config.setString("KYC-RULE-R1", "next_measures", "M1"); + + config.setString("KYC-MEASURE-M1", "check_name", "C1"); + config.setString("KYC-MEASURE-M1", "context", "{}"); + config.setString("KYC-MEASURE-M1", "program", "P1"); + + config.setString("AML-PROGRAM-P1", "command", "/bin/true"); + config.setString("AML-PROGRAM-P1", "enabled", "true"); + config.setString("AML-PROGRAM-P1", "description", "this does nothing"); + config.setString("AML-PROGRAM-P1", "fallback", "M1"); + + config.setString("KYC-CHECK-C1", "type", "INFO"); + config.setString("KYC-CHECK-C1", "description", "my check!"); + config.setString("KYC-CHECK-C1", "fallback", "M1"); + }); + + await exchange.start(); + + const cryptoApi = createSyncCryptoApi(); + const amlKeypair = await cryptoApi.createEddsaKeypair({}); + + await exchange.enableAmlAccount(amlKeypair.pub, "Alice"); + + const walletService = new WalletService(t, { + name: "wallet", + useInMemoryDb: true, + }); + await walletService.start(); + await walletService.pingUntilAvailable(); + + const walletClient = new WalletClient({ + name: "wallet", + unixPath: walletService.socketPath, + onNotification(n) { + console.log("got notification", n); + }, + }); + await walletClient.connect(); + await walletClient.client.call(WalletApiOperation.InitWallet, { + config: { + testing: { + skipDefaults: true, + }, + }, + }); + + console.log("setup done!"); + + return { + commonDb: db, + exchange, + amlKeypair, + walletClient, + walletService, + bankClient, + exchangeBankAccount: { + accountName: "", + accountPassword: "", + accountPaytoUri: exchangePaytoUri, + wireGatewayApiBaseUrl, + }, + }; +} + +export async function runKycDepositDepositKyctransferTest(t: GlobalTestState) { + // Set up test environment + + const { + walletClient, + bankClient, + exchange, + amlKeypair, + exchangeBankAccount, + } = await createKycTestkudosEnvironment(t); + + // Withdraw digital cash into the wallet. + + const wres = await withdrawViaBankV3(t, { + bankClient, + amount: "TESTKUDOS:50", + exchange: exchange, + walletClient: walletClient, + }); + + await wres.withdrawalFinishedCond; + + const depositPaytoUri = getTestHarnessPaytoForLabel("deposit-test"); + + await bankClient.registerAccountExtended({ + name: "deposit-test", + password: "test", + username: "deposit-test", + is_taler_exchange: false, + payto_uri: depositPaytoUri, + }); + + const depositResp = await walletClient.call( + WalletApiOperation.CreateDepositGroup, + { + amount: "TESTKUDOS:10", + depositPaytoUri, + }, + ); + + console.log("waiting for kyc-required"); + + await walletClient.call(WalletApiOperation.TestingWaitTransactionState, { + transactionId: depositResp.transactionId, + txState: { + major: TransactionMajorState.Pending, + minor: TransactionMinorState.KycAuthRequired, + }, + }); + + t.logStep("kyc-auth-requested"); + + const wireGatewayApiClient = new WireGatewayApiClient( + exchangeBankAccount.wireGatewayApiBaseUrl, + { + auth: { + username: "admin", + password: "adminpw", + }, + }, + ); + + { + const txDetails = await walletClient.call( + WalletApiOperation.GetTransactionById, + { + transactionId: depositResp.transactionId, + }, + ); + + t.assertDeepEqual(txDetails.type, TransactionType.Deposit); + const kycTx = txDetails.kycAuthTransferInfo; + t.assertTrue(!!kycTx); + + logger.info(`account pub: ${kycTx.accountPub}`); + + await wireGatewayApiClient.adminAddKycauth({ + amount: "TESTKUDOS:0.1", + debitAccountPayto: depositPaytoUri, + accountPub: kycTx.accountPub, + }); + } + + await walletClient.call(WalletApiOperation.TestingWaitTransactionState, { + transactionId: depositResp.transactionId, + txState: { + major: TransactionMajorState.Pending, + minor: TransactionMinorState.KycRequired, + }, + }); + + const txDetails = await walletClient.call( + WalletApiOperation.GetTransactionById, + { + transactionId: depositResp.transactionId, + }, + ); + + { + const kycAuthCreditPayto = + txDetails.kycAuthTransferInfo?.creditPaytoUris[0]; + t.assertTrue(!!kycAuthCreditPayto); + const p = parsePaytoUri(kycAuthCreditPayto); + t.assertTrue(!!p); + t.assertAmountEquals(p.params["amount"], "TESTKUDOS:0.01"); + } + + const kycPaytoHash = txDetails.kycPaytoHash; + + t.assertTrue(!!kycPaytoHash); + + await postAmlDecisionNoRules(t, { + amlPriv: amlKeypair.priv, + amlPub: amlKeypair.pub, + exchangeBaseUrl: exchange.baseUrl, + paytoHash: kycPaytoHash, + }); + + logger.info(`made decision to have no rules on ${kycPaytoHash}`); + + await walletClient.call(WalletApiOperation.TestingWaitTransactionState, { + transactionId: depositResp.transactionId, + txState: [ + { + major: TransactionMajorState.Done, + }, + { + major: TransactionMajorState.Pending, + minor: TransactionMinorState.Track, + }, + ], + }); + + await exchange.runAggregatorOnceWithTimetravel({ + timetravelMicroseconds: 1000 * 1000 * 60 * 60, + }); + + await exchange.runTransferOnceWithTimetravel({ + timetravelMicroseconds: 1000 * 1000 * 60 * 60, + }); + + await walletClient.call(WalletApiOperation.TestingWaitTransactionState, { + transactionId: depositResp.transactionId, + txState: { + major: TransactionMajorState.Done, + }, + }); +} + +runKycDepositDepositKyctransferTest.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/test-kyc-deposit-deposit.ts b/packages/taler-harness/src/integrationtests/test-kyc-deposit-deposit.ts new file mode 100644 index 000000000..10f34a56a --- /dev/null +++ b/packages/taler-harness/src/integrationtests/test-kyc-deposit-deposit.ts @@ -0,0 +1,297 @@ +/* + 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 { + Logger, + TalerCorebankApiClient, + TransactionMajorState, + TransactionMinorState, +} from "@gnu-taler/taler-util"; +import { + createSyncCryptoApi, + EddsaKeyPairStrings, + WalletApiOperation, +} from "@gnu-taler/taler-wallet-core"; +import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js"; +import { + BankService, + DbInfo, + ExchangeService, + getTestHarnessPaytoForLabel, + GlobalTestState, + HarnessExchangeBankAccount, + setupDb, + WalletClient, + WalletService, +} from "../harness/harness.js"; +import { + EnvOptions, + postAmlDecisionNoRules, + withdrawViaBankV3, +} from "../harness/helpers.js"; + +const logger = new Logger("test-kyc-deposit-deposit.ts"); + +interface KycTestEnv { + commonDb: DbInfo; + bankClient: TalerCorebankApiClient; + exchange: ExchangeService; + exchangeBankAccount: HarnessExchangeBankAccount; + walletClient: WalletClient; + walletService: WalletService; + amlKeypair: EddsaKeyPairStrings; +} + +async function createKycTestkudosEnvironment( + t: GlobalTestState, + coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")), + opts: EnvOptions = {}, +): Promise<KycTestEnv> { + const db = await setupDb(t); + + const bank = await BankService.create(t, { + allowRegistrations: true, + currency: "TESTKUDOS", + database: db.connStr, + httpPort: 8082, + }); + + const exchange = ExchangeService.create(t, { + name: "testexchange-1", + currency: "TESTKUDOS", + httpPort: 8081, + database: db.connStr, + }); + + let receiverName = "Exchange"; + let exchangeBankUsername = "exchange"; + let exchangeBankPassword = "mypw"; + let exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername); + + await exchange.addBankAccount("1", { + accountName: exchangeBankUsername, + accountPassword: exchangeBankPassword, + wireGatewayApiBaseUrl: new URL( + "accounts/exchange/taler-wire-gateway/", + bank.baseUrl, + ).href, + accountPaytoUri: exchangePaytoUri, + }); + + bank.setSuggestedExchange(exchange, exchangePaytoUri); + + await bank.start(); + + await bank.pingUntilAvailable(); + + const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl, { + auth: { + username: "admin", + password: "adminpw", + }, + }); + + await bankClient.registerAccountExtended({ + name: receiverName, + password: exchangeBankPassword, + username: exchangeBankUsername, + is_taler_exchange: true, + payto_uri: exchangePaytoUri, + }); + + const ageMaskSpec = opts.ageMaskSpec; + + if (ageMaskSpec) { + exchange.enableAgeRestrictions(ageMaskSpec); + // Enable age restriction for all coins. + exchange.addCoinConfigList( + coinConfig.map((x) => ({ + ...x, + name: `${x.name}-age`, + ageRestricted: true, + })), + ); + // For mixed age restrictions, we also offer coins without age restrictions + if (opts.mixedAgeRestriction) { + exchange.addCoinConfigList( + coinConfig.map((x) => ({ ...x, ageRestricted: false })), + ); + } + } else { + exchange.addCoinConfigList(coinConfig); + } + + await exchange.modifyConfig(async (config) => { + config.setString("exchange", "enable_kyc", "yes"); + + config.setString("KYC-RULE-R1", "operation_type", "deposit"); + config.setString("KYC-RULE-R1", "enabled", "yes"); + config.setString("KYC-RULE-R1", "exposed", "yes"); + config.setString("KYC-RULE-R1", "is_and_combinator", "yes"); + config.setString("KYC-RULE-R1", "threshold", "TESTKUDOS:5"); + config.setString("KYC-RULE-R1", "timeframe", "1d"); + config.setString("KYC-RULE-R1", "next_measures", "M1"); + + config.setString("KYC-MEASURE-M1", "check_name", "C1"); + config.setString("KYC-MEASURE-M1", "context", "{}"); + config.setString("KYC-MEASURE-M1", "program", "P1"); + + config.setString("AML-PROGRAM-P1", "command", "/bin/true"); + config.setString("AML-PROGRAM-P1", "enabled", "true"); + config.setString("AML-PROGRAM-P1", "description", "this does nothing"); + config.setString("AML-PROGRAM-P1", "fallback", "M1"); + + config.setString("KYC-CHECK-C1", "type", "INFO"); + config.setString("KYC-CHECK-C1", "description", "my check!"); + config.setString("KYC-CHECK-C1", "fallback", "M1"); + }); + + await exchange.start(); + + const cryptoApi = createSyncCryptoApi(); + const amlKeypair = await cryptoApi.createEddsaKeypair({}); + + await exchange.enableAmlAccount(amlKeypair.pub, "Alice"); + + const walletService = new WalletService(t, { + name: "wallet", + useInMemoryDb: true, + }); + await walletService.start(); + await walletService.pingUntilAvailable(); + + const walletClient = new WalletClient({ + name: "wallet", + unixPath: walletService.socketPath, + onNotification(n) { + console.log("got notification", n); + }, + }); + await walletClient.connect(); + await walletClient.client.call(WalletApiOperation.InitWallet, { + config: { + testing: { + skipDefaults: true, + }, + }, + }); + + console.log("setup done!"); + + return { + commonDb: db, + exchange, + amlKeypair, + walletClient, + walletService, + bankClient, + exchangeBankAccount: { + accountName: "", + accountPassword: "", + accountPaytoUri: "", + wireGatewayApiBaseUrl: "", + }, + }; +} + +export async function runKycDepositDepositTest(t: GlobalTestState) { + // Set up test environment + + const { walletClient, bankClient, exchange, amlKeypair } = + await createKycTestkudosEnvironment(t); + + // Withdraw digital cash into the wallet. + + const wres = await withdrawViaBankV3(t, { + bankClient, + amount: "TESTKUDOS:50", + exchange: exchange, + walletClient: walletClient, + }); + + await wres.withdrawalFinishedCond; + + const depositResp = await walletClient.call( + WalletApiOperation.CreateDepositGroup, + { + amount: "TESTKUDOS:10", + depositPaytoUri: wres.accountPaytoUri, + }, + ); + + console.log("waiting for kyc-required"); + + await walletClient.call(WalletApiOperation.TestingWaitTransactionState, { + transactionId: depositResp.transactionId, + txState: { + major: TransactionMajorState.Pending, + minor: TransactionMinorState.KycRequired, + }, + }); + + const txDetails = await walletClient.call( + WalletApiOperation.GetTransactionById, + { + transactionId: depositResp.transactionId, + }, + ); + + const kycPaytoHash = txDetails.kycPaytoHash; + + t.assertTrue(!!kycPaytoHash); + + await postAmlDecisionNoRules(t, { + amlPriv: amlKeypair.priv, + amlPub: amlKeypair.pub, + exchangeBaseUrl: exchange.baseUrl, + paytoHash: kycPaytoHash, + }); + + logger.info(`made decision to have no rules on ${kycPaytoHash}`); + + await walletClient.call(WalletApiOperation.TestingWaitTransactionState, { + transactionId: depositResp.transactionId, + txState: [ + { + major: TransactionMajorState.Done, + }, + { + major: TransactionMajorState.Pending, + minor: TransactionMinorState.Track, + }, + ], + }); + + await exchange.runAggregatorOnceWithTimetravel({ + timetravelMicroseconds: 1000 * 1000 * 60 * 60, + }); + + await exchange.runTransferOnceWithTimetravel({ + timetravelMicroseconds: 1000 * 1000 * 60 * 60, + }); + + await walletClient.call(WalletApiOperation.TestingWaitTransactionState, { + transactionId: depositResp.transactionId, + txState: { + major: TransactionMajorState.Done, + }, + }); +} + +runKycDepositDepositTest.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/test-kyc-exchange-wallet.ts b/packages/taler-harness/src/integrationtests/test-kyc-exchange-wallet.ts new file mode 100644 index 000000000..fbbaf382d --- /dev/null +++ b/packages/taler-harness/src/integrationtests/test-kyc-exchange-wallet.ts @@ -0,0 +1,248 @@ +/* + 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 { + encodeCrock, + ExchangeWalletKycStatus, + hashPaytoUri, + j2s, + TalerCorebankApiClient, +} from "@gnu-taler/taler-util"; +import { + createSyncCryptoApi, + EddsaKeyPairStrings, + WalletApiOperation, +} from "@gnu-taler/taler-wallet-core"; +import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js"; +import { + BankService, + DbInfo, + ExchangeService, + getTestHarnessPaytoForLabel, + GlobalTestState, + HarnessExchangeBankAccount, + setupDb, + WalletClient, + WalletService, +} from "../harness/harness.js"; +import { EnvOptions, postAmlDecisionNoRules } from "../harness/helpers.js"; + +interface KycTestEnv { + commonDb: DbInfo; + bankClient: TalerCorebankApiClient; + exchange: ExchangeService; + exchangeBankAccount: HarnessExchangeBankAccount; + walletClient: WalletClient; + walletService: WalletService; + amlKeypair: EddsaKeyPairStrings; +} + +async function createKycTestkudosEnvironment( + t: GlobalTestState, + coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")), + opts: EnvOptions = {}, +): Promise<KycTestEnv> { + const db = await setupDb(t); + + const bank = await BankService.create(t, { + allowRegistrations: true, + currency: "TESTKUDOS", + database: db.connStr, + httpPort: 8082, + }); + + const exchange = ExchangeService.create(t, { + name: "testexchange-1", + currency: "TESTKUDOS", + httpPort: 8081, + database: db.connStr, + }); + + let receiverName = "Exchange"; + let exchangeBankUsername = "exchange"; + let exchangeBankPassword = "mypw"; + let exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername); + + await exchange.addBankAccount("1", { + accountName: exchangeBankUsername, + accountPassword: exchangeBankPassword, + wireGatewayApiBaseUrl: new URL( + "accounts/exchange/taler-wire-gateway/", + bank.baseUrl, + ).href, + accountPaytoUri: exchangePaytoUri, + }); + + bank.setSuggestedExchange(exchange, exchangePaytoUri); + + await bank.start(); + + await bank.pingUntilAvailable(); + + const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl, { + auth: { + username: "admin", + password: "adminpw", + }, + }); + + await bankClient.registerAccountExtended({ + name: receiverName, + password: exchangeBankPassword, + username: exchangeBankUsername, + is_taler_exchange: true, + payto_uri: exchangePaytoUri, + }); + + exchange.addCoinConfigList(coinConfig); + + await exchange.modifyConfig(async (config) => { + config.setString("exchange", "enable_kyc", "yes"); + + config.setString("KYC-RULE-R1", "operation_type", "balance"); + config.setString("KYC-RULE-R1", "enabled", "yes"); + config.setString("KYC-RULE-R1", "exposed", "yes"); + config.setString("KYC-RULE-R1", "is_and_combinator", "yes"); + config.setString("KYC-RULE-R1", "threshold", "TESTKUDOS:5"); + config.setString("KYC-RULE-R1", "timeframe", "forever"); + config.setString("KYC-RULE-R1", "next_measures", "M1"); + + config.setString("KYC-MEASURE-M1", "check_name", "C1"); + config.setString("KYC-MEASURE-M1", "context", "{}"); + config.setString("KYC-MEASURE-M1", "program", "P1"); + + config.setString("AML-PROGRAM-P1", "command", "/bin/true"); + config.setString("AML-PROGRAM-P1", "enabled", "true"); + config.setString("AML-PROGRAM-P1", "description", "this does nothing"); + config.setString("AML-PROGRAM-P1", "fallback", "M1"); + + config.setString("KYC-CHECK-C1", "type", "INFO"); + config.setString("KYC-CHECK-C1", "description", "my check!"); + config.setString("KYC-CHECK-C1", "fallback", "M1"); + }); + + await exchange.start(); + + const cryptoApi = createSyncCryptoApi(); + const amlKeypair = await cryptoApi.createEddsaKeypair({}); + + await exchange.enableAmlAccount(amlKeypair.pub, "Alice"); + + const walletService = new WalletService(t, { + name: "wallet", + useInMemoryDb: true, + }); + await walletService.start(); + await walletService.pingUntilAvailable(); + + const walletClient = new WalletClient({ + name: "wallet", + unixPath: walletService.socketPath, + onNotification(n) { + console.log("got notification", n); + }, + }); + await walletClient.connect(); + await walletClient.client.call(WalletApiOperation.InitWallet, { + config: { + testing: { + skipDefaults: true, + }, + }, + }); + + console.log("setup done!"); + + return { + commonDb: db, + exchange, + walletClient, + walletService, + bankClient, + exchangeBankAccount: { + accountName: "", + accountPassword: "", + accountPaytoUri: "", + wireGatewayApiBaseUrl: "", + }, + amlKeypair, + }; +} + +export async function runKycExchangeWalletTest(t: GlobalTestState) { + // Set up test environment + + const { walletClient, exchange, amlKeypair } = + await createKycTestkudosEnvironment(t); + + await walletClient.call(WalletApiOperation.AddExchange, { + exchangeBaseUrl: exchange.baseUrl, + }); + + await walletClient.call(WalletApiOperation.StartExchangeWalletKyc, { + amount: "TESTKUDOS:20", + exchangeBaseUrl: exchange.baseUrl, + }); + + await walletClient.call(WalletApiOperation.TestingWaitExchangeWalletKyc, { + amount: "TESTKUDOS:20", + exchangeBaseUrl: exchange.baseUrl, + passed: false, + }); + + const exchangeEntry = await walletClient.call( + WalletApiOperation.GetExchangeEntryByUrl, + { + exchangeBaseUrl: exchange.baseUrl, + }, + ); + + console.log(j2s(exchangeEntry)); + + t.assertDeepEqual( + exchangeEntry.walletKycStatus, + ExchangeWalletKycStatus.Legi, + ); + + const kycReservePub = exchangeEntry.walletKycReservePub; + + t.assertTrue(!!kycReservePub); + + // FIXME: Create/user helper function for this! + const hPayto = hashPaytoUri( + `payto://taler-reserve-http/localhost:${exchange.port}/${kycReservePub}`, + ); + + console.log(`hPayto: ${hPayto}`); + + await postAmlDecisionNoRules(t, { + amlPriv: amlKeypair.priv, + amlPub: amlKeypair.pub, + exchangeBaseUrl: exchange.baseUrl, + paytoHash: encodeCrock(hPayto), + }); + + await walletClient.call(WalletApiOperation.TestingWaitExchangeWalletKyc, { + amount: "TESTKUDOS:20", + exchangeBaseUrl: exchange.baseUrl, + passed: true, + }); +} + +runKycExchangeWalletTest.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/test-kyc-form-withdrawal.ts b/packages/taler-harness/src/integrationtests/test-kyc-form-withdrawal.ts new file mode 100644 index 000000000..6a8a13ab9 --- /dev/null +++ b/packages/taler-harness/src/integrationtests/test-kyc-form-withdrawal.ts @@ -0,0 +1,311 @@ +/* + 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 { + codecForAny, + codecForKycProcessClientInformation, + decodeCrock, + encodeCrock, + j2s, + signAmlQuery, + TalerCorebankApiClient, + TransactionIdStr, + TransactionMajorState, + TransactionMinorState, +} from "@gnu-taler/taler-util"; +import { readResponseJsonOrThrow } from "@gnu-taler/taler-util/http"; +import { + createSyncCryptoApi, + EddsaKeyPairStrings, + WalletApiOperation, +} from "@gnu-taler/taler-wallet-core"; +import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js"; +import { + BankService, + DbInfo, + ExchangeService, + getTestHarnessPaytoForLabel, + GlobalTestState, + HarnessExchangeBankAccount, + harnessHttpLib, + setupDb, + WalletClient, + WalletService, +} from "../harness/harness.js"; +import { EnvOptions, withdrawViaBankV3 } from "../harness/helpers.js"; + +interface KycTestEnv { + commonDb: DbInfo; + bankClient: TalerCorebankApiClient; + exchange: ExchangeService; + exchangeBankAccount: HarnessExchangeBankAccount; + walletClient: WalletClient; + walletService: WalletService; + amlKeypair: EddsaKeyPairStrings; +} + +async function createKycTestkudosEnvironment( + t: GlobalTestState, + coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")), + opts: EnvOptions = {}, +): Promise<KycTestEnv> { + const db = await setupDb(t); + + const bank = await BankService.create(t, { + allowRegistrations: true, + currency: "TESTKUDOS", + database: db.connStr, + httpPort: 8082, + }); + + const exchange = ExchangeService.create(t, { + name: "testexchange-1", + currency: "TESTKUDOS", + httpPort: 8081, + database: db.connStr, + }); + + let receiverName = "Exchange"; + let exchangeBankUsername = "exchange"; + let exchangeBankPassword = "mypw"; + let exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername); + + await exchange.addBankAccount("1", { + accountName: exchangeBankUsername, + accountPassword: exchangeBankPassword, + wireGatewayApiBaseUrl: new URL( + "accounts/exchange/taler-wire-gateway/", + bank.baseUrl, + ).href, + accountPaytoUri: exchangePaytoUri, + }); + + bank.setSuggestedExchange(exchange, exchangePaytoUri); + + await bank.start(); + + await bank.pingUntilAvailable(); + + const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl, { + auth: { + username: "admin", + password: "adminpw", + }, + }); + + await bankClient.registerAccountExtended({ + name: receiverName, + password: exchangeBankPassword, + username: exchangeBankUsername, + is_taler_exchange: true, + payto_uri: exchangePaytoUri, + }); + + exchange.addCoinConfigList(coinConfig); + + await exchange.modifyConfig(async (config) => { + config.setString("exchange", "enable_kyc", "yes"); + + config.setString("KYC-RULE-R1", "operation_type", "withdraw"); + config.setString("KYC-RULE-R1", "enabled", "yes"); + config.setString("KYC-RULE-R1", "exposed", "yes"); + config.setString("KYC-RULE-R1", "is_and_combinator", "yes"); + config.setString("KYC-RULE-R1", "threshold", "TESTKUDOS:5"); + config.setString("KYC-RULE-R1", "timeframe", "1d"); + config.setString("KYC-RULE-R1", "next_measures", "M1 M2"); + + config.setString("KYC-MEASURE-M1", "check_name", "C1"); + config.setString("KYC-MEASURE-M1", "context", "{}"); + config.setString("KYC-MEASURE-M1", "program", "P1"); + + config.setString("KYC-MEASURE-M2", "check_name", "C2"); + config.setString("KYC-MEASURE-M2", "context", "{}"); + config.setString("KYC-MEASURE-M2", "program", "P2"); + + config.setString( + "AML-PROGRAM-P1", + "command", + "taler-exchange-helper-measure-test-form", + ); + config.setString("AML-PROGRAM-P1", "enabled", "true"); + config.setString( + "AML-PROGRAM-P1", + "description", + "test for full_name and birthdate", + ); + config.setString("AML-PROGRAM-P1", "description_i18n", "{}"); + config.setString("AML-PROGRAM-P1", "fallback", "M1"); + + config.setString("AML-PROGRAM-P2", "command", "/bin/true"); + config.setString("AML-PROGRAM-P2", "enabled", "true"); + config.setString("AML-PROGRAM-P2", "description", "does nothing"); + config.setString("AML-PROGRAM-P2", "description_i18n", "{}"); + config.setString("AML-PROGRAM-P2", "fallback", "M1"); + + config.setString("KYC-CHECK-C1", "type", "FORM"); + config.setString("KYC-CHECK-C1", "form_name", "myform"); + config.setString("KYC-CHECK-C1", "description", "my check!"); + config.setString("KYC-CHECK-C1", "description_i18n", "{}"); + config.setString("KYC-CHECK-C1", "outputs", "full_name birthdate"); + config.setString("KYC-CHECK-C1", "fallback", "M1"); + + config.setString("KYC-CHECK-C2", "type", "INFO"); + config.setString("KYC-CHECK-C2", "description", "my check info!"); + config.setString("KYC-CHECK-C2", "description_i18n", "{}"); + config.setString("KYC-CHECK-C2", "fallback", "M2"); + }); + + await exchange.start(); + + const cryptoApi = createSyncCryptoApi(); + const amlKeypair = await cryptoApi.createEddsaKeypair({}); + + await exchange.enableAmlAccount(amlKeypair.pub, "Alice"); + + const walletService = new WalletService(t, { + name: "wallet", + useInMemoryDb: true, + }); + await walletService.start(); + await walletService.pingUntilAvailable(); + + const walletClient = new WalletClient({ + name: "wallet", + unixPath: walletService.socketPath, + onNotification(n) { + console.log("got notification", n); + }, + }); + await walletClient.connect(); + await walletClient.client.call(WalletApiOperation.InitWallet, { + config: { + testing: { + skipDefaults: true, + }, + }, + }); + + console.log("setup done!"); + + return { + commonDb: db, + exchange, + amlKeypair, + walletClient, + walletService, + bankClient, + exchangeBankAccount: { + accountName: "", + accountPassword: "", + accountPaytoUri: "", + wireGatewayApiBaseUrl: "", + }, + }; +} + +export async function runKycFormWithdrawalTest(t: GlobalTestState) { + // Set up test environment + + const { walletClient, bankClient, exchange, amlKeypair } = + await createKycTestkudosEnvironment(t); + + // Withdraw digital cash into the wallet. + + const wres = await withdrawViaBankV3(t, { + amount: "TESTKUDOS:20", + bankClient, + exchange, + walletClient, + }); + + await walletClient.call(WalletApiOperation.TestingWaitTransactionState, { + transactionId: wres.transactionId as TransactionIdStr, + txState: { + major: TransactionMajorState.Pending, + minor: TransactionMinorState.KycRequired, + }, + }); + + const txDetails = await walletClient.call( + WalletApiOperation.GetTransactionById, + { + transactionId: wres.transactionId, + }, + ); + + console.log(j2s(txDetails)); + const accessToken = txDetails.kycAccessToken; + t.assertTrue(!!accessToken); + + const infoResp = await harnessHttpLib.fetch( + new URL(`kyc-info/${txDetails.kycAccessToken}`, exchange.baseUrl).href, + ); + + const clientInfo = await readResponseJsonOrThrow( + infoResp, + codecForKycProcessClientInformation(), + ); + + console.log(j2s(clientInfo)); + + const kycId = clientInfo.requirements.find((x) => x.id != null)?.id; + t.assertTrue(!!kycId); + + const uploadResp = await harnessHttpLib.fetch( + new URL(`kyc-upload/${kycId}`, exchange.baseUrl).href, + { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: "full_name=Alice+Abc&birthdate=2000-01-01", + }, + ); + + console.log("resp status", uploadResp.status); + + t.assertDeepEqual(uploadResp.status, 204); + + const sig = signAmlQuery(decodeCrock(amlKeypair.priv)); + + const decisionsResp = await harnessHttpLib.fetch( + new URL(`aml/${amlKeypair.pub}/decisions`, exchange.baseUrl).href, + { + headers: { + "Taler-AML-Officer-Signature": encodeCrock(sig), + }, + }, + ); + + const decisions = await readResponseJsonOrThrow(decisionsResp, codecForAny()); + console.log(j2s(decisions)); + + t.assertDeepEqual(decisionsResp.status, 200); + + // KYC should pass now + + await walletClient.call(WalletApiOperation.TestingWaitTransactionState, { + transactionId: wres.transactionId as TransactionIdStr, + txState: { + major: TransactionMajorState.Done, + }, + }); +} + +runKycFormWithdrawalTest.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/test-kyc-merchant-aggregate.ts b/packages/taler-harness/src/integrationtests/test-kyc-merchant-aggregate.ts new file mode 100644 index 000000000..3b32656ae --- /dev/null +++ b/packages/taler-harness/src/integrationtests/test-kyc-merchant-aggregate.ts @@ -0,0 +1,292 @@ +/* + 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 { Duration, j2s, TalerCorebankApiClient } from "@gnu-taler/taler-util"; +import { + createSyncCryptoApi, + EddsaKeyPairStrings, + WalletApiOperation, +} from "@gnu-taler/taler-wallet-core"; +import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js"; +import { + BankService, + DbInfo, + ExchangeService, + getTestHarnessPaytoForLabel, + GlobalTestState, + HarnessExchangeBankAccount, + harnessHttpLib, + MerchantService, + setupDb, + WalletClient, + WalletService, +} from "../harness/harness.js"; +import { + EnvOptions, + makeTestPaymentV2, + withdrawViaBankV3, +} from "../harness/helpers.js"; + +interface KycTestEnv { + commonDb: DbInfo; + bankClient: TalerCorebankApiClient; + exchange: ExchangeService; + exchangeBankAccount: HarnessExchangeBankAccount; + walletClient: WalletClient; + walletService: WalletService; + amlKeypair: EddsaKeyPairStrings; + merchant: MerchantService; +} + +async function createKycTestkudosEnvironment( + t: GlobalTestState, + coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")), + opts: EnvOptions = {}, +): Promise<KycTestEnv> { + const db = await setupDb(t); + + const bank = await BankService.create(t, { + allowRegistrations: true, + currency: "TESTKUDOS", + database: db.connStr, + httpPort: 8082, + }); + + const exchange = ExchangeService.create(t, { + name: "testexchange-1", + currency: "TESTKUDOS", + httpPort: 8081, + database: db.connStr, + }); + + let receiverName = "Exchange"; + let exchangeBankUsername = "exchange"; + let exchangeBankPassword = "mypw"; + let exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername); + + await exchange.addBankAccount("1", { + accountName: exchangeBankUsername, + accountPassword: exchangeBankPassword, + wireGatewayApiBaseUrl: new URL( + "accounts/exchange/taler-wire-gateway/", + bank.baseUrl, + ).href, + accountPaytoUri: exchangePaytoUri, + }); + + bank.setSuggestedExchange(exchange, exchangePaytoUri); + + await bank.start(); + + const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl, { + auth: { + username: "admin", + password: "adminpw", + }, + }); + + await bankClient.registerAccountExtended({ + name: receiverName, + password: exchangeBankPassword, + username: exchangeBankUsername, + is_taler_exchange: true, + payto_uri: exchangePaytoUri, + }); + + const ageMaskSpec = opts.ageMaskSpec; + + if (ageMaskSpec) { + exchange.enableAgeRestrictions(ageMaskSpec); + // Enable age restriction for all coins. + exchange.addCoinConfigList( + coinConfig.map((x) => ({ + ...x, + name: `${x.name}-age`, + ageRestricted: true, + })), + ); + // For mixed age restrictions, we also offer coins without age restrictions + if (opts.mixedAgeRestriction) { + exchange.addCoinConfigList( + coinConfig.map((x) => ({ ...x, ageRestricted: false })), + ); + } + } else { + exchange.addCoinConfigList(coinConfig); + } + + await exchange.modifyConfig(async (config) => { + config.setString("exchange", "enable_kyc", "yes"); + + config.setString("KYC-RULE-R1", "operation_type", "aggregate"); + config.setString("KYC-RULE-R1", "enabled", "yes"); + config.setString("KYC-RULE-R1", "exposed", "yes"); + config.setString("KYC-RULE-R1", "is_and_combinator", "yes"); + config.setString("KYC-RULE-R1", "threshold", "TESTKUDOS:5"); + config.setString("KYC-RULE-R1", "timeframe", "1d"); + config.setString("KYC-RULE-R1", "next_measures", "M1"); + + config.setString("KYC-MEASURE-M1", "check_name", "C1"); + config.setString("KYC-MEASURE-M1", "context", "{}"); + config.setString("KYC-MEASURE-M1", "program", "P1"); + + config.setString("AML-PROGRAM-P1", "command", "/bin/true"); + config.setString("AML-PROGRAM-P1", "enabled", "true"); + config.setString("AML-PROGRAM-P1", "description", "this does nothing"); + config.setString("AML-PROGRAM-P1", "fallback", "M1"); + + config.setString("KYC-CHECK-C1", "type", "INFO"); + config.setString("KYC-CHECK-C1", "description", "my check!"); + config.setString("KYC-CHECK-C1", "fallback", "M1"); + }); + + await exchange.start(); + + const cryptoApi = createSyncCryptoApi(); + const amlKeypair = await cryptoApi.createEddsaKeypair({}); + + await exchange.enableAmlAccount(amlKeypair.pub, "Alice"); + + const walletService = new WalletService(t, { + name: "wallet", + useInMemoryDb: true, + }); + await walletService.start(); + await walletService.pingUntilAvailable(); + + const walletClient = new WalletClient({ + name: "wallet", + unixPath: walletService.socketPath, + onNotification(n) { + console.log("got notification", n); + }, + }); + await walletClient.connect(); + await walletClient.client.call(WalletApiOperation.InitWallet, { + config: { + testing: { + skipDefaults: true, + }, + }, + }); + + const merchant = await MerchantService.create(t, { + name: "testmerchant-1", + currency: "TESTKUDOS", + httpPort: 8083, + database: db.connStr, + }); + + merchant.addExchange(exchange); + + if (opts.additionalMerchantConfig) { + opts.additionalMerchantConfig(merchant); + } + await merchant.start(); + await merchant.pingUntilAvailable(); + + await merchant.addInstanceWithWireAccount({ + id: "default", + name: "Default Instance", + paytoUris: [getTestHarnessPaytoForLabel("merchant-default")], + defaultWireTransferDelay: Duration.toTalerProtocolDuration( + Duration.fromSpec({ minutes: 1 }), + ), + }); + + await merchant.addInstanceWithWireAccount({ + id: "minst1", + name: "minst1", + paytoUris: [getTestHarnessPaytoForLabel("minst1")], + defaultWireTransferDelay: Duration.toTalerProtocolDuration( + Duration.fromSpec({ minutes: 1 }), + ), + }); + + console.log("setup done!"); + + return { + commonDb: db, + exchange, + amlKeypair, + walletClient, + walletService, + bankClient, + merchant, + exchangeBankAccount: { + accountName: "", + accountPassword: "", + accountPaytoUri: "", + wireGatewayApiBaseUrl: "", + }, + }; +} + +export async function runKycMerchantAggregateTest(t: GlobalTestState) { + // Set up test environment + + const { merchant, walletClient, bankClient, exchange, amlKeypair } = + await createKycTestkudosEnvironment(t); + + // Withdraw digital cash into the wallet. + + const wres = await withdrawViaBankV3(t, { + bankClient, + amount: "TESTKUDOS:50", + exchange: exchange, + walletClient: walletClient, + }); + + await wres.withdrawalFinishedCond; + + await makeTestPaymentV2(t, { + merchant, + walletClient, + order: { + amount: "TESTKUDOS:20", + summary: "hello", + }, + }); + + await exchange.runAggregatorOnceWithTimetravel({ + timetravelMicroseconds: 1000 * 1000 * 60 * 60, + }); + + await exchange.runTransferOnceWithTimetravel({ + timetravelMicroseconds: 1000 * 1000 * 60 * 60, + }); + + t.logStep("start-run-kyccheck"); + + await merchant.runReconciliationOnceWithTimetravel({ + timetravelMicroseconds: 1000 * 1000 * 60 * 60, + }); + + t.logStep("start-request-kyc"); + const kycStatusUrl = new URL("private/kyc", merchant.makeInstanceBaseUrl()); + const resp = await harnessHttpLib.fetch(kycStatusUrl.href); + + console.log(`mechant kyc status: ${resp.status}`); + + t.assertDeepEqual(resp.status, 200); + + console.log(j2s(await resp.json())); +} + +runKycMerchantAggregateTest.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/test-kyc-merchant-deposit.ts b/packages/taler-harness/src/integrationtests/test-kyc-merchant-deposit.ts new file mode 100644 index 000000000..73449ecb7 --- /dev/null +++ b/packages/taler-harness/src/integrationtests/test-kyc-merchant-deposit.ts @@ -0,0 +1,413 @@ +/* + 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 { + codecForAccountKycRedirects, + codecForKycProcessClientInformation, + codecForQueryInstancesResponse, + Duration, + encodeCrock, + hashPaytoUri, + j2s, + Logger, + MerchantAccountKycRedirectsResponse, + TalerCorebankApiClient, + WireGatewayApiClient, +} from "@gnu-taler/taler-util"; +import { + readResponseJsonOrThrow, + readSuccessResponseJsonOrThrow, +} from "@gnu-taler/taler-util/http"; +import { + createSyncCryptoApi, + EddsaKeyPairStrings, + WalletApiOperation, +} from "@gnu-taler/taler-wallet-core"; +import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js"; +import { + BankService, + DbInfo, + ExchangeService, + getTestHarnessPaytoForLabel, + GlobalTestState, + HarnessExchangeBankAccount, + harnessHttpLib, + MerchantService, + setupDb, + WalletClient, + WalletService, +} from "../harness/harness.js"; +import { + EnvOptions, + postAmlDecisionNoRules, + withdrawViaBankV3, +} from "../harness/helpers.js"; + +const logger = new Logger(`test-kyc-merchant-deposit.ts`); + +interface KycTestEnv { + commonDb: DbInfo; + bankClient: TalerCorebankApiClient; + exchange: ExchangeService; + exchangeBankAccount: HarnessExchangeBankAccount; + walletClient: WalletClient; + walletService: WalletService; + amlKeypair: EddsaKeyPairStrings; + merchant: MerchantService; +} + +async function createKycTestkudosEnvironment( + t: GlobalTestState, + coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")), + opts: EnvOptions = {}, +): Promise<KycTestEnv> { + const db = await setupDb(t); + + const bank = await BankService.create(t, { + allowRegistrations: true, + currency: "TESTKUDOS", + database: db.connStr, + httpPort: 8082, + }); + + const exchange = ExchangeService.create(t, { + name: "testexchange-1", + currency: "TESTKUDOS", + httpPort: 8081, + database: db.connStr, + }); + + let receiverName = "Exchange"; + let exchangeBankUsername = "exchange"; + let exchangeBankPassword = "mypw"; + let exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername); + + const wireGatewayApiBaseUrl = new URL( + `accounts/${exchangeBankUsername}/taler-wire-gateway/`, + bank.corebankApiBaseUrl, + ).href; + + await exchange.addBankAccount("1", { + accountName: exchangeBankUsername, + accountPassword: exchangeBankPassword, + wireGatewayApiBaseUrl, + accountPaytoUri: exchangePaytoUri, + }); + + bank.setSuggestedExchange(exchange, exchangePaytoUri); + + await bank.start(); + + const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl, { + auth: { + username: "admin", + password: "adminpw", + }, + }); + + await bankClient.registerAccountExtended({ + name: receiverName, + password: exchangeBankPassword, + username: exchangeBankUsername, + is_taler_exchange: true, + payto_uri: exchangePaytoUri, + }); + + const ageMaskSpec = opts.ageMaskSpec; + + if (ageMaskSpec) { + exchange.enableAgeRestrictions(ageMaskSpec); + // Enable age restriction for all coins. + exchange.addCoinConfigList( + coinConfig.map((x) => ({ + ...x, + name: `${x.name}-age`, + ageRestricted: true, + })), + ); + // For mixed age restrictions, we also offer coins without age restrictions + if (opts.mixedAgeRestriction) { + exchange.addCoinConfigList( + coinConfig.map((x) => ({ ...x, ageRestricted: false })), + ); + } + } else { + exchange.addCoinConfigList(coinConfig); + } + + await exchange.modifyConfig(async (config) => { + config.setString("exchange", "enable_kyc", "yes"); + + config.setString("KYC-RULE-R1", "operation_type", "deposit"); + config.setString("KYC-RULE-R1", "enabled", "yes"); + config.setString("KYC-RULE-R1", "exposed", "yes"); + config.setString("KYC-RULE-R1", "is_and_combinator", "yes"); + config.setString("KYC-RULE-R1", "threshold", "TESTKUDOS:0"); + config.setString("KYC-RULE-R1", "timeframe", "1d"); + config.setString("KYC-RULE-R1", "next_measures", "M1"); + + config.setString("KYC-MEASURE-M1", "check_name", "C1"); + config.setString("KYC-MEASURE-M1", "context", "{}"); + config.setString("KYC-MEASURE-M1", "program", "P1"); + + config.setString("AML-PROGRAM-P1", "command", "/bin/true"); + config.setString("AML-PROGRAM-P1", "enabled", "true"); + config.setString("AML-PROGRAM-P1", "description", "this does nothing"); + config.setString("AML-PROGRAM-P1", "fallback", "M1"); + + config.setString("KYC-CHECK-C1", "type", "INFO"); + config.setString("KYC-CHECK-C1", "description", "my check!"); + config.setString("KYC-CHECK-C1", "fallback", "M1"); + }); + + await exchange.start(); + + const cryptoApi = createSyncCryptoApi(); + const amlKeypair = await cryptoApi.createEddsaKeypair({}); + + await exchange.enableAmlAccount(amlKeypair.pub, "Alice"); + + const walletService = new WalletService(t, { + name: "wallet", + useInMemoryDb: true, + }); + await walletService.start(); + await walletService.pingUntilAvailable(); + + const walletClient = new WalletClient({ + name: "wallet", + unixPath: walletService.socketPath, + onNotification(n) { + console.log("got notification", n); + }, + }); + await walletClient.connect(); + await walletClient.client.call(WalletApiOperation.InitWallet, { + config: { + testing: { + skipDefaults: true, + }, + }, + }); + + const merchant = await MerchantService.create(t, { + name: "testmerchant-1", + currency: "TESTKUDOS", + httpPort: 8083, + database: db.connStr, + }); + + merchant.addExchange(exchange); + + if (opts.additionalMerchantConfig) { + opts.additionalMerchantConfig(merchant); + } + await merchant.start(); + await merchant.pingUntilAvailable(); + + await merchant.addInstanceWithWireAccount({ + id: "default", + name: "Default Instance", + paytoUris: [getTestHarnessPaytoForLabel("merchant-default")], + defaultWireTransferDelay: Duration.toTalerProtocolDuration( + Duration.fromSpec({ minutes: 1 }), + ), + }); + + await merchant.addInstanceWithWireAccount({ + id: "minst1", + name: "minst1", + paytoUris: [getTestHarnessPaytoForLabel("minst1")], + defaultWireTransferDelay: Duration.toTalerProtocolDuration( + Duration.fromSpec({ minutes: 1 }), + ), + }); + + console.log("setup done!"); + + return { + commonDb: db, + exchange, + amlKeypair, + walletClient, + walletService, + bankClient, + merchant, + exchangeBankAccount: { + accountName: "", + accountPassword: "", + accountPaytoUri: "", + wireGatewayApiBaseUrl, + }, + }; +} + +export async function runKycMerchantDepositTest(t: GlobalTestState) { + // Set up test environment + + const { + merchant, + walletClient, + bankClient, + exchange, + exchangeBankAccount, + amlKeypair, + } = await createKycTestkudosEnvironment(t); + + let accountPub: string; + + { + const instanceUrl = new URL("private", merchant.makeInstanceBaseUrl()); + const resp = await harnessHttpLib.fetch(instanceUrl.href); + const parsedResp = await readSuccessResponseJsonOrThrow( + resp, + codecForQueryInstancesResponse(), + ); + accountPub = parsedResp.merchant_pub; + } + + const wireGatewayApiClient = new WireGatewayApiClient( + exchangeBankAccount.wireGatewayApiBaseUrl, + { + auth: { + username: "admin", + password: "adminpw", + }, + }, + ); + + // Withdraw digital cash into the wallet. + + const wres = await withdrawViaBankV3(t, { + bankClient, + amount: "TESTKUDOS:50", + exchange: exchange, + walletClient: walletClient, + }); + + await wres.withdrawalFinishedCond; + + let kycRespOne: MerchantAccountKycRedirectsResponse | undefined = undefined; + + while (1) { + const kycStatusUrl = new URL("private/kyc", merchant.makeInstanceBaseUrl()) + .href; + logger.info(`requesting GET ${kycStatusUrl}`); + const resp = await harnessHttpLib.fetch(kycStatusUrl); + if (resp.status === 200) { + kycRespOne = await readSuccessResponseJsonOrThrow( + resp, + codecForAccountKycRedirects(), + ); + break; + } + // Wait 500ms + await new Promise<void>((resolve) => { + setTimeout(() => resolve(), 500); + }); + } + + t.assertTrue(!!kycRespOne); + + logger.info(`mechant kyc status: ${j2s(kycRespOne)}`); + + await wireGatewayApiClient.adminAddKycauth({ + amount: "TESTKUDOS:0.1", + debitAccountPayto: kycRespOne.kyc_data[0].payto_uri, + accountPub, + }); + + let kycRespTwo: MerchantAccountKycRedirectsResponse | undefined = undefined; + + // We do this in a loop as a work-around. + // Not exactly the correct behavior from the merchant right now. + while (true) { + const kycStatusLongpollUrl = new URL( + "private/kyc", + merchant.makeInstanceBaseUrl(), + ); + kycStatusLongpollUrl.searchParams.set("lpt", "1"); + const resp = await harnessHttpLib.fetch(kycStatusLongpollUrl.href); + t.assertDeepEqual(resp.status, 200); + const parsedResp = await readSuccessResponseJsonOrThrow( + resp, + codecForAccountKycRedirects(), + ); + logger.info(`kyc resp 2: ${j2s(parsedResp)}`); + if (parsedResp.kyc_data[0].payto_kycauths == null) { + kycRespTwo = parsedResp; + break; + } + // Wait 500ms + await new Promise<void>((resolve) => { + setTimeout(() => resolve(), 500); + }); + } + + t.assertTrue(!!kycRespTwo); + + await postAmlDecisionNoRules(t, { + amlPriv: amlKeypair.priv, + amlPub: amlKeypair.pub, + exchangeBaseUrl: exchange.baseUrl, + paytoHash: encodeCrock(hashPaytoUri(kycRespTwo.kyc_data[0].payto_uri)), + }); + + // We do this in a loop as a work-around. + // Not exactly the correct behavior from the merchant right now. + while (true) { + const kycStatusLongpollUrl = new URL( + "private/kyc", + merchant.makeInstanceBaseUrl(), + ); + kycStatusLongpollUrl.searchParams.set("lpt", "3"); + const resp = await harnessHttpLib.fetch(kycStatusLongpollUrl.href); + t.assertDeepEqual(resp.status, 200); + const parsedResp = await readSuccessResponseJsonOrThrow( + resp, + codecForAccountKycRedirects(), + ); + logger.info(`kyc resp 3: ${j2s(parsedResp)}`); + if ((parsedResp.kyc_data[0].limits?.length ?? 0) == 0) { + break; + } + + const accessToken = parsedResp.kyc_data[0].access_token; + + t.assertTrue(!!accessToken); + + const infoResp = await harnessHttpLib.fetch( + new URL(`kyc-info/${accessToken}`, exchange.baseUrl).href, + ); + + const clientInfo = await readResponseJsonOrThrow( + infoResp, + codecForKycProcessClientInformation(), + ); + + logger.info(`kyc-info: ${j2s(clientInfo)}`); + + // Wait 500ms + await new Promise<void>((resolve) => { + setTimeout(() => resolve(), 500); + }); + } +} + +runKycMerchantDepositTest.suites = ["wallet", "merchant", "kyc"]; diff --git a/packages/taler-harness/src/integrationtests/test-kyc-new-measure.ts b/packages/taler-harness/src/integrationtests/test-kyc-new-measure.ts new file mode 100644 index 000000000..c0d3881a0 --- /dev/null +++ b/packages/taler-harness/src/integrationtests/test-kyc-new-measure.ts @@ -0,0 +1,410 @@ +/* + 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 { + codecForAny, + codecForKycProcessClientInformation, + decodeCrock, + encodeCrock, + j2s, + signAmlQuery, + TalerCorebankApiClient, + TalerProtocolTimestamp, + TransactionIdStr, + TransactionMajorState, + TransactionMinorState, +} from "@gnu-taler/taler-util"; +import { readResponseJsonOrThrow } from "@gnu-taler/taler-util/http"; +import { + createSyncCryptoApi, + EddsaKeyPairStrings, + WalletApiOperation, +} from "@gnu-taler/taler-wallet-core"; +import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js"; +import { + BankService, + DbInfo, + ExchangeService, + getTestHarnessPaytoForLabel, + GlobalTestState, + HarnessExchangeBankAccount, + harnessHttpLib, + setupDb, + WalletClient, + WalletService, +} from "../harness/harness.js"; +import { + EnvOptions, + postAmlDecision, + withdrawViaBankV3, +} from "../harness/helpers.js"; + +interface KycTestEnv { + commonDb: DbInfo; + bankClient: TalerCorebankApiClient; + exchange: ExchangeService; + exchangeBankAccount: HarnessExchangeBankAccount; + walletClient: WalletClient; + walletService: WalletService; + amlKeypair: EddsaKeyPairStrings; +} + +async function createKycTestkudosEnvironment( + t: GlobalTestState, + coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")), + opts: EnvOptions = {}, +): Promise<KycTestEnv> { + const db = await setupDb(t); + + const bank = await BankService.create(t, { + allowRegistrations: true, + currency: "TESTKUDOS", + database: db.connStr, + httpPort: 8082, + }); + + const exchange = ExchangeService.create(t, { + name: "testexchange-1", + currency: "TESTKUDOS", + httpPort: 8081, + database: db.connStr, + }); + + let receiverName = "Exchange"; + let exchangeBankUsername = "exchange"; + let exchangeBankPassword = "mypw"; + let exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername); + + await exchange.addBankAccount("1", { + accountName: exchangeBankUsername, + accountPassword: exchangeBankPassword, + wireGatewayApiBaseUrl: new URL( + "accounts/exchange/taler-wire-gateway/", + bank.baseUrl, + ).href, + accountPaytoUri: exchangePaytoUri, + }); + + bank.setSuggestedExchange(exchange, exchangePaytoUri); + + await bank.start(); + + await bank.pingUntilAvailable(); + + const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl, { + auth: { + username: "admin", + password: "adminpw", + }, + }); + + await bankClient.registerAccountExtended({ + name: receiverName, + password: exchangeBankPassword, + username: exchangeBankUsername, + is_taler_exchange: true, + payto_uri: exchangePaytoUri, + }); + + exchange.addCoinConfigList(coinConfig); + + await exchange.modifyConfig(async (config) => { + config.setString("exchange", "enable_kyc", "yes"); + + config.setString("KYC-RULE-R1", "operation_type", "withdraw"); + config.setString("KYC-RULE-R1", "enabled", "yes"); + config.setString("KYC-RULE-R1", "exposed", "yes"); + config.setString("KYC-RULE-R1", "is_and_combinator", "yes"); + config.setString("KYC-RULE-R1", "threshold", "TESTKUDOS:5"); + config.setString("KYC-RULE-R1", "timeframe", "1d"); + config.setString("KYC-RULE-R1", "next_measures", "M1 M2"); + + config.setString("KYC-MEASURE-M1", "check_name", "C1"); + config.setString("KYC-MEASURE-M1", "context", "{}"); + config.setString("KYC-MEASURE-M1", "program", "P1"); + + config.setString("KYC-MEASURE-M2", "check_name", "C2"); + config.setString("KYC-MEASURE-M2", "context", "{}"); + config.setString("KYC-MEASURE-M2", "program", "P2"); + + config.setString("KYC-MEASURE-M3", "check_name", "C3"); + config.setString("KYC-MEASURE-M3", "context", "{}"); + config.setString("KYC-MEASURE-M3", "program", "P2"); + + config.setString( + "AML-PROGRAM-P1", + "command", + "taler-exchange-helper-measure-test-form", + ); + config.setString("AML-PROGRAM-P1", "enabled", "true"); + config.setString( + "AML-PROGRAM-P1", + "description", + "test for full_name and birthdate", + ); + config.setString("AML-PROGRAM-P1", "description_i18n", "{}"); + config.setString("AML-PROGRAM-P1", "fallback", "M1"); + + config.setString("AML-PROGRAM-P2", "command", "/bin/true"); + config.setString("AML-PROGRAM-P2", "enabled", "true"); + config.setString("AML-PROGRAM-P2", "description", "does nothing"); + config.setString("AML-PROGRAM-P2", "description_i18n", "{}"); + config.setString("AML-PROGRAM-P2", "fallback", "M1"); + + config.setString("KYC-CHECK-C1", "type", "FORM"); + config.setString("KYC-CHECK-C1", "form_name", "myform"); + config.setString("KYC-CHECK-C1", "description", "my check!"); + config.setString("KYC-CHECK-C1", "description_i18n", "{}"); + config.setString("KYC-CHECK-C1", "outputs", "full_name birthdate"); + config.setString("KYC-CHECK-C1", "fallback", "M1"); + + config.setString("KYC-CHECK-C2", "type", "INFO"); + config.setString("KYC-CHECK-C2", "description", "my check info!"); + config.setString("KYC-CHECK-C2", "description_i18n", "{}"); + config.setString("KYC-CHECK-C2", "fallback", "M2"); + + config.setString("KYC-CHECK-C3", "type", "INFO"); + config.setString("KYC-CHECK-C3", "description", "this is info c3"); + config.setString("KYC-CHECK-C3", "description_i18n", "{}"); + config.setString("KYC-CHECK-C3", "fallback", "M2"); + }); + + await exchange.start(); + + const cryptoApi = createSyncCryptoApi(); + const amlKeypair = await cryptoApi.createEddsaKeypair({}); + + await exchange.enableAmlAccount(amlKeypair.pub, "Alice"); + + const walletService = new WalletService(t, { + name: "wallet", + useInMemoryDb: true, + }); + await walletService.start(); + await walletService.pingUntilAvailable(); + + const walletClient = new WalletClient({ + name: "wallet", + unixPath: walletService.socketPath, + onNotification(n) { + console.log("got notification", n); + }, + }); + await walletClient.connect(); + await walletClient.client.call(WalletApiOperation.InitWallet, { + config: { + testing: { + skipDefaults: true, + }, + }, + }); + + console.log("setup done!"); + + return { + commonDb: db, + exchange, + amlKeypair, + walletClient, + walletService, + bankClient, + exchangeBankAccount: { + accountName: "", + accountPassword: "", + accountPaytoUri: "", + wireGatewayApiBaseUrl: "", + }, + }; +} + +/** + * Test setting a `new_measure` as the AML officer. + */ +export async function runKycNewMeasureTest(t: GlobalTestState) { + // Set up test environment + + const { walletClient, bankClient, exchange, amlKeypair } = + await createKycTestkudosEnvironment(t); + + // Withdraw digital cash into the wallet. + let kycPaytoHash: string | undefined; + let accessToken: string | undefined; + let firstTransaction: string | undefined; + + { + const wres = await withdrawViaBankV3(t, { + amount: "TESTKUDOS:20", + bankClient, + exchange, + walletClient, + }); + + await walletClient.call(WalletApiOperation.TestingWaitTransactionState, { + transactionId: wres.transactionId as TransactionIdStr, + txState: { + major: TransactionMajorState.Pending, + minor: TransactionMinorState.KycRequired, + }, + }); + + const txDetails = await walletClient.call( + WalletApiOperation.GetTransactionById, + { + transactionId: wres.transactionId, + }, + ); + + console.log(j2s(txDetails)); + + accessToken = txDetails.kycAccessToken; + kycPaytoHash = txDetails.kycPaytoHash; + firstTransaction = wres.transactionId; + } + + t.assertTrue(!!accessToken); + const infoResp = await harnessHttpLib.fetch( + new URL(`kyc-info/${accessToken}`, exchange.baseUrl).href, + ); + + const clientInfo = await readResponseJsonOrThrow( + infoResp, + codecForKycProcessClientInformation(), + ); + + console.log(j2s(clientInfo)); + + const kycId = clientInfo.requirements.find((x) => x.id != null)?.id; + t.assertTrue(!!kycId); + + const uploadResp = await harnessHttpLib.fetch( + new URL(`kyc-upload/${kycId}`, exchange.baseUrl).href, + { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: "full_name=Alice+Abc&birthdate=2000-01-01", + }, + ); + + console.log("resp status", uploadResp.status); + + t.assertDeepEqual(uploadResp.status, 204); + + const sig = signAmlQuery(decodeCrock(amlKeypair.priv)); + { + const decisionsResp = await harnessHttpLib.fetch( + new URL(`aml/${amlKeypair.pub}/decisions`, exchange.baseUrl).href, + { + headers: { + "Taler-AML-Officer-Signature": encodeCrock(sig), + }, + }, + ); + + const decisions = await readResponseJsonOrThrow(decisionsResp, codecForAny()); + console.log(j2s(decisions)); + + t.assertDeepEqual(decisionsResp.status, 200); + } + // KYC should pass now + await walletClient.call(WalletApiOperation.TestingWaitTransactionState, { + transactionId: firstTransaction as TransactionIdStr, + txState: { + major: TransactionMajorState.Done, + }, + }); + + // Now, AML officer takes action and freezes the account, requiring a new_measure + t.assertTrue(!!kycPaytoHash); + + await postAmlDecision(t, { + amlPriv: amlKeypair.priv, + amlPub: amlKeypair.pub, + exchangeBaseUrl: exchange.baseUrl, + paytoHash: kycPaytoHash, + newMeasure: "m3", + newRules: { + expiration_time: TalerProtocolTimestamp.never(), + custom_measures: {}, + rules: [ + // No rules! + ], + }, + }); + + + { + const decisionsResp = await harnessHttpLib.fetch( + new URL(`aml/${amlKeypair.pub}/decisions`, exchange.baseUrl).href, + { + headers: { + "Taler-AML-Officer-Signature": encodeCrock(sig), + }, + }, + ); + + const decisions = await readResponseJsonOrThrow(decisionsResp, codecForAny()); + console.log(j2s(decisions)); + + t.assertDeepEqual(decisionsResp.status, 200); + } + + { + const wres = await withdrawViaBankV3(t, { + amount: "TESTKUDOS:21", + bankClient, + exchange, + walletClient, + }); + + await walletClient.call(WalletApiOperation.TestingWaitTransactionState, { + transactionId: wres.transactionId as TransactionIdStr, + txState: { + major: TransactionMajorState.Pending, + minor: TransactionMinorState.KycRequired, + }, + }); + + const txDetails = await walletClient.call( + WalletApiOperation.GetTransactionById, + { + transactionId: wres.transactionId, + }, + ); + console.log(j2s(txDetails)); + + const accessToken = txDetails.kycAccessToken; + t.assertTrue(!!accessToken); + + const infoResp = await harnessHttpLib.fetch( + new URL(`kyc-info/${txDetails.kycAccessToken}`, exchange.baseUrl).href, + ); + + const clientInfo = await readResponseJsonOrThrow( + infoResp, + codecForKycProcessClientInformation(), + ); + + console.log("second withdrawal, clientInfo:"); + console.log(j2s(clientInfo)); + } +} + +runKycNewMeasureTest.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/test-kyc-peer-pull.ts b/packages/taler-harness/src/integrationtests/test-kyc-peer-pull.ts new file mode 100644 index 000000000..0919f6550 --- /dev/null +++ b/packages/taler-harness/src/integrationtests/test-kyc-peer-pull.ts @@ -0,0 +1,356 @@ +/* + 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 { + AbsoluteTime, + AmountString, + Duration, + j2s, + TalerCorebankApiClient, + TransactionIdStr, + TransactionMajorState, + TransactionMinorState, + TransactionType, +} from "@gnu-taler/taler-util"; +import { + createSyncCryptoApi, + EddsaKeyPairStrings, + WalletApiOperation, +} from "@gnu-taler/taler-wallet-core"; +import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js"; +import { + BankService, + DbInfo, + ExchangeService, + getTestHarnessPaytoForLabel, + GlobalTestState, + HarnessExchangeBankAccount, + setupDb, + WalletClient, + WalletService, +} from "../harness/harness.js"; +import { + createWalletDaemonWithClient, + EnvOptions, + postAmlDecisionNoRules, + withdrawViaBankV3, +} from "../harness/helpers.js"; + +interface KycTestEnv { + commonDb: DbInfo; + bankClient: TalerCorebankApiClient; + exchange: ExchangeService; + exchangeBankAccount: HarnessExchangeBankAccount; + walletClient: WalletClient; + walletService: WalletService; + amlKeypair: EddsaKeyPairStrings; +} + +async function createKycTestkudosEnvironment( + t: GlobalTestState, + coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")), + opts: EnvOptions = {}, +): Promise<KycTestEnv> { + const db = await setupDb(t); + + const bank = await BankService.create(t, { + allowRegistrations: true, + currency: "TESTKUDOS", + database: db.connStr, + httpPort: 8082, + }); + + const exchange = ExchangeService.create(t, { + name: "testexchange-1", + currency: "TESTKUDOS", + httpPort: 8081, + database: db.connStr, + }); + + let receiverName = "Exchange"; + let exchangeBankUsername = "exchange"; + let exchangeBankPassword = "mypw"; + let exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername); + + await exchange.addBankAccount("1", { + accountName: exchangeBankUsername, + accountPassword: exchangeBankPassword, + wireGatewayApiBaseUrl: new URL( + "accounts/exchange/taler-wire-gateway/", + bank.baseUrl, + ).href, + accountPaytoUri: exchangePaytoUri, + }); + + bank.setSuggestedExchange(exchange, exchangePaytoUri); + + await bank.start(); + + await bank.pingUntilAvailable(); + + const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl, { + auth: { + username: "admin", + password: "adminpw", + }, + }); + + await bankClient.registerAccountExtended({ + name: receiverName, + password: exchangeBankPassword, + username: exchangeBankUsername, + is_taler_exchange: true, + payto_uri: exchangePaytoUri, + }); + + const ageMaskSpec = opts.ageMaskSpec; + + if (ageMaskSpec) { + exchange.enableAgeRestrictions(ageMaskSpec); + // Enable age restriction for all coins. + exchange.addCoinConfigList( + coinConfig.map((x) => ({ + ...x, + name: `${x.name}-age`, + ageRestricted: true, + })), + ); + // For mixed age restrictions, we also offer coins without age restrictions + if (opts.mixedAgeRestriction) { + exchange.addCoinConfigList( + coinConfig.map((x) => ({ ...x, ageRestricted: false })), + ); + } + } else { + exchange.addCoinConfigList(coinConfig); + } + + await exchange.modifyConfig(async (config) => { + config.setString("exchange", "enable_kyc", "yes"); + + config.setString("KYC-RULE-R1", "operation_type", "merge"); + config.setString("KYC-RULE-R1", "enabled", "yes"); + config.setString("KYC-RULE-R1", "exposed", "yes"); + config.setString("KYC-RULE-R1", "is_and_combinator", "yes"); + config.setString("KYC-RULE-R1", "threshold", "TESTKUDOS:5"); + config.setString("KYC-RULE-R1", "timeframe", "1d"); + config.setString("KYC-RULE-R1", "next_measures", "M1"); + + config.setString("KYC-MEASURE-M1", "check_name", "C1"); + config.setString("KYC-MEASURE-M1", "context", "{}"); + config.setString("KYC-MEASURE-M1", "program", "P1"); + + config.setString("AML-PROGRAM-P1", "command", "/bin/true"); + config.setString("AML-PROGRAM-P1", "enabled", "true"); + config.setString("AML-PROGRAM-P1", "description", "this does nothing"); + config.setString("AML-PROGRAM-P1", "fallback", "M1"); + + config.setString("KYC-CHECK-C1", "type", "INFO"); + config.setString("KYC-CHECK-C1", "description", "my check!"); + config.setString("KYC-CHECK-C1", "fallback", "M1"); + }); + + await exchange.start(); + + const cryptoApi = createSyncCryptoApi(); + const amlKeypair = await cryptoApi.createEddsaKeypair({}); + + await exchange.enableAmlAccount(amlKeypair.pub, "Alice"); + + const walletService = new WalletService(t, { + name: "wallet", + useInMemoryDb: true, + }); + await walletService.start(); + await walletService.pingUntilAvailable(); + + const walletClient = new WalletClient({ + name: "wallet", + unixPath: walletService.socketPath, + onNotification(n) { + console.log("got notification", n); + }, + }); + await walletClient.connect(); + await walletClient.client.call(WalletApiOperation.InitWallet, { + config: { + testing: { + skipDefaults: true, + }, + }, + }); + + console.log("setup done!"); + + return { + commonDb: db, + exchange, + amlKeypair, + walletClient, + walletService, + bankClient, + exchangeBankAccount: { + accountName: "", + accountPassword: "", + accountPaytoUri: "", + wireGatewayApiBaseUrl: "", + }, + }; +} + +export async function runKycPeerPullTest(t: GlobalTestState) { + // Set up test environment + + const { walletClient, bankClient, exchange, amlKeypair } = + await createKycTestkudosEnvironment(t); + + // Origin wallet for the p2p transaction, + // will pay for the invoice. + const w0 = await createWalletDaemonWithClient(t, { + name: "w0", + }); + + // Withdraw digital cash into the wallet. + + const wres1 = await withdrawViaBankV3(t, { + bankClient, + amount: "TESTKUDOS:20", + exchange: exchange, + walletClient: w0.walletClient, + }); + + await wres1.withdrawalFinishedCond; + + const wres2 = await withdrawViaBankV3(t, { + bankClient, + amount: "TESTKUDOS:1", + exchange: exchange, + walletClient: walletClient, + }); + + await wres2.withdrawalFinishedCond; + + const pullRes = await doPeerPullCredit(t, { + walletClient, + amount: "TESTKUDOS:10", + summary: "test123", + }); + + const txDet = await walletClient.call(WalletApiOperation.GetTransactionById, { + transactionId: pullRes.transactionId, + }); + + console.log("tx details", j2s(txDet)); + + const kycPaytoHash = txDet.kycPaytoHash; + + t.assertTrue(!!kycPaytoHash); + + await postAmlDecisionNoRules(t, { + amlPriv: amlKeypair.priv, + amlPub: amlKeypair.pub, + exchangeBaseUrl: exchange.baseUrl, + paytoHash: kycPaytoHash, + }); + + await walletClient.call(WalletApiOperation.TestingWaitTransactionState, { + transactionId: pullRes.transactionId as TransactionIdStr, + txState: { + major: TransactionMajorState.Pending, + minor: TransactionMinorState.Ready, + }, + }); + + const prepRes = await w0.walletClient.call( + WalletApiOperation.PreparePeerPullDebit, + { + talerUri: pullRes.talerUri, + }, + ); + + await w0.walletClient.call(WalletApiOperation.ConfirmPeerPullDebit, { + transactionId: prepRes.transactionId, + }); + + await walletClient.call(WalletApiOperation.TestingWaitTransactionState, { + transactionId: pullRes.transactionId as TransactionIdStr, + txState: { + major: TransactionMajorState.Done, + }, + }); +} + +/** + * Initiate a pull credit transaction, wait until the transaction + * is ready. + */ +async function doPeerPullCredit( + t: GlobalTestState, + args: { + walletClient: WalletClient; + amount: AmountString; + summary?: string; + }, +): Promise<{ + transactionId: string; + talerUri: string; +}> { + const purse_expiration = AbsoluteTime.toProtocolTimestamp( + AbsoluteTime.addDuration( + AbsoluteTime.now(), + Duration.fromSpec({ days: 2 }), + ), + ); + const initRet = await args.walletClient.call( + WalletApiOperation.InitiatePeerPullCredit, + { + partialContractTerms: { + amount: args.amount, + summary: args.summary ?? "Test P2P Payment", + purse_expiration, + }, + }, + ); + + await args.walletClient.call(WalletApiOperation.TestingWaitTransactionState, { + transactionId: initRet.transactionId, + txState: { + major: TransactionMajorState.Pending, + minor: TransactionMinorState.MergeKycRequired, + }, + }); + + const txDet = await args.walletClient.call( + WalletApiOperation.GetTransactionById, + { + transactionId: initRet.transactionId, + }, + ); + + t.assertTrue(txDet.type === TransactionType.PeerPullCredit); + const talerUri = txDet.talerUri; + t.assertTrue(!!talerUri); + + return { + transactionId: initRet.transactionId, + talerUri, + }; +} + +runKycPeerPullTest.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/test-kyc-peer-push.ts b/packages/taler-harness/src/integrationtests/test-kyc-peer-push.ts new file mode 100644 index 000000000..6e1a78a26 --- /dev/null +++ b/packages/taler-harness/src/integrationtests/test-kyc-peer-push.ts @@ -0,0 +1,347 @@ +/* + 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 { + AbsoluteTime, + AmountString, + Duration, + j2s, + TalerCorebankApiClient, + TransactionMajorState, + TransactionMinorState, + TransactionType, +} from "@gnu-taler/taler-util"; +import { + createSyncCryptoApi, + EddsaKeyPairStrings, + WalletApiOperation, +} from "@gnu-taler/taler-wallet-core"; +import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js"; +import { + BankService, + DbInfo, + ExchangeService, + getTestHarnessPaytoForLabel, + GlobalTestState, + HarnessExchangeBankAccount, + setupDb, + WalletClient, + WalletService, +} from "../harness/harness.js"; +import { + createWalletDaemonWithClient, + EnvOptions, + postAmlDecisionNoRules, + withdrawViaBankV3, +} from "../harness/helpers.js"; + +interface KycTestEnv { + commonDb: DbInfo; + bankClient: TalerCorebankApiClient; + exchange: ExchangeService; + exchangeBankAccount: HarnessExchangeBankAccount; + walletClient: WalletClient; + walletService: WalletService; + amlKeypair: EddsaKeyPairStrings; +} + +async function createKycTestkudosEnvironment( + t: GlobalTestState, + coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")), + opts: EnvOptions = {}, +): Promise<KycTestEnv> { + const db = await setupDb(t); + + const bank = await BankService.create(t, { + allowRegistrations: true, + currency: "TESTKUDOS", + database: db.connStr, + httpPort: 8082, + }); + + const exchange = ExchangeService.create(t, { + name: "testexchange-1", + currency: "TESTKUDOS", + httpPort: 8081, + database: db.connStr, + }); + + let receiverName = "Exchange"; + let exchangeBankUsername = "exchange"; + let exchangeBankPassword = "mypw"; + let exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername); + + await exchange.addBankAccount("1", { + accountName: exchangeBankUsername, + accountPassword: exchangeBankPassword, + wireGatewayApiBaseUrl: new URL( + "accounts/exchange/taler-wire-gateway/", + bank.baseUrl, + ).href, + accountPaytoUri: exchangePaytoUri, + }); + + bank.setSuggestedExchange(exchange, exchangePaytoUri); + + await bank.start(); + + await bank.pingUntilAvailable(); + + const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl, { + auth: { + username: "admin", + password: "adminpw", + }, + }); + + await bankClient.registerAccountExtended({ + name: receiverName, + password: exchangeBankPassword, + username: exchangeBankUsername, + is_taler_exchange: true, + payto_uri: exchangePaytoUri, + }); + + const ageMaskSpec = opts.ageMaskSpec; + + if (ageMaskSpec) { + exchange.enableAgeRestrictions(ageMaskSpec); + // Enable age restriction for all coins. + exchange.addCoinConfigList( + coinConfig.map((x) => ({ + ...x, + name: `${x.name}-age`, + ageRestricted: true, + })), + ); + // For mixed age restrictions, we also offer coins without age restrictions + if (opts.mixedAgeRestriction) { + exchange.addCoinConfigList( + coinConfig.map((x) => ({ ...x, ageRestricted: false })), + ); + } + } else { + exchange.addCoinConfigList(coinConfig); + } + + await exchange.modifyConfig(async (config) => { + config.setString("exchange", "enable_kyc", "yes"); + + config.setString("KYC-RULE-R1", "operation_type", "merge"); + config.setString("KYC-RULE-R1", "enabled", "yes"); + config.setString("KYC-RULE-R1", "exposed", "yes"); + config.setString("KYC-RULE-R1", "is_and_combinator", "yes"); + config.setString("KYC-RULE-R1", "threshold", "TESTKUDOS:5"); + config.setString("KYC-RULE-R1", "timeframe", "1d"); + config.setString("KYC-RULE-R1", "next_measures", "M1"); + + config.setString("KYC-MEASURE-M1", "check_name", "C1"); + config.setString("KYC-MEASURE-M1", "context", "{}"); + config.setString("KYC-MEASURE-M1", "program", "P1"); + + config.setString("AML-PROGRAM-P1", "command", "/bin/true"); + config.setString("AML-PROGRAM-P1", "enabled", "true"); + config.setString("AML-PROGRAM-P1", "description", "this does nothing"); + config.setString("AML-PROGRAM-P1", "fallback", "M1"); + + config.setString("KYC-CHECK-C1", "type", "INFO"); + config.setString("KYC-CHECK-C1", "description", "my check!"); + config.setString("KYC-CHECK-C1", "fallback", "M1"); + }); + + await exchange.start(); + + const cryptoApi = createSyncCryptoApi(); + const amlKeypair = await cryptoApi.createEddsaKeypair({}); + + await exchange.enableAmlAccount(amlKeypair.pub, "Alice"); + + const walletService = new WalletService(t, { + name: "wallet", + useInMemoryDb: true, + }); + await walletService.start(); + await walletService.pingUntilAvailable(); + + const walletClient = new WalletClient({ + name: "wallet", + unixPath: walletService.socketPath, + onNotification(n) { + console.log("got notification", n); + }, + }); + await walletClient.connect(); + await walletClient.client.call(WalletApiOperation.InitWallet, { + config: { + testing: { + skipDefaults: true, + }, + }, + }); + + console.log("setup done!"); + + return { + commonDb: db, + exchange, + amlKeypair, + walletClient, + walletService, + bankClient, + exchangeBankAccount: { + accountName: "", + accountPassword: "", + accountPaytoUri: "", + wireGatewayApiBaseUrl: "", + }, + }; +} + +export async function runKycPeerPushTest(t: GlobalTestState) { + // Set up test environment + + const { walletClient, bankClient, exchange, amlKeypair } = + await createKycTestkudosEnvironment(t); + + // Origin wallet for the p2p transaction. + const w0 = await createWalletDaemonWithClient(t, { + name: "w0", + }); + + // Withdraw digital cash into the wallet. + + const wres = await withdrawViaBankV3(t, { + bankClient, + amount: "TESTKUDOS:20", + exchange: exchange, + walletClient: w0.walletClient, + }); + + await wres.withdrawalFinishedCond; + + const pushDebitRes = await doPeerPushDebit(t, { + walletClient: w0.walletClient, + amount: "TESTKUDOS:10", + summary: "Test1", + }); + + const prepRes = await walletClient.call( + WalletApiOperation.PreparePeerPushCredit, + { + talerUri: pushDebitRes.talerUri, + }, + ); + + console.log("prepRes", j2s(prepRes)); + + await walletClient.call(WalletApiOperation.ConfirmPeerPushCredit, { + transactionId: prepRes.transactionId, + }); + + await walletClient.call(WalletApiOperation.TestingWaitTransactionState, { + transactionId: prepRes.transactionId, + txState: { + major: TransactionMajorState.Pending, + minor: TransactionMinorState.MergeKycRequired, + }, + }); + + const txDet = await walletClient.call(WalletApiOperation.GetTransactionById, { + transactionId: prepRes.transactionId, + }); + + console.log("tx details", j2s(txDet)); + + const kycPaytoHash = txDet.kycPaytoHash; + + t.assertTrue(!!kycPaytoHash); + + await postAmlDecisionNoRules(t, { + amlPriv: amlKeypair.priv, + amlPub: amlKeypair.pub, + exchangeBaseUrl: exchange.baseUrl, + paytoHash: kycPaytoHash, + }); + + await walletClient.call(WalletApiOperation.TestingWaitTransactionState, { + transactionId: prepRes.transactionId, + txState: { + major: TransactionMajorState.Done, + }, + }); +} + +/** + * Initiate a push debit transaction, wait until the transaction + * is ready. + */ +async function doPeerPushDebit( + t: GlobalTestState, + args: { + walletClient: WalletClient; + amount: AmountString; + summary?: string; + }, +): Promise<{ + transactionId: string; + talerUri: string; +}> { + const purse_expiration = AbsoluteTime.toProtocolTimestamp( + AbsoluteTime.addDuration( + AbsoluteTime.now(), + Duration.fromSpec({ days: 2 }), + ), + ); + const initRet = await args.walletClient.call( + WalletApiOperation.InitiatePeerPushDebit, + { + partialContractTerms: { + amount: args.amount, + summary: args.summary ?? "Test P2P Payment", + purse_expiration, + }, + }, + ); + + await args.walletClient.call(WalletApiOperation.TestingWaitTransactionState, { + transactionId: initRet.transactionId, + txState: { + major: TransactionMajorState.Pending, + minor: TransactionMinorState.Ready, + }, + }); + + const txDet = await args.walletClient.call( + WalletApiOperation.GetTransactionById, + { + transactionId: initRet.transactionId, + }, + ); + + t.assertTrue(txDet.type === TransactionType.PeerPushDebit); + const talerUri = txDet.talerUri; + t.assertTrue(!!talerUri); + + return { + transactionId: initRet.transactionId, + talerUri, + }; +} + +runKycPeerPushTest.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/test-kyc-threshold-withdrawal.ts b/packages/taler-harness/src/integrationtests/test-kyc-threshold-withdrawal.ts new file mode 100644 index 000000000..6aa65992a --- /dev/null +++ b/packages/taler-harness/src/integrationtests/test-kyc-threshold-withdrawal.ts @@ -0,0 +1,322 @@ +/* + 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 { + NotificationType, + TalerCorebankApiClient, + TransactionMajorState, + TransactionMinorState, + TransactionType, +} from "@gnu-taler/taler-util"; +import { + createSyncCryptoApi, + EddsaKeyPairStrings, + WalletApiOperation, +} from "@gnu-taler/taler-wallet-core"; +import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js"; +import { + BankService, + DbInfo, + ExchangeService, + getTestHarnessPaytoForLabel, + GlobalTestState, + HarnessExchangeBankAccount, + setupDb, + WalletClient, + WalletService, +} from "../harness/harness.js"; +import { EnvOptions, postAmlDecisionNoRules } from "../harness/helpers.js"; + +interface KycTestEnv { + commonDb: DbInfo; + bankClient: TalerCorebankApiClient; + exchange: ExchangeService; + exchangeBankAccount: HarnessExchangeBankAccount; + walletClient: WalletClient; + walletService: WalletService; + amlKeypair: EddsaKeyPairStrings; +} + +async function createKycTestkudosEnvironment( + t: GlobalTestState, + coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")), + opts: EnvOptions = {}, +): Promise<KycTestEnv> { + const db = await setupDb(t); + + const bank = await BankService.create(t, { + allowRegistrations: true, + currency: "TESTKUDOS", + database: db.connStr, + httpPort: 8082, + }); + + const exchange = ExchangeService.create(t, { + name: "testexchange-1", + currency: "TESTKUDOS", + httpPort: 8081, + database: db.connStr, + }); + + let receiverName = "Exchange"; + let exchangeBankUsername = "exchange"; + let exchangeBankPassword = "mypw"; + let exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername); + + await exchange.addBankAccount("1", { + accountName: exchangeBankUsername, + accountPassword: exchangeBankPassword, + wireGatewayApiBaseUrl: new URL( + "accounts/exchange/taler-wire-gateway/", + bank.baseUrl, + ).href, + accountPaytoUri: exchangePaytoUri, + }); + + bank.setSuggestedExchange(exchange, exchangePaytoUri); + + await bank.start(); + + await bank.pingUntilAvailable(); + + const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl, { + auth: { + username: "admin", + password: "adminpw", + }, + }); + + await bankClient.registerAccountExtended({ + name: receiverName, + password: exchangeBankPassword, + username: exchangeBankUsername, + is_taler_exchange: true, + payto_uri: exchangePaytoUri, + }); + + const ageMaskSpec = opts.ageMaskSpec; + + if (ageMaskSpec) { + exchange.enableAgeRestrictions(ageMaskSpec); + // Enable age restriction for all coins. + exchange.addCoinConfigList( + coinConfig.map((x) => ({ + ...x, + name: `${x.name}-age`, + ageRestricted: true, + })), + ); + // For mixed age restrictions, we also offer coins without age restrictions + if (opts.mixedAgeRestriction) { + exchange.addCoinConfigList( + coinConfig.map((x) => ({ ...x, ageRestricted: false })), + ); + } + } else { + exchange.addCoinConfigList(coinConfig); + } + + await exchange.modifyConfig(async (config) => { + config.setString("exchange", "enable_kyc", "yes"); + + config.setString("KYC-RULE-R1", "operation_type", "withdraw"); + config.setString("KYC-RULE-R1", "enabled", "yes"); + config.setString("KYC-RULE-R1", "exposed", "yes"); + config.setString("KYC-RULE-R1", "is_and_combinator", "yes"); + config.setString("KYC-RULE-R1", "threshold", "TESTKUDOS:5"); + config.setString("KYC-RULE-R1", "timeframe", "1d"); + config.setString("KYC-RULE-R1", "next_measures", "M1"); + + config.setString("KYC-RULE-R1", "operation_type", "withdraw"); + config.setString("KYC-RULE-R1", "enabled", "yes"); + config.setString("KYC-RULE-R1", "exposed", "yes"); + config.setString("KYC-RULE-R1", "is_and_combinator", "yes"); + config.setString("KYC-RULE-R1", "threshold", "TESTKUDOS:300"); + config.setString("KYC-RULE-R1", "timeframe", "1d"); + config.setString("KYC-RULE-R1", "next_measures", "verboten"); + + config.setString("KYC-MEASURE-M1", "check_name", "C1"); + config.setString("KYC-MEASURE-M1", "context", "{}"); + config.setString("KYC-MEASURE-M1", "program", "P1"); + + config.setString("AML-PROGRAM-P1", "command", "/bin/true"); + config.setString("AML-PROGRAM-P1", "enabled", "true"); + config.setString("AML-PROGRAM-P1", "description", "this does nothing"); + config.setString("AML-PROGRAM-P1", "fallback", "M1"); + + config.setString("KYC-CHECK-C1", "type", "INFO"); + config.setString("KYC-CHECK-C1", "description", "my check!"); + config.setString("KYC-CHECK-C1", "fallback", "M1"); + }); + + await exchange.start(); + + const cryptoApi = createSyncCryptoApi(); + const amlKeypair = await cryptoApi.createEddsaKeypair({}); + + await exchange.enableAmlAccount(amlKeypair.pub, "Alice"); + + const walletService = new WalletService(t, { + name: "wallet", + useInMemoryDb: true, + }); + await walletService.start(); + await walletService.pingUntilAvailable(); + + const walletClient = new WalletClient({ + name: "wallet", + unixPath: walletService.socketPath, + onNotification(n) { + console.log("got notification", n); + }, + }); + await walletClient.connect(); + await walletClient.client.call(WalletApiOperation.InitWallet, { + config: { + testing: { + skipDefaults: true, + }, + }, + }); + + console.log("setup done!"); + + return { + commonDb: db, + exchange, + amlKeypair, + walletClient, + walletService, + bankClient, + exchangeBankAccount: { + accountName: "", + accountPassword: "", + accountPaytoUri: "", + wireGatewayApiBaseUrl: "", + }, + }; +} + +export async function runKycThresholdWithdrawalTest(t: GlobalTestState) { + // Set up test environment + + const { walletClient, bankClient, exchange, amlKeypair } = + await createKycTestkudosEnvironment(t); + + // Withdraw digital cash into the wallet. + + const amount = "TESTKUDOS:20"; + const user = await bankClient.createRandomBankUser(); + bankClient.setAuth({ + username: user.username, + password: user.password, + }); + + const wop = await bankClient.createWithdrawalOperation(user.username, amount); + + // Hand it to the wallet + + const withdrawalUrlInfo = await walletClient.client.call( + WalletApiOperation.GetWithdrawalDetailsForUri, + { + talerWithdrawUri: wop.taler_withdraw_uri, + }, + ); + + const withdrawalAmountInfo = await walletClient.call( + WalletApiOperation.GetWithdrawalDetailsForAmount, + { + amount: withdrawalUrlInfo.amount!, + exchangeBaseUrl: withdrawalUrlInfo.possibleExchanges[0].exchangeBaseUrl, + }, + ); + + // t.assertTrue(!!withdrawalAmountInfo.kycHardLimit); + // t.assertAmountEquals(withdrawalAmountInfo.kycHardLimit, "TESTKUDOS:300"); + + // Withdraw + + const acceptResp = await walletClient.client.call( + WalletApiOperation.AcceptBankIntegratedWithdrawal, + { + exchangeBaseUrl: exchange.baseUrl, + talerWithdrawUri: wop.taler_withdraw_uri, + }, + ); + + const withdrawalTxId = acceptResp.transactionId; + + // Confirm it + + await bankClient.confirmWithdrawalOperation(user.username, { + withdrawalOperationId: wop.withdrawal_id, + }); + + + t.logStep("waiting for pending(kyc-required)"); + + const kycNotificationCond = walletClient.waitForNotificationCond((x) => { + if ( + x.type === NotificationType.TransactionStateTransition && + x.transactionId === withdrawalTxId && + x.newTxState.major === TransactionMajorState.Pending && + x.newTxState.minor === TransactionMinorState.KycRequired + ) { + return x; + } + return false; + }); + + await kycNotificationCond; + + const txDet = await walletClient.call(WalletApiOperation.GetTransactionById, { + transactionId: withdrawalTxId, + }); + + t.assertDeepEqual(txDet.type, TransactionType.Withdrawal); + + const kycPaytoHash = txDet.kycPaytoHash; + t.assertTrue(!!kycPaytoHash); + + t.logStep("posting aml decision"); + + await postAmlDecisionNoRules(t, { + amlPriv: amlKeypair.priv, + amlPub: amlKeypair.pub, + exchangeBaseUrl: exchange.baseUrl, + paytoHash: kycPaytoHash, + }); + + t.logStep("waiting for withdrawal to be done"); + + const doneNotificationCond = walletClient.waitForNotificationCond((x) => { + if ( + x.type === NotificationType.TransactionStateTransition && + x.transactionId === withdrawalTxId && + x.newTxState.major === TransactionMajorState.Done + ) { + return x; + } + return false; + }); + + await doneNotificationCond; +} + +runKycThresholdWithdrawalTest.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/test-kyc.ts b/packages/taler-harness/src/integrationtests/test-kyc.ts index 213dd9df4..3a68fcbce 100644 --- a/packages/taler-harness/src/integrationtests/test-kyc.ts +++ b/packages/taler-harness/src/integrationtests/test-kyc.ts @@ -25,27 +25,29 @@ import { TransactionMajorState, TransactionMinorState, TransactionType, + codecForKycProcessClientInformation, j2s, } from "@gnu-taler/taler-util"; -import { createPlatformHttpLib } from "@gnu-taler/taler-util/http"; +import { readResponseJsonOrThrow } from "@gnu-taler/taler-util/http"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import * as http from "node:http"; import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js"; import { - BankService, + BankService, ExchangeService, GlobalTestState, MerchantService, WalletClient, WalletService, - generateRandomPayto, + getTestHarnessPaytoForLabel, + harnessHttpLib, setupDb, } from "../harness/harness.js"; import { EnvOptions, SimpleTestEnvironmentNg3 } from "../harness/helpers.js"; const logger = new Logger("test-kyc.ts"); -export async function createKycTestkudosEnvironment( +async function createKycTestkudosEnvironment( t: GlobalTestState, coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")), opts: EnvOptions = {}, @@ -76,12 +78,15 @@ export async function createKycTestkudosEnvironment( let receiverName = "Exchange"; let exchangeBankUsername = "exchange"; let exchangeBankPassword = "mypw"; - let exchangePaytoUri = generateRandomPayto(exchangeBankUsername); + let exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername); await exchange.addBankAccount("1", { accountName: exchangeBankUsername, accountPassword: exchangeBankPassword, - wireGatewayApiBaseUrl: new URL("accounts/exchange/taler-wire-gateway/", bank.baseUrl).href, + wireGatewayApiBaseUrl: new URL( + "accounts/exchange/taler-wire-gateway/", + bank.baseUrl, + ).href, accountPaytoUri: exchangePaytoUri, }); @@ -129,11 +134,48 @@ export async function createKycTestkudosEnvironment( } await exchange.modifyConfig(async (config) => { - const myprov = "kyc-provider-myprov"; - config.setString(myprov, "cost", "0"); + config.setString("exchange", "enable_kyc", "yes"); + + config.setString("KYC-RULE-R1", "operation_type", "withdraw"); + config.setString("KYC-RULE-R1", "enabled", "yes"); + config.setString("KYC-RULE-R1", "exposed", "yes"); + config.setString("KYC-RULE-R1", "is_and_combinator", "yes"); + config.setString("KYC-RULE-R1", "threshold", "TESTKUDOS:5"); + config.setString("KYC-RULE-R1", "timeframe", "1d"); + config.setString("KYC-RULE-R1", "next_measures", "M1"); + + config.setString("KYC-MEASURE-M1", "check_name", "C1"); + config.setString("KYC-MEASURE-M1", "context", "{}"); + config.setString("KYC-MEASURE-M1", "program", "P1"); + + config.setString("KYC-CHECK-C1", "type", "LINK"); + config.setString("KYC-CHECK-C1", "provider_id", "MYPROV"); + config.setString("KYC-CHECK-C1", "description", "my check!"); + config.setString("KYC-CHECK-C1", "description_i18n", "{}"); + config.setString("KYC-CHECK-C1", "outputs", "full_name birthdate"); + config.setString("KYC-CHECK-C1", "fallback", "M1"); + + config.setString( + "AML-PROGRAM-P1", + "command", + "taler-exchange-helper-measure-test-form", + ); + config.setString("AML-PROGRAM-P1", "enabled", "true"); + config.setString( + "AML-PROGRAM-P1", + "description", + "test for full_name and birthdate", + ); + config.setString("AML-PROGRAM-P1", "description_i18n", "{}"); + config.setString("AML-PROGRAM-P1", "fallback", "M1"); + + const myprov = "KYC-PROVIDER-MYPROV"; config.setString(myprov, "logic", "oauth2"); - config.setString(myprov, "provided_checks", "dummy1"); - config.setString(myprov, "user_type", "individual"); + config.setString( + myprov, + "converter", + "taler-exchange-kyc-oauth2-test-converter.sh", + ); config.setString(myprov, "kyc_oauth2_validity", "forever"); config.setString( myprov, @@ -164,17 +206,6 @@ export async function createKycTestkudosEnvironment( "operation_type", "withdraw", ); - config.setString( - "kyc-legitimization-withdraw1", - "required_checks", - "dummy1", - ); - config.setString("kyc-legitimization-withdraw1", "timeframe", "1d"); - config.setString( - "kyc-legitimization-withdraw1", - "threshold", - "TESTKUDOS:5", - ); }); await exchange.start(); @@ -188,7 +219,7 @@ export async function createKycTestkudosEnvironment( await merchant.addInstanceWithWireAccount({ id: "default", name: "Default Instance", - paytoUris: [generateRandomPayto("merchant-default")], + paytoUris: [getTestHarnessPaytoForLabel("merchant-default")], defaultWireTransferDelay: Duration.toTalerProtocolDuration( Duration.fromSpec({ minutes: 1 }), ), @@ -197,7 +228,7 @@ export async function createKycTestkudosEnvironment( await merchant.addInstanceWithWireAccount({ id: "minst1", name: "minst1", - paytoUris: [generateRandomPayto("minst1")], + paytoUris: [getTestHarnessPaytoForLabel("minst1")], defaultWireTransferDelay: Duration.toTalerProtocolDuration( Duration.fromSpec({ minutes: 1 }), ), @@ -234,12 +265,13 @@ export async function createKycTestkudosEnvironment( merchant, walletClient, walletService, + bank, bankClient, exchangeBankAccount: { - accountName: '', - accountPassword: '', - accountPaytoUri: '', - wireGatewayApiBaseUrl: '', + accountName: "", + accountPassword: "", + accountPaytoUri: "", + wireGatewayApiBaseUrl: "", }, }; } @@ -314,7 +346,10 @@ async function runTestfakeKycService(): Promise<TestfakeKycService> { JSON.stringify({ status: "success", data: { - id: "foobar", + id: "Foobar", + first_name: "Alice", + last_name: "Abc", + birthdate: "2000-01-01", }, }), ); @@ -410,26 +445,48 @@ export async function runKycTest(t: GlobalTestState) { ); t.assertDeepEqual(txState.type, TransactionType.Withdrawal); + const paytoHash = txState.kycPaytoHash; - const kycUrl = txState.kycUrl; - - t.assertTrue(!!kycUrl); - - logger.info(`kyc URL is ${kycUrl}`); + t.assertTrue(!!txState.kycUrl); + t.assertTrue(!!paytoHash); // We now simulate the user interacting with the KYC service, // which would usually done in the browser. - const httpLib = createPlatformHttpLib({ - enableThrottling: false, - }); - const kycServerResp = await httpLib.fetch(kycUrl); - const kycLoginResp = await kycServerResp.json(); - logger.info(`kyc server resp: ${j2s(kycLoginResp)}`); - const kycProofUrl = kycLoginResp.redirect_uri; + const accessToken = txState.kycAccessToken; + t.assertTrue(!!accessToken); + + const infoResp = await harnessHttpLib.fetch( + new URL(`kyc-info/${txState.kycAccessToken}`, exchange.baseUrl).href, + ); + + const clientInfo = await readResponseJsonOrThrow( + infoResp, + codecForKycProcessClientInformation(), + ); + + console.log(j2s(clientInfo)); + + const kycId = clientInfo.requirements.find((x) => x.id != null)?.id; + t.assertTrue(!!kycId); + + const startResp = await harnessHttpLib.fetch( + new URL(`kyc-start/${kycId}`, exchange.baseUrl).href, + { + method: "POST", + body: {}, + }, + ); + + logger.info(`kyc-start resp status: ${startResp.status}`); + logger.info(j2s(startResp.json())); + // We need to "visit" the KYC proof URL at least once to trigger the exchange // asking for the KYC status. - const proofHttpResp = await httpLib.fetch(kycProofUrl); + const proofUrl = new URL(`kyc-proof/MYPROV`, exchange.baseUrl); + proofUrl.searchParams.set("state", paytoHash); + proofUrl.searchParams.set("code", "code_is_ok"); + const proofHttpResp = await harnessHttpLib.fetch(proofUrl.href); logger.info(`proof resp status ${proofHttpResp.status}`); logger.info(`resp headers ${j2s(proofHttpResp.headers.toJSON())}`); if ( diff --git a/packages/taler-harness/src/integrationtests/test-libeufin-bank.ts b/packages/taler-harness/src/integrationtests/test-libeufin-bank.ts index 01b20ddbf..1b67332d8 100644 --- a/packages/taler-harness/src/integrationtests/test-libeufin-bank.ts +++ b/packages/taler-harness/src/integrationtests/test-libeufin-bank.ts @@ -35,7 +35,7 @@ import { GlobalTestState, LibeufinBankService, MerchantService, - generateRandomPayto, + getTestHarnessPaytoForLabel, generateRandomTestIban, setupDb, } from "../harness/harness.js"; @@ -74,7 +74,7 @@ export async function runLibeufinBankTest(t: GlobalTestState) { const exchangeBankUsername = "exchange"; const exchangeBankPw = "mypw"; - const exchangePayto = generateRandomPayto(exchangeBankUsername); + const exchangePayto = getTestHarnessPaytoForLabel(exchangeBankUsername); const wireGatewayApiBaseUrl = new URL( "accounts/exchange/taler-wire-gateway/", bank.baseUrl, @@ -108,13 +108,13 @@ export async function runLibeufinBankTest(t: GlobalTestState) { await merchant.addInstanceWithWireAccount({ id: "default", name: "Default Instance", - paytoUris: [generateRandomPayto("merchant-default")], + paytoUris: [getTestHarnessPaytoForLabel("merchant-default")], }); await merchant.addInstanceWithWireAccount({ id: "minst1", name: "minst1", - paytoUris: [generateRandomPayto("minst1")], + paytoUris: [getTestHarnessPaytoForLabel("minst1")], }); const { walletClient } = await createWalletDaemonWithClient(t, { diff --git a/packages/taler-harness/src/integrationtests/test-merchant-categories.ts b/packages/taler-harness/src/integrationtests/test-merchant-categories.ts index a6ddc31e2..21ccfef3a 100644 --- a/packages/taler-harness/src/integrationtests/test-merchant-categories.ts +++ b/packages/taler-harness/src/integrationtests/test-merchant-categories.ts @@ -17,12 +17,12 @@ /** * Imports. */ -import { URL } from "@gnu-taler/taler-util"; +import { URL, j2s } from "@gnu-taler/taler-util"; import { ExchangeService, GlobalTestState, MerchantService, - generateRandomPayto, + getTestHarnessPaytoForLabel, harnessHttpLib, setupDb, } from "../harness/harness.js"; @@ -78,25 +78,101 @@ export async function runMerchantCategoriesTest(t: GlobalTestState) { await merchant.addInstanceWithWireAccount({ id: "default", name: "Default Instance", - paytoUris: [generateRandomPayto("merchant-default")], + paytoUris: [getTestHarnessPaytoForLabel("merchant-default")], auth: { method: "external", }, }); - const url = new URL("private/categories", merchant.makeInstanceBaseUrl()); - const res = await harnessHttpLib.fetch(url.href, { - method: "POST", - body: { - name: "Snacks", - name_i18n: {}, - }, - }); + let myNewCategoryId: number; + + { + const url = new URL("private/categories", merchant.makeInstanceBaseUrl()); + const res = await harnessHttpLib.fetch(url.href, { + method: "POST", + body: { + name: "Snacks", + name_i18n: {}, + }, + }); + + console.log(res.requestUrl); + console.log("status", res.status); + const categoryJson = await res.json(); + console.log(categoryJson); + t.assertTrue(res.status >= 200 && res.status < 300); + myNewCategoryId = categoryJson.category_id; + } + + { + const url = new URL("private/products", merchant.makeInstanceBaseUrl()); + const res = await harnessHttpLib.fetch(url.href, { + method: "POST", + body: { + product_id: "foo", + description: "Bla Bla", + unit: "item", + price: "TESTKUDOS:6", + total_stock: -1, + }, + }); + t.assertTrue(res.status >= 200 && res.status < 300); + } - console.log(res.requestUrl); - console.log("status", res.status); - console.log(await res.json()); - t.assertTrue(res.status >= 200 && res.status < 300); + { + const url = new URL("private/products", merchant.makeInstanceBaseUrl()); + const res = await harnessHttpLib.fetch(url.href, { + method: "POST", + body: { + product_id: "bar", + description: "Bla Bla", + unit: "item", + price: "TESTKUDOS:2", + total_stock: -1, + categories: [myNewCategoryId], + }, + }); + t.assertTrue(res.status >= 200 && res.status < 300); + } + + { + const url = new URL("private/products", merchant.makeInstanceBaseUrl()); + const res = await harnessHttpLib.fetch(url.href, { + method: "POST", + body: { + product_id: "baz", + description: "Eggs", + unit: "item", + price: "TESTKUDOS:42", + total_stock: -1, + }, + }); + t.assertTrue(res.status >= 200 && res.status < 300); + } + + { + const posUrl = new URL("private/pos", merchant.makeInstanceBaseUrl()); + const res = await harnessHttpLib.fetch(posUrl.href, { + method: "GET", + }); + const posJson = await res.json(); + console.log(j2s(posJson)); + t.assertTrue(res.status >= 200 && res.status < 300); + + t.assertDeepEqual(posJson.products.length, 3); + + const prodFoo = posJson.products.find((x: any) => x.product_id == "foo"); + console.log(`prod foo`, prodFoo); + t.assertTrue(!!prodFoo); + // Only default category + t.assertDeepEqual(prodFoo.categories, [0]); + + const prodBar = posJson.products.find((x: any) => x.product_id == "bar"); + console.log(`prod bar`, prodBar); + t.assertTrue(!!prodBar); + // This should have the one we assigned to it. + t.assertDeepEqual(prodBar.categories, [myNewCategoryId]); + } } runMerchantCategoriesTest.suites = ["merchant"]; diff --git a/packages/taler-harness/src/integrationtests/test-merchant-exchange-confusion.ts b/packages/taler-harness/src/integrationtests/test-merchant-exchange-confusion.ts index 19f89ae2c..8f47eda1b 100644 --- a/packages/taler-harness/src/integrationtests/test-merchant-exchange-confusion.ts +++ b/packages/taler-harness/src/integrationtests/test-merchant-exchange-confusion.ts @@ -20,9 +20,12 @@ import { codecForMerchantOrderStatusUnpaid, ConfirmPayResultType, + j2s, MerchantApiClient, PreparePayResultType, TalerCorebankApiClient, + TalerErrorCode, + TypedTalerErrorDetail, } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { URL } from "url"; @@ -32,9 +35,9 @@ import { FaultInjectedMerchantService, } from "../harness/faultInjection.js"; import { - BankService, + BankService, ExchangeService, - generateRandomPayto, + getTestHarnessPaytoForLabel, GlobalTestState, harnessHttpLib, MerchantService, @@ -87,12 +90,15 @@ export async function createConfusedMerchantTestkudosEnvironment( let receiverName = "Exchange"; let exchangeBankUsername = "exchange"; let exchangeBankPassword = "mypw"; - let exchangePaytoUri = generateRandomPayto(exchangeBankUsername); + let exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername); await exchange.addBankAccount("1", { accountName: exchangeBankUsername, accountPassword: exchangeBankPassword, - wireGatewayApiBaseUrl: new URL("accounts/exchange/taler-wire-gateway/", bank.baseUrl).href, + wireGatewayApiBaseUrl: new URL( + "accounts/exchange/taler-wire-gateway/", + bank.baseUrl, + ).href, accountPaytoUri: exchangePaytoUri, }); @@ -131,13 +137,13 @@ export async function createConfusedMerchantTestkudosEnvironment( await merchant.addInstanceWithWireAccount({ id: "default", name: "Default Instance", - paytoUris: [generateRandomPayto("merchant-default")], + paytoUris: [getTestHarnessPaytoForLabel("merchant-default")], }); await merchant.addInstanceWithWireAccount({ id: "minst1", name: "minst1", - paytoUris: [generateRandomPayto("minst1")], + paytoUris: [getTestHarnessPaytoForLabel("minst1")], }); console.log("setup done!"); @@ -258,7 +264,25 @@ export async function runMerchantExchangeConfusionTest(t: GlobalTestState) { proposalId: proposalId, }); - t.assertTrue(confirmPayRes.type === ConfirmPayResultType.Done); + t.assertTrue(confirmPayRes.type === ConfirmPayResultType.Pending); + + console.log(j2s(confirmPayRes.lastError)); + + // Merchant should not accept the payment! + // Something is clearly wrong, as the exchange now announces + // its own base URL and something is wrong. + + // FIXME: This error code should probably be refined in the future. + + t.assertDeepEqual( + confirmPayRes.lastError?.code, + TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR, + ); + + const err = + confirmPayRes.lastError as TypedTalerErrorDetail<TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR>; + + t.assertDeepEqual(err.httpStatusCode, 400); } runMerchantExchangeConfusionTest.suites = ["merchant"]; diff --git a/packages/taler-harness/src/integrationtests/test-merchant-instances-delete.ts b/packages/taler-harness/src/integrationtests/test-merchant-instances-delete.ts index c0c9353e4..51beded8d 100644 --- a/packages/taler-harness/src/integrationtests/test-merchant-instances-delete.ts +++ b/packages/taler-harness/src/integrationtests/test-merchant-instances-delete.ts @@ -22,7 +22,7 @@ import { ExchangeService, GlobalTestState, MerchantService, - generateRandomPayto, + getTestHarnessPaytoForLabel, harnessHttpLib, setupDb, } from "../harness/harness.js"; @@ -78,7 +78,7 @@ export async function runMerchantInstancesDeleteTest(t: GlobalTestState) { await merchant.addInstanceWithWireAccount({ id: "default", name: "Default Instance", - paytoUris: [generateRandomPayto("merchant-default")], + paytoUris: [getTestHarnessPaytoForLabel("merchant-default")], auth: { method: "external", }, @@ -88,7 +88,7 @@ export async function runMerchantInstancesDeleteTest(t: GlobalTestState) { await merchant.addInstanceWithWireAccount({ id: "myinst", name: "Second Instance", - paytoUris: [generateRandomPayto("merchant-default")], + paytoUris: [getTestHarnessPaytoForLabel("merchant-default")], auth: { method: "external", }, diff --git a/packages/taler-harness/src/integrationtests/test-merchant-instances.ts b/packages/taler-harness/src/integrationtests/test-merchant-instances.ts index 188451e15..f41b028ae 100644 --- a/packages/taler-harness/src/integrationtests/test-merchant-instances.ts +++ b/packages/taler-harness/src/integrationtests/test-merchant-instances.ts @@ -22,7 +22,7 @@ import { ExchangeService, GlobalTestState, MerchantService, - generateRandomPayto, + getTestHarnessPaytoForLabel, harnessHttpLib, setupDb, } from "../harness/harness.js"; @@ -78,7 +78,7 @@ export async function runMerchantInstancesTest(t: GlobalTestState) { await merchant.addInstanceWithWireAccount({ id: "default", name: "Default Instance", - paytoUris: [generateRandomPayto("merchant-default")], + paytoUris: [getTestHarnessPaytoForLabel("merchant-default")], auth: { method: "external", }, @@ -88,7 +88,7 @@ export async function runMerchantInstancesTest(t: GlobalTestState) { await merchant.addInstanceWithWireAccount({ id: "default", name: "Default Instance", - paytoUris: [generateRandomPayto("merchant-default")], + paytoUris: [getTestHarnessPaytoForLabel("merchant-default")], auth: { method: "external", }, @@ -98,7 +98,7 @@ export async function runMerchantInstancesTest(t: GlobalTestState) { await merchant.addInstanceWithWireAccount({ id: "myinst", name: "Second Instance", - paytoUris: [generateRandomPayto("merchant-default")], + paytoUris: [getTestHarnessPaytoForLabel("merchant-default")], auth: { method: "external", }, diff --git a/packages/taler-harness/src/integrationtests/test-multiexchange.ts b/packages/taler-harness/src/integrationtests/test-multiexchange.ts index 26e843073..8c1424437 100644 --- a/packages/taler-harness/src/integrationtests/test-multiexchange.ts +++ b/packages/taler-harness/src/integrationtests/test-multiexchange.ts @@ -26,7 +26,7 @@ import { GlobalTestState, HarnessExchangeBankAccount, MerchantService, - generateRandomPayto, + getTestHarnessPaytoForLabel, setupDb, } from "../harness/harness.js"; import { @@ -82,7 +82,7 @@ export async function runMultiExchangeTest(t: GlobalTestState) { ).href, accountName: "myexchange", accountPassword: "x", - accountPaytoUri: generateRandomPayto("myexchange"), + accountPaytoUri: getTestHarnessPaytoForLabel("myexchange"), }; let exchangeTwoBankAccount: HarnessExchangeBankAccount = { @@ -92,7 +92,7 @@ export async function runMultiExchangeTest(t: GlobalTestState) { ).href, accountName: "myexchange2", accountPassword: "x", - accountPaytoUri: generateRandomPayto("myexchange2"), + accountPaytoUri: getTestHarnessPaytoForLabel("myexchange2"), }; bank.setSuggestedExchange( @@ -152,7 +152,7 @@ export async function runMultiExchangeTest(t: GlobalTestState) { await merchant.addInstanceWithWireAccount({ id: "default", name: "Default Instance", - paytoUris: [generateRandomPayto("merchant-default")], + paytoUris: [getTestHarnessPaytoForLabel("merchant-default")], defaultWireTransferDelay: Duration.toTalerProtocolDuration( Duration.fromSpec({ minutes: 1 }), ), @@ -161,7 +161,7 @@ export async function runMultiExchangeTest(t: GlobalTestState) { await merchant.addInstanceWithWireAccount({ id: "minst1", name: "minst1", - paytoUris: [generateRandomPayto("minst1")], + paytoUris: [getTestHarnessPaytoForLabel("minst1")], defaultWireTransferDelay: Duration.toTalerProtocolDuration( Duration.fromSpec({ minutes: 1 }), ), diff --git a/packages/taler-harness/src/integrationtests/test-payment-fault.ts b/packages/taler-harness/src/integrationtests/test-payment-fault.ts index dabe42a6b..a6836953e 100644 --- a/packages/taler-harness/src/integrationtests/test-payment-fault.ts +++ b/packages/taler-harness/src/integrationtests/test-payment-fault.ts @@ -38,7 +38,7 @@ import { ExchangeService, GlobalTestState, MerchantService, - generateRandomPayto, + getTestHarnessPaytoForLabel, setupDb, } from "../harness/harness.js"; import { @@ -71,7 +71,7 @@ export async function runPaymentFaultTest(t: GlobalTestState) { let receiverName = "Exchange"; let exchangeBankUsername = "exchange"; let exchangeBankPassword = "mypw"; - let exchangePaytoUri = generateRandomPayto(exchangeBankUsername); + let exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername); await exchange.addBankAccount("1", { accountName: exchangeBankUsername, @@ -140,7 +140,7 @@ export async function runPaymentFaultTest(t: GlobalTestState) { await merchant.addInstanceWithWireAccount({ id: "default", name: "Default Instance", - paytoUris: [generateRandomPayto("merchant-default")], + paytoUris: [getTestHarnessPaytoForLabel("merchant-default")], }); const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl()); diff --git a/packages/taler-harness/src/integrationtests/test-payment-multiple.ts b/packages/taler-harness/src/integrationtests/test-payment-multiple.ts index 3c902ee17..e3f3e18e3 100644 --- a/packages/taler-harness/src/integrationtests/test-payment-multiple.ts +++ b/packages/taler-harness/src/integrationtests/test-payment-multiple.ts @@ -25,7 +25,7 @@ import { ExchangeService, GlobalTestState, MerchantService, - generateRandomPayto, + getTestHarnessPaytoForLabel, setupDb, } from "../harness/harness.js"; import { @@ -59,7 +59,7 @@ async function setupTest(t: GlobalTestState): Promise<{ let receiverName = "Exchange"; let exchangeBankUsername = "exchange"; let exchangeBankPassword = "mypw"; - let exchangePaytoUri = generateRandomPayto(exchangeBankUsername); + let exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername); await exchange.addBankAccount("1", { accountName: exchangeBankUsername, @@ -107,13 +107,13 @@ async function setupTest(t: GlobalTestState): Promise<{ await merchant.addInstanceWithWireAccount({ id: "default", name: "Default Instance", - paytoUris: [generateRandomPayto("merchant-default")], + paytoUris: [getTestHarnessPaytoForLabel("merchant-default")], }); await merchant.addInstanceWithWireAccount({ id: "minst1", name: "minst1", - paytoUris: [generateRandomPayto("minst1")], + paytoUris: [getTestHarnessPaytoForLabel("minst1")], }); console.log("setup done!"); diff --git a/packages/taler-harness/src/integrationtests/test-peer-to-peer-push.ts b/packages/taler-harness/src/integrationtests/test-peer-to-peer-push.ts index e38b690ab..05a0d2790 100644 --- a/packages/taler-harness/src/integrationtests/test-peer-to-peer-push.ts +++ b/packages/taler-harness/src/integrationtests/test-peer-to-peer-push.ts @@ -69,6 +69,19 @@ export async function runPeerToPeerPushTest(t: GlobalTestState) { await withdrawRes.withdrawalFinishedCond; + { + const maxResp1 = await w1.walletClient.call( + WalletApiOperation.GetMaxPeerPushDebitAmount, + { + currency: "TESTKUDOS", + }, + ); + + t.assertDeepEqual(maxResp1.exchangeBaseUrl, exchange.baseUrl); + t.assertAmountEquals(maxResp1.rawAmount, "TESTKUDOS:19.1"); + t.assertAmountEquals(maxResp1.effectiveAmount, "TESTKUDOS:19.53"); + } + const purse_expiration = AbsoluteTime.toProtocolTimestamp( AbsoluteTime.addDuration( AbsoluteTime.now(), @@ -83,6 +96,7 @@ export async function runPeerToPeerPushTest(t: GlobalTestState) { }, ); + t.assertAmountEquals(checkResp0.amountRaw, "TESTKUDOS:5"); t.assertAmountEquals(checkResp0.amountEffective, "TESTKUDOS:5.49"); { diff --git a/packages/taler-harness/src/integrationtests/test-refund-auto.ts b/packages/taler-harness/src/integrationtests/test-refund-auto.ts index 582f30299..822e465e0 100644 --- a/packages/taler-harness/src/integrationtests/test-refund-auto.ts +++ b/packages/taler-harness/src/integrationtests/test-refund-auto.ts @@ -62,12 +62,12 @@ export async function runRefundAutoTest(t: GlobalTestState) { summary: "Buy me!", amount: "TESTKUDOS:5", fulfillment_url: "taler://fulfillment-success/thx", - auto_refund: { - d_us: 3000 * 1000, - }, + auto_refund: Duration.toTalerProtocolDuration( + Duration.fromSpec({ minutes: 5 }), + ), }, refund_delay: Duration.toTalerProtocolDuration( - Duration.fromSpec({ minutes: 5 }), + Duration.fromSpec({ minutes: 10 }), ), }); @@ -104,6 +104,8 @@ export async function runRefundAutoTest(t: GlobalTestState) { console.log(ref); + t.logStep("gave-refund"); + // The wallet should now automatically pick up the refund. await walletClient.call( WalletApiOperation.TestingWaitTransactionsFinal, @@ -133,12 +135,12 @@ export async function runRefundAutoTest(t: GlobalTestState) { summary: "Buy me!", amount: "TESTKUDOS:5", fulfillment_url: "taler://fulfillment-success/thx", - auto_refund: { - d_us: 3000 * 1000, - }, + auto_refund: Duration.toTalerProtocolDuration( + Duration.fromSpec({ minutes: 5 }), + ), }, refund_delay: Duration.toTalerProtocolDuration( - Duration.fromSpec({ minutes: 5 }), + Duration.fromSpec({ minutes: 10 }), ), }); @@ -175,7 +177,7 @@ export async function runRefundAutoTest(t: GlobalTestState) { }); // Only time-travel the wallet await walletClient.call(WalletApiOperation.TestingSetTimetravel, { - offsetMs: 5000, + offsetMs: 10 * 60 * 1000, }); await walletClient.call(WalletApiOperation.TestingWaitTransactionState, { transactionId: r1.transactionId, diff --git a/packages/taler-harness/src/integrationtests/test-repurchase.ts b/packages/taler-harness/src/integrationtests/test-repurchase.ts new file mode 100644 index 000000000..e2dece8b7 --- /dev/null +++ b/packages/taler-harness/src/integrationtests/test-repurchase.ts @@ -0,0 +1,164 @@ +/* + 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 { + ConfirmPayResultType, + MerchantApiClient, + PreparePayResultType, + TalerMerchantApi, +} from "@gnu-taler/taler-util"; +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { GlobalTestState, harnessHttpLib } from "../harness/harness.js"; +import { + useSharedTestkudosEnvironment, + withdrawViaBankV2, +} from "../harness/helpers.js"; + +export async function runRepurchaseTest(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", + }); + + await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); + + const order = { + summary: "Buy me!", + amount: "TESTKUDOS:5", + fulfillment_url: "taler://fulfillment-success/thx", + } satisfies TalerMerchantApi.Order; + + const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl()); + + const orderOneResp = await merchantClient.createOrder({ + order: { + summary: "Buy me", + amount: "TESTKUDOS:5", + fulfillment_url: "https://example.com/test", + }, + }); + + let orderOneStatus = await merchantClient.queryPrivateOrderStatus({ + orderId: orderOneResp.order_id, + sessionId: "session1", + }); + + t.assertTrue(orderOneStatus.order_status === "unpaid"); + + const preparePayOneResult = await walletClient.call( + WalletApiOperation.PreparePayForUri, + { + talerPayUri: orderOneStatus.taler_pay_uri, + }, + ); + + t.assertTrue( + preparePayOneResult.status === PreparePayResultType.PaymentPossible, + ); + + const r2 = await walletClient.call(WalletApiOperation.ConfirmPay, { + transactionId: preparePayOneResult.transactionId, + }); + + t.assertTrue(r2.type === ConfirmPayResultType.Done); + + const orderTwoResp = await merchantClient.createOrder({ + order: { + summary: "Buy me", + amount: "TESTKUDOS:5", + fulfillment_url: "https://example.com/test", + }, + }); + + let orderTwoStatus = await merchantClient.queryPrivateOrderStatus({ + orderId: orderTwoResp.order_id, + sessionId: "session2", + }); + + t.assertTrue(orderTwoStatus.order_status === "unpaid"); + + const orderLongpollUrl = new URL( + `orders/${orderTwoResp.order_id}`, + merchant.makeInstanceBaseUrl(), + ); + if (orderTwoResp.token) { + orderLongpollUrl.searchParams.set("token", orderTwoResp.token); + } + orderLongpollUrl.searchParams.set("timeout_ms", "60000"); + orderLongpollUrl.searchParams.set("session_id", "session2"); + + const longpollPromise = harnessHttpLib.fetch(orderLongpollUrl.href); + + const preparePayTwoResult = await walletClient.call( + WalletApiOperation.PreparePayForUri, + { + talerPayUri: orderTwoStatus.taler_pay_uri, + }, + ); + + // Repurchase should be detected + t.assertTrue( + preparePayTwoResult.status === PreparePayResultType.AlreadyConfirmed, + ); + + t.logStep("start-wait-longpoll-promise"); + await longpollPromise; + t.logStep("done-wait-longpoll-promise"); + + // Order three + + const orderThreeResp = await merchantClient.createOrder({ + order: { + summary: "Buy me", + amount: "TESTKUDOS:5", + fulfillment_url: "https://example.com/test", + }, + }); + + let orderThreeStatus = await merchantClient.queryPrivateOrderStatus({ + orderId: orderThreeResp.order_id, + // Go back to session1 + sessionId: "session1", + }); + + t.assertTrue(orderThreeStatus.order_status === "unpaid"); + + const preparePayThreeResult = await walletClient.call( + WalletApiOperation.PreparePayForUri, + { + talerPayUri: orderThreeStatus.taler_pay_uri, + }, + ); + + // Repurchase should be detected + t.assertTrue( + preparePayThreeResult.status === PreparePayResultType.AlreadyConfirmed, + ); +} + +runRepurchaseTest.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/test-revocation.ts b/packages/taler-harness/src/integrationtests/test-revocation.ts index 8714a3769..d88a74150 100644 --- a/packages/taler-harness/src/integrationtests/test-revocation.ts +++ b/packages/taler-harness/src/integrationtests/test-revocation.ts @@ -31,7 +31,7 @@ import { WalletCli, WalletClient, delayMs, - generateRandomPayto, + getTestHarnessPaytoForLabel, setupDb, } from "../harness/harness.js"; import { @@ -93,7 +93,7 @@ async function createTestEnvironment( let receiverName = "Exchange"; let exchangeBankUsername = "exchange"; let exchangeBankPassword = "mypw"; - let exchangePaytoUri = generateRandomPayto(exchangeBankUsername); + let exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername); await exchange.addBankAccount("1", { accountName: exchangeBankUsername, @@ -153,13 +153,13 @@ async function createTestEnvironment( await merchant.addInstanceWithWireAccount({ id: "default", name: "Default Instance", - paytoUris: [generateRandomPayto("merchant-default")], + paytoUris: [getTestHarnessPaytoForLabel("merchant-default")], }); await merchant.addInstanceWithWireAccount({ id: "minst1", name: "minst1", - paytoUris: [generateRandomPayto("minst1")], + paytoUris: [getTestHarnessPaytoForLabel("minst1")], }); console.log("setup done!"); @@ -179,6 +179,7 @@ async function createTestEnvironment( merchant, walletClient, walletService, + bank, bankClient, exchangeBankAccount: { accountName: "", diff --git a/packages/taler-harness/src/integrationtests/test-timetravel-autorefresh.ts b/packages/taler-harness/src/integrationtests/test-timetravel-autorefresh.ts index 046bd5aed..27bf7bdd6 100644 --- a/packages/taler-harness/src/integrationtests/test-timetravel-autorefresh.ts +++ b/packages/taler-harness/src/integrationtests/test-timetravel-autorefresh.ts @@ -33,7 +33,7 @@ import { ExchangeService, GlobalTestState, MerchantService, - generateRandomPayto, + getTestHarnessPaytoForLabel, setupDb, } from "../harness/harness.js"; import { @@ -74,7 +74,7 @@ export async function runTimetravelAutorefreshTest(t: GlobalTestState) { let receiverName = "Exchange"; let exchangeBankUsername = "exchange"; let exchangeBankPassword = "mypw"; - let exchangePaytoUri = generateRandomPayto(exchangeBankUsername); + let exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername); await exchange.addBankAccount("1", { accountName: exchangeBankUsername, @@ -120,13 +120,13 @@ export async function runTimetravelAutorefreshTest(t: GlobalTestState) { await merchant.addInstanceWithWireAccount({ id: "default", name: "Default Instance", - paytoUris: [generateRandomPayto("merchant-default")], + paytoUris: [getTestHarnessPaytoForLabel("merchant-default")], }); await merchant.addInstanceWithWireAccount({ id: "minst1", name: "minst1", - paytoUris: [generateRandomPayto("minst1")], + paytoUris: [getTestHarnessPaytoForLabel("minst1")], }); console.log("setup done!"); diff --git a/packages/taler-harness/src/integrationtests/test-timetravel-withdraw.ts b/packages/taler-harness/src/integrationtests/test-timetravel-withdraw.ts index 4ee3a86e9..1ba7dc2ad 100644 --- a/packages/taler-harness/src/integrationtests/test-timetravel-withdraw.ts +++ b/packages/taler-harness/src/integrationtests/test-timetravel-withdraw.ts @@ -97,7 +97,7 @@ export async function runTimetravelWithdrawTest(t: GlobalTestState) { // Now we also let the wallet time travel - walletClient.call(WalletApiOperation.TestingSetTimetravel, { + await walletClient.call(WalletApiOperation.TestingSetTimetravel, { offsetMs: Duration.toMilliseconds(timetravelDuration), }); diff --git a/packages/taler-harness/src/integrationtests/test-wallet-balance.ts b/packages/taler-harness/src/integrationtests/test-wallet-balance.ts index c37a6e482..a8e62aee8 100644 --- a/packages/taler-harness/src/integrationtests/test-wallet-balance.ts +++ b/packages/taler-harness/src/integrationtests/test-wallet-balance.ts @@ -21,7 +21,6 @@ import { Amounts, Duration, MerchantApiClient, - MerchantContractTerms, PreparePayResultType, TalerMerchantApi, } from "@gnu-taler/taler-util"; diff --git a/packages/taler-harness/src/integrationtests/test-wallet-blocked-deposit.ts b/packages/taler-harness/src/integrationtests/test-wallet-blocked-deposit.ts index 66f985114..70f8989c8 100644 --- a/packages/taler-harness/src/integrationtests/test-wallet-blocked-deposit.ts +++ b/packages/taler-harness/src/integrationtests/test-wallet-blocked-deposit.ts @@ -26,7 +26,7 @@ import { } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { CoinConfig } from "../harness/denomStructures.js"; -import { GlobalTestState, generateRandomPayto } from "../harness/harness.js"; +import { GlobalTestState, getTestHarnessPaytoForLabel } from "../harness/harness.js"; import { createSimpleTestkudosEnvironmentV3, createWalletDaemonWithClient, @@ -104,7 +104,7 @@ export async function runWalletBlockedDepositTest(t: GlobalTestState) { }, }); - const userPayto = generateRandomPayto("foo"); + const userPayto = getTestHarnessPaytoForLabel("foo"); const bal = await w1.call(WalletApiOperation.GetBalances, {}); console.log(`balance: ${j2s(bal)}`); @@ -114,7 +114,7 @@ export async function runWalletBlockedDepositTest(t: GlobalTestState) { }); console.log(`balance details: ${j2s(balDet)}`); - const depositCheckResp = await w1.call(WalletApiOperation.PrepareDeposit, { + const depositCheckResp = await w1.call(WalletApiOperation.CheckDeposit, { amount: "TESTKUDOS:18" as AmountString, depositPaytoUri: userPayto, }); diff --git a/packages/taler-harness/src/integrationtests/test-wallet-dbless.ts b/packages/taler-harness/src/integrationtests/test-wallet-dbless.ts index a089d99b5..1d57e3458 100644 --- a/packages/taler-harness/src/integrationtests/test-wallet-dbless.ts +++ b/packages/taler-harness/src/integrationtests/test-wallet-dbless.ts @@ -25,7 +25,6 @@ import { TalerError, } from "@gnu-taler/taler-util"; import { - applyRunConfigDefaults, CryptoDispatcher, SynchronousCryptoWorkerFactoryPlain, } from "@gnu-taler/taler-wallet-core"; @@ -86,11 +85,10 @@ export async function runWalletDblessTest(t: GlobalTestState) { console.log(exchangeInfo); await checkReserve(http, exchange.baseUrl, reserveKeyPair.pub); - - const defaultConfig = applyRunConfigDefaults(); + const denomselAllowLate = false; const d1 = findDenomOrThrow(exchangeInfo, "TESTKUDOS:8" as AmountString, { - denomselAllowLate: defaultConfig.testing.denomselAllowLate, + denomselAllowLate, }); const coin = await withdrawCoin({ @@ -133,10 +131,10 @@ export async function runWalletDblessTest(t: GlobalTestState) { const refreshDenoms = [ findDenomOrThrow(exchangeInfo, "TESTKUDOS:1" as AmountString, { - denomselAllowLate: defaultConfig.testing.denomselAllowLate, + denomselAllowLate, }), findDenomOrThrow(exchangeInfo, "TESTKUDOS:1" as AmountString, { - denomselAllowLate: defaultConfig.testing.denomselAllowLate, + denomselAllowLate, }), ]; diff --git a/packages/taler-harness/src/integrationtests/test-wallet-dd48.ts b/packages/taler-harness/src/integrationtests/test-wallet-dd48.ts index ba2b2670c..7378e272a 100644 --- a/packages/taler-harness/src/integrationtests/test-wallet-dd48.ts +++ b/packages/taler-harness/src/integrationtests/test-wallet-dd48.ts @@ -34,7 +34,7 @@ import { GlobalTestState, WalletClient, WalletService, - generateRandomPayto, + getTestHarnessPaytoForLabel, setupDb, } from "../harness/harness.js"; import { withdrawViaBankV3 } from "../harness/helpers.js"; @@ -64,7 +64,7 @@ export async function runWalletDd48Test(t: GlobalTestState) { let receiverName = "Exchange"; let exchangeBankUsername = "exchange"; let exchangeBankPassword = "mypw"; - let exchangePaytoUri = generateRandomPayto(exchangeBankUsername); + let exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername); await exchange.addBankAccount("1", { accountName: exchangeBankUsername, diff --git a/packages/taler-harness/src/integrationtests/test-wallet-exchange-update.ts b/packages/taler-harness/src/integrationtests/test-wallet-exchange-update.ts index 3a1b467c3..3d7ea32e6 100644 --- a/packages/taler-harness/src/integrationtests/test-wallet-exchange-update.ts +++ b/packages/taler-harness/src/integrationtests/test-wallet-exchange-update.ts @@ -32,7 +32,7 @@ import { FakebankService, GlobalTestState, HarnessExchangeBankAccount, - generateRandomPayto, + getTestHarnessPaytoForLabel, setupDb, } from "../harness/harness.js"; import { @@ -98,7 +98,7 @@ export async function runWalletExchangeUpdateTest( ).href, accountName: "myexchange", accountPassword: "x", - accountPaytoUri: generateRandomPayto("myexchange"), + accountPaytoUri: getTestHarnessPaytoForLabel("myexchange"), }; await exchangeOne.addBankAccount("1", exchangeBankAccount); diff --git a/packages/taler-harness/src/integrationtests/test-wallet-insufficient-balance.ts b/packages/taler-harness/src/integrationtests/test-wallet-insufficient-balance.ts index 4062e186d..3b1f4bf27 100644 --- a/packages/taler-harness/src/integrationtests/test-wallet-insufficient-balance.ts +++ b/packages/taler-harness/src/integrationtests/test-wallet-insufficient-balance.ts @@ -20,39 +20,82 @@ import { AmountString, Duration, + j2s, PaymentInsufficientBalanceDetails, TalerErrorCode, } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js"; import { + ExchangeService, + getTestHarnessPaytoForLabel, GlobalTestState, - generateRandomPayto, + HarnessExchangeBankAccount, setupDb, } from "../harness/harness.js"; -import { createSimpleTestkudosEnvironmentV3, withdrawViaBankV3 } from "../harness/helpers.js"; +import { + createSimpleTestkudosEnvironmentV3, + withdrawViaBankV3, +} from "../harness/helpers.js"; export async function runWalletInsufficientBalanceTest(t: GlobalTestState) { // Set up test environment - const db = await setupDb(t); - const coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")); - let { - bankClient, - exchange, - merchant, - walletService, - walletClient, - } = await createSimpleTestkudosEnvironmentV3(t, coinConfig, { - skipWireFeeCreation: true, + let { bankClient, bank, exchange, merchant, walletClient } = + await createSimpleTestkudosEnvironmentV3(t, coinConfig, { + skipWireFeeCreation: true, + }); + + const dbTwo = await setupDb(t, { + nameSuffix: "two", }); + const exchangeTwo = ExchangeService.create(t, { + name: "testexchange-2", + currency: "TESTKUDOS", + httpPort: 9081, + database: dbTwo.connStr, + }); + + { + const receiverName = "Exchange2"; + const exchangeBankUsername = "exchange2"; + const exchangeBankPassword = "mypw"; + const exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername); + const wireGatewayApiBaseUrl = new URL( + `accounts/${exchangeBankUsername}/taler-wire-gateway/`, + bank.corebankApiBaseUrl, + ).href; + + const exchangeBankAccount: HarnessExchangeBankAccount = { + wireGatewayApiBaseUrl, + accountName: exchangeBankUsername, + accountPassword: exchangeBankPassword, + accountPaytoUri: exchangePaytoUri, + skipWireFeeCreation: true, + }; + + await exchangeTwo.addBankAccount("1", exchangeBankAccount); + + await bankClient.registerAccountExtended({ + name: receiverName, + password: exchangeBankPassword, + username: exchangeBankUsername, + is_taler_exchange: true, + payto_uri: exchangePaytoUri, + }); + + exchangeTwo.addCoinConfigList(coinConfig); + + await exchangeTwo.start(); + } + await merchant.addInstanceWithWireAccount({ id: "default", name: "Default Instance", - paytoUris: [generateRandomPayto("merchant-default")], + paytoUris: [getTestHarnessPaytoForLabel("merchant-default")], defaultWireTransferDelay: Duration.toTalerProtocolDuration( Duration.fromSpec({ minutes: 1 }), ), @@ -61,7 +104,7 @@ export async function runWalletInsufficientBalanceTest(t: GlobalTestState) { await merchant.addInstanceWithWireAccount({ id: "minst1", name: "minst1", - paytoUris: [generateRandomPayto("minst1")], + paytoUris: [getTestHarnessPaytoForLabel("minst1")], defaultWireTransferDelay: Duration.toTalerProtocolDuration( Duration.fromSpec({ minutes: 1 }), ), @@ -75,6 +118,8 @@ export async function runWalletInsufficientBalanceTest(t: GlobalTestState) { }, }); + t.logStep("setup-done"); + const wres = await withdrawViaBankV3(t, { amount: "TESTKUDOS:10", bankClient, @@ -83,29 +128,74 @@ export async function runWalletInsufficientBalanceTest(t: GlobalTestState) { }); await wres.withdrawalFinishedCond; - const exc = await t.assertThrowsTalerErrorAsync(async () => { - await walletClient.call(WalletApiOperation.PrepareDeposit, { - amount: "TESTKUDOS:5" as AmountString, - depositPaytoUri: "payto://x-taler-bank/localhost/foobar", + { + const exc = await t.assertThrowsTalerErrorAsync(async () => { + await walletClient.call(WalletApiOperation.CheckDeposit, { + amount: "TESTKUDOS:5" as AmountString, + depositPaytoUri: "payto://x-taler-bank/localhost/foobar", + }); + }); + + t.assertDeepEqual( + exc.errorDetail.code, + TalerErrorCode.WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE, + ); + + const insufficientBalanceDetails: PaymentInsufficientBalanceDetails = + exc.errorDetail.insufficientBalanceDetails; + + t.assertAmountEquals( + insufficientBalanceDetails.balanceAvailable, + "TESTKUDOS:9.72", + ); + t.assertAmountEquals( + insufficientBalanceDetails.balanceExchangeDepositable, + "TESTKUDOS:0", + ); + } + + t.logStep("start-p2p-push-test"); + + // Now check for p2p-push + + { + const wres2 = await withdrawViaBankV3(t, { + amount: "TESTKUDOS:5", + bankClient, + exchange: exchangeTwo, + walletClient, + }); + await wres2.withdrawalFinishedCond; + + const exc = await t.assertThrowsTalerErrorAsync(async () => { + await walletClient.call(WalletApiOperation.CheckPeerPushDebit, { + amount: "TESTKUDOS:20" as AmountString, + }); }); - }); - t.assertDeepEqual( - exc.errorDetail.code, - TalerErrorCode.WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE, - ); - - const insufficientBalanceDetails: PaymentInsufficientBalanceDetails = - exc.errorDetail.insufficientBalanceDetails; - - t.assertAmountEquals( - insufficientBalanceDetails.balanceAvailable, - "TESTKUDOS:9.72", - ); - t.assertAmountEquals( - insufficientBalanceDetails.balanceExchangeDepositable, - "TESTKUDOS:0", - ); + const insufficientBalanceDetails: PaymentInsufficientBalanceDetails = + exc.errorDetail.insufficientBalanceDetails; + + const perMyExchange = + insufficientBalanceDetails.perExchange[exchange.baseUrl]; + + t.assertTrue(!!perMyExchange); + + console.log(j2s(exc.errorDetail)); + + t.assertAmountEquals( + insufficientBalanceDetails.amountRequested, + "TESTKUDOS:20", + ); + t.assertAmountEquals( + insufficientBalanceDetails.maxEffectiveSpendAmount, + "TESTKUDOS:14.22", + ); + t.assertAmountEquals( + perMyExchange.maxEffectiveSpendAmount, + "TESTKUDOS:9.47", + ); + } } runWalletInsufficientBalanceTest.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/test-wallet-network-availability.ts b/packages/taler-harness/src/integrationtests/test-wallet-network-availability.ts index d97737e25..ba0038608 100644 --- a/packages/taler-harness/src/integrationtests/test-wallet-network-availability.ts +++ b/packages/taler-harness/src/integrationtests/test-wallet-network-availability.ts @@ -25,7 +25,7 @@ import { TransactionType, } from "@gnu-taler/taler-util"; import { WalletApiOperation, parseTransactionIdentifier } from "@gnu-taler/taler-wallet-core"; -import { GlobalTestState, generateRandomPayto } from "harness/harness.js"; +import { GlobalTestState, getTestHarnessPaytoForLabel } from "harness/harness.js"; import { createSimpleTestkudosEnvironmentV3, withdrawViaBankV3 } from "harness/helpers.js"; import { TaskRunResultType } from "../../../taler-wallet-core/src/common.js"; @@ -78,7 +78,7 @@ export async function runWalletNetworkAvailabilityTest(t: GlobalTestState) { WalletApiOperation.CreateDepositGroup, { amount: "TESTKUDOS:10.5" as AmountString, - depositPaytoUri: generateRandomPayto("foo"), + depositPaytoUri: getTestHarnessPaytoForLabel("foo"), }, ); diff --git a/packages/taler-harness/src/integrationtests/test-wallet-notifications.ts b/packages/taler-harness/src/integrationtests/test-wallet-notifications.ts index 5088c8228..27280103b 100644 --- a/packages/taler-harness/src/integrationtests/test-wallet-notifications.ts +++ b/packages/taler-harness/src/integrationtests/test-wallet-notifications.ts @@ -32,7 +32,7 @@ import { MerchantService, WalletClient, WalletService, - generateRandomPayto, + getTestHarnessPaytoForLabel, generateRandomTestIban, setupDb, } from "../harness/harness.js"; @@ -62,7 +62,7 @@ export async function runWalletNotificationsTest(t: GlobalTestState) { let receiverName = "Exchange"; let exchangeBankUsername = "exchange"; let exchangeBankPassword = "mypw"; - let exchangePaytoUri = generateRandomPayto(exchangeBankUsername); + let exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername); const merchant = await MerchantService.create(t, { name: "testmerchant-1", diff --git a/packages/taler-harness/src/integrationtests/test-wallet-observability.ts b/packages/taler-harness/src/integrationtests/test-wallet-observability.ts index 55a60cb76..cf924f649 100644 --- a/packages/taler-harness/src/integrationtests/test-wallet-observability.ts +++ b/packages/taler-harness/src/integrationtests/test-wallet-observability.ts @@ -26,7 +26,7 @@ import { GlobalTestState, WalletClient, WalletService, - generateRandomPayto, + getTestHarnessPaytoForLabel, setupDb, } from "../harness/harness.js"; import { withdrawViaBankV3 } from "../harness/helpers.js"; @@ -53,7 +53,7 @@ export async function runWalletObservabilityTest(t: GlobalTestState) { let receiverName = "Exchange"; let exchangeBankUsername = "exchange"; let exchangeBankPassword = "mypw"; - let exchangePaytoUri = generateRandomPayto(exchangeBankUsername); + let exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername); await exchange.addBankAccount("1", { accountName: exchangeBankUsername, diff --git a/packages/taler-harness/src/integrationtests/test-wallet-refresh.ts b/packages/taler-harness/src/integrationtests/test-wallet-refresh.ts index 93fe94270..6197e6c36 100644 --- a/packages/taler-harness/src/integrationtests/test-wallet-refresh.ts +++ b/packages/taler-harness/src/integrationtests/test-wallet-refresh.ts @@ -30,7 +30,7 @@ import { WalletApiOperation, parseTransactionIdentifier, } from "@gnu-taler/taler-wallet-core"; -import { GlobalTestState, generateRandomPayto } from "../harness/harness.js"; +import { GlobalTestState, getTestHarnessPaytoForLabel } from "../harness/harness.js"; import { createSimpleTestkudosEnvironmentV3, makeTestPaymentV2, @@ -120,7 +120,7 @@ export async function runWalletRefreshTest(t: GlobalTestState) { WalletApiOperation.CreateDepositGroup, { amount: "TESTKUDOS:10.5" as AmountString, - depositPaytoUri: generateRandomPayto("foo"), + depositPaytoUri: getTestHarnessPaytoForLabel("foo"), }, ); @@ -175,7 +175,7 @@ export async function runWalletRefreshTest(t: GlobalTestState) { WalletApiOperation.CreateDepositGroup, { amount: "TESTKUDOS:10.5" as AmountString, - depositPaytoUri: generateRandomPayto("foo"), + depositPaytoUri: getTestHarnessPaytoForLabel("foo"), }, ); diff --git a/packages/taler-harness/src/integrationtests/test-wallet-wirefees.ts b/packages/taler-harness/src/integrationtests/test-wallet-wirefees.ts index c5a0fd363..359adc7e3 100644 --- a/packages/taler-harness/src/integrationtests/test-wallet-wirefees.ts +++ b/packages/taler-harness/src/integrationtests/test-wallet-wirefees.ts @@ -32,7 +32,7 @@ import { ExchangeService, GlobalTestState, MerchantService, - generateRandomPayto, + getTestHarnessPaytoForLabel, setupDb, } from "../harness/harness.js"; import { @@ -74,7 +74,7 @@ export async function runWalletWirefeesTest(t: GlobalTestState) { let receiverName = "Exchange"; let exchangeBankUsername = "exchange"; let exchangeBankPassword = "mypw"; - let exchangePaytoUri = generateRandomPayto(exchangeBankUsername); + let exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername); await exchange.addBankAccount("1", { accountName: exchangeBankUsername, @@ -121,7 +121,7 @@ export async function runWalletWirefeesTest(t: GlobalTestState) { await merchant.addInstanceWithWireAccount({ id: "default", name: "Default Instance", - paytoUris: [generateRandomPayto("merchant-default")], + paytoUris: [getTestHarnessPaytoForLabel("merchant-default")], defaultWireTransferDelay: Duration.toTalerProtocolDuration( Duration.fromSpec({ minutes: 1 }), ), @@ -130,7 +130,7 @@ export async function runWalletWirefeesTest(t: GlobalTestState) { await merchant.addInstanceWithWireAccount({ id: "minst1", name: "minst1", - paytoUris: [generateRandomPayto("minst1")], + paytoUris: [getTestHarnessPaytoForLabel("minst1")], defaultWireTransferDelay: Duration.toTalerProtocolDuration( Duration.fromSpec({ minutes: 1 }), ), diff --git a/packages/taler-harness/src/integrationtests/test-wallettesting.ts b/packages/taler-harness/src/integrationtests/test-wallettesting.ts index ef9bb9073..42b73654c 100644 --- a/packages/taler-harness/src/integrationtests/test-wallettesting.ts +++ b/packages/taler-harness/src/integrationtests/test-wallettesting.ts @@ -25,10 +25,7 @@ import { AmountString, Amounts, CoinStatus } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js"; -import { - GlobalTestState, - setupDb, -} from "../harness/harness.js"; +import { GlobalTestState, setupDb } from "../harness/harness.js"; import { SimpleTestEnvironmentNg3, createSimpleTestkudosEnvironmentV3, @@ -47,10 +44,9 @@ export async function createMyEnvironment( ): Promise<SimpleTestEnvironmentNg3> { const db = await setupDb(t); - const {bankClient, exchange, merchant, exchangeBankAccount} = + const { bankClient, bank, exchange, merchant, exchangeBankAccount } = await createSimpleTestkudosEnvironmentV3(t, coinConfig, {}); - console.log("setup done!"); const { walletClient, walletService } = await createWalletDaemonWithClient( @@ -66,6 +62,7 @@ export async function createMyEnvironment( merchant, walletClient, walletService, + bank, bankClient, exchangeBankAccount, }; @@ -187,8 +184,6 @@ export async function runWallettestingTest(t: GlobalTestState) { await walletClient.call(WalletApiOperation.ClearDb, {}); await walletClient.call(WalletApiOperation.RunIntegrationTestV2, { - amountToSpend: "TESTKUDOS:5" as AmountString, - amountToWithdraw: "TESTKUDOS:10" as AmountString, corebankApiBaseUrl: bankClient.baseUrl, exchangeBaseUrl: exchange.baseUrl, merchantAuthToken: merchantAuthToken, diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-conversion.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-conversion.ts index c55e1faf0..2f6d2ff5d 100644 --- a/packages/taler-harness/src/integrationtests/test-withdrawal-conversion.ts +++ b/packages/taler-harness/src/integrationtests/test-withdrawal-conversion.ts @@ -33,13 +33,12 @@ import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import * as http from "node:http"; import { defaultCoinConfig } from "../harness/denomStructures.js"; import { - BankService, + BankService, ExchangeService, - FakebankService, GlobalTestState, HarnessExchangeBankAccount, MerchantService, - generateRandomPayto, + getTestHarnessPaytoForLabel, setupDb, } from "../harness/harness.js"; import { createWalletDaemonWithClient } from "../harness/helpers.js"; @@ -104,7 +103,7 @@ async function runTestfakeConversionService(): Promise<TestfakeConversionService cashout_rounding_mode: "zero", cashout_tiny_amount: "A:1" as AmountString, }, - } satisfies TalerBankConversionApi.IntegrationConfig), + } satisfies TalerBankConversionApi.TalerConversionInfoConfig), ); } else if (path === "/cashin-rate") { res.writeHead(200, { "Content-Type": "application/json" }); @@ -165,7 +164,7 @@ export async function runWithdrawalConversionTest(t: GlobalTestState) { ).href, accountName: "myexchange", accountPassword: "x", - accountPaytoUri: generateRandomPayto("myexchange"), + accountPaytoUri: getTestHarnessPaytoForLabel("myexchange"), conversionUrl: "http://localhost:8071/", }; @@ -204,7 +203,7 @@ export async function runWithdrawalConversionTest(t: GlobalTestState) { await merchant.addInstanceWithWireAccount({ id: "default", name: "Default Instance", - paytoUris: [generateRandomPayto("merchant-default")], + paytoUris: [getTestHarnessPaytoForLabel("merchant-default")], defaultWireTransferDelay: Duration.toTalerProtocolDuration( Duration.fromSpec({ minutes: 1 }), ), @@ -213,16 +212,15 @@ export async function runWithdrawalConversionTest(t: GlobalTestState) { await merchant.addInstanceWithWireAccount({ id: "minst1", name: "minst1", - paytoUris: [generateRandomPayto("minst1")], + paytoUris: [getTestHarnessPaytoForLabel("minst1")], defaultWireTransferDelay: Duration.toTalerProtocolDuration( Duration.fromSpec({ minutes: 1 }), ), }); - const { walletClient } = await createWalletDaemonWithClient( - t, - { name: "wallet" }, - ); + const { walletClient } = await createWalletDaemonWithClient(t, { + name: "wallet", + }); await runTestfakeConversionService(); diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-external.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-external.ts new file mode 100644 index 000000000..3cd02882b --- /dev/null +++ b/packages/taler-harness/src/integrationtests/test-withdrawal-external.ts @@ -0,0 +1,101 @@ +/* + 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 { + TransactionMajorState, + TransactionMinorState, + TransactionType, + WithdrawalType, +} from "@gnu-taler/taler-util"; +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { GlobalTestState } from "../harness/harness.js"; +import { createSimpleTestkudosEnvironmentV3 } from "../harness/helpers.js"; + +/** + * Test for a withdrawal that is externally confirmed. + */ +export async function runWithdrawalExternalTest(t: GlobalTestState) { + // Set up test environment + + const { walletClient, bankClient, exchange } = + await createSimpleTestkudosEnvironmentV3(t); + + // Create a withdrawal operation + + const bankUser = await bankClient.createRandomBankUser(); + bankClient.setAuth(bankUser); + const wop = await bankClient.createWithdrawalOperation( + bankUser.username, + "TESTKUDOS:10", + ); + + const talerWithdrawUri = wop.taler_withdraw_uri + "?external-confirmation=1"; + + // Hand it to the wallet + + const detResp = await walletClient.call( + WalletApiOperation.GetWithdrawalDetailsForUri, + { + talerWithdrawUri: talerWithdrawUri, + }, + ); + + const acceptResp = await walletClient.call( + WalletApiOperation.AcceptBankIntegratedWithdrawal, + { + exchangeBaseUrl: detResp.defaultExchangeBaseUrl!!, + talerWithdrawUri, + }, + ); + + await walletClient.call(WalletApiOperation.TestingWaitTransactionState, { + transactionId: acceptResp.transactionId, + txState: { + major: TransactionMajorState.Pending, + minor: TransactionMinorState.BankConfirmTransfer, + }, + }); + + const txDetails = await walletClient.call( + WalletApiOperation.GetTransactionById, + { + transactionId: acceptResp.transactionId, + }, + ); + + // Now we check that the external-confirmation=1 flag actually did something! + + t.assertDeepEqual(txDetails.type, TransactionType.Withdrawal); + t.assertDeepEqual( + txDetails.withdrawalDetails.type, + WithdrawalType.TalerBankIntegrationApi, + ); + t.assertDeepEqual(txDetails.withdrawalDetails.externalConfirmation, true); + t.assertDeepEqual(txDetails.withdrawalDetails.bankConfirmationUrl, undefined); + + t.logStep("confirming withdrawal operation"); + + await bankClient.confirmWithdrawalOperation(bankUser.username, { + withdrawalOperationId: wop.withdrawal_id, + }); + + await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); +} + +runWithdrawalExternalTest.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-fees.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-fees.ts index 0657d2da7..c7bf0b938 100644 --- a/packages/taler-harness/src/integrationtests/test-withdrawal-fees.ts +++ b/packages/taler-harness/src/integrationtests/test-withdrawal-fees.ts @@ -25,7 +25,7 @@ import { ExchangeService, GlobalTestState, WalletCli, - generateRandomPayto, + getTestHarnessPaytoForLabel, setupDb, } from "../harness/harness.js"; @@ -85,7 +85,7 @@ export async function runWithdrawalFeesTest(t: GlobalTestState) { let receiverName = "Exchange"; let exchangeBankUsername = "exchange"; let exchangeBankPassword = "mypw"; - let exchangePaytoUri = generateRandomPayto(exchangeBankUsername); + let exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername); await exchange.addBankAccount("1", { accountName: exchangeBankUsername, diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-idempotent.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-idempotent.ts new file mode 100644 index 000000000..0daa53f64 --- /dev/null +++ b/packages/taler-harness/src/integrationtests/test-withdrawal-idempotent.ts @@ -0,0 +1,172 @@ +/* + 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 { + AgeRestriction, + Amounts, + AmountString, + codecForExchangeWithdrawBatchResponse, + encodeCrock, + ExchangeBatchWithdrawRequest, + getRandomBytes, +} from "@gnu-taler/taler-util"; +import { + HttpRequestLibrary, + readSuccessResponseJsonOrThrow, +} from "@gnu-taler/taler-util/http"; +import { + CryptoDispatcher, + SynchronousCryptoWorkerFactoryPlain, + TalerCryptoInterface, +} from "@gnu-taler/taler-wallet-core"; +import { + checkReserve, + CoinInfo, + downloadExchangeInfo, + findDenomOrThrow, + ReserveKeypair, + topupReserveWithBank, +} from "@gnu-taler/taler-wallet-core/dbless"; +import { DenominationRecord } from "../../../taler-wallet-core/src/db.js"; +import { GlobalTestState, harnessHttpLib } from "../harness/harness.js"; +import { createSimpleTestkudosEnvironmentV2 } from "../harness/helpers.js"; + +/** + * Run test for basic, bank-integrated withdrawal and payment. + */ +export async function runWithdrawalIdempotentTest(t: GlobalTestState) { + // Set up test environment + + const { bank, exchange } = await createSimpleTestkudosEnvironmentV2(t); + + const http = harnessHttpLib; + const cryptiDisp = new CryptoDispatcher( + new SynchronousCryptoWorkerFactoryPlain(), + ); + const cryptoApi = cryptiDisp.cryptoApi; + + const exchangeInfo = await downloadExchangeInfo(exchange.baseUrl, http); + + const reserveKeyPair = await cryptoApi.createEddsaKeypair({}); + + let reserveUrl = new URL(`reserves/${reserveKeyPair.pub}`, exchange.baseUrl); + reserveUrl.searchParams.set("timeout_ms", "30000"); + const longpollReq = http.fetch(reserveUrl.href, { + method: "GET", + }); + + await topupReserveWithBank({ + amount: "TESTKUDOS:10" as AmountString, + http, + reservePub: reserveKeyPair.pub, + corebankApiBaseUrl: bank.corebankApiBaseUrl, + exchangeInfo, + }); + + console.log("waiting for longpoll request"); + const resp = await longpollReq; + console.log(`got response, status ${resp.status}`); + + console.log(exchangeInfo); + + await checkReserve(http, exchange.baseUrl, reserveKeyPair.pub); + const denomselAllowLate = false; + + const d1 = findDenomOrThrow(exchangeInfo, "TESTKUDOS:8" as AmountString, { + denomselAllowLate, + }); + + await myWithdrawCoin({ + http, + cryptoApi, + reserveKeyPair: { + reservePriv: reserveKeyPair.priv, + reservePub: reserveKeyPair.pub, + }, + denom: d1, + exchangeBaseUrl: exchange.baseUrl, + }); +} + +async function myWithdrawCoin(args: { + http: HttpRequestLibrary; + cryptoApi: TalerCryptoInterface; + reserveKeyPair: ReserveKeypair; + denom: DenominationRecord; + exchangeBaseUrl: string; +}): Promise<CoinInfo> { + const { http, cryptoApi, reserveKeyPair, denom, exchangeBaseUrl } = args; + const planchet = await cryptoApi.createPlanchet({ + coinIndex: 0, + denomPub: denom.denomPub, + feeWithdraw: Amounts.parseOrThrow(denom.fees.feeWithdraw), + reservePriv: reserveKeyPair.reservePriv, + reservePub: reserveKeyPair.reservePub, + secretSeed: encodeCrock(getRandomBytes(32)), + value: Amounts.parseOrThrow(denom.value), + }); + + const reqBody: ExchangeBatchWithdrawRequest = { + planchets: [ + { + denom_pub_hash: planchet.denomPubHash, + reserve_sig: planchet.withdrawSig, + coin_ev: planchet.coinEv, + }, + ], + }; + const reqUrl = new URL( + `reserves/${planchet.reservePub}/batch-withdraw`, + exchangeBaseUrl, + ).href; + + const resp = await http.fetch(reqUrl, { method: "POST", body: reqBody }); + const rBatch = await readSuccessResponseJsonOrThrow( + resp, + codecForExchangeWithdrawBatchResponse(), + ); + + { + // Check for idempotency! + const resp2 = await http.fetch(reqUrl, { method: "POST", body: reqBody }); + await readSuccessResponseJsonOrThrow( + resp2, + codecForExchangeWithdrawBatchResponse(), + ); + } + + const ubSig = await cryptoApi.unblindDenominationSignature({ + planchet, + evSig: rBatch.ev_sigs[0].ev_sig, + }); + + return { + coinPriv: planchet.coinPriv, + coinPub: planchet.coinPub, + denomSig: ubSig, + denomPub: denom.denomPub, + denomPubHash: denom.denomPubHash, + feeDeposit: Amounts.stringify(denom.fees.feeDeposit), + feeRefresh: Amounts.stringify(denom.fees.feeRefresh), + exchangeBaseUrl: args.exchangeBaseUrl, + maxAge: AgeRestriction.AGE_UNRESTRICTED, + }; +} + +runWithdrawalIdempotentTest.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-prepare.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-prepare.ts new file mode 100644 index 000000000..b5c6cc769 --- /dev/null +++ b/packages/taler-harness/src/integrationtests/test-withdrawal-prepare.ts @@ -0,0 +1,78 @@ +/* + 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 { j2s, Logger } from "@gnu-taler/taler-util"; +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { GlobalTestState } from "../harness/harness.js"; +import { createSimpleTestkudosEnvironmentV3 } from "../harness/helpers.js"; + +const logger = new Logger("test-withdrawal-prepare.ts"); + +/** + * Test the separate prepare step of a bank-integrated withdrawal. + */ +export async function runWithdrawalPrepareTest(t: GlobalTestState) { + // Set up test environment + + const { walletClient, bankClient, exchange } = + await createSimpleTestkudosEnvironmentV3(t); + + // Create a withdrawal operation + const user = await bankClient.createRandomBankUser(); + bankClient.setAuth(user); + const wop = await bankClient.createWithdrawalOperation( + user.username, + "TESTKUDOS:20", + ); + + const r1 = await walletClient.call( + WalletApiOperation.GetWithdrawalDetailsForUri, + { + talerWithdrawUri: wop.taler_withdraw_uri, + }, + ); + + console.log(j2s(r1)); + + //t.assertTrue(!r1.amount); + + // Withdraw + + const prepRes = await walletClient.call( + WalletApiOperation.PrepareBankIntegratedWithdrawal, + { + talerWithdrawUri: wop.taler_withdraw_uri, + }, + ); + + logger.info(`Prep res: ${j2s(prepRes)}`); + + // Make sure that we can get the transaction details for the prepared transaction + + const txDetail = await walletClient.call( + WalletApiOperation.GetTransactionById, + { + transactionId: prepRes.transactionId, + }, + ); + + logger.info(`Transaction details: ${j2s(txDetail)}`); +} + +runWithdrawalPrepareTest.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts index 238bf3b98..5b36d6145 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"; @@ -47,6 +48,18 @@ 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 { runKycBalanceWithdrawalTest } from "./test-kyc-balance-withdrawal.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 { 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 { runKycThresholdWithdrawalTest } from "./test-kyc-threshold-withdrawal.js"; import { runKycTest } from "./test-kyc.js"; import { runLibeufinBankTest } from "./test-libeufin-bank.js"; import { runMerchantCategoriesTest } from "./test-merchant-categories.js"; @@ -83,6 +96,7 @@ import { runRefundAutoTest } from "./test-refund-auto.js"; import { runRefundGoneTest } from "./test-refund-gone.js"; import { runRefundIncrementalTest } from "./test-refund-incremental.js"; import { runRefundTest } from "./test-refund.js"; +import { runRepurchaseTest } from "./test-repurchase.js"; import { runRevocationTest } from "./test-revocation.js"; import { runSimplePaymentTest } from "./test-simple-payment.js"; import { runStoredBackupsTest } from "./test-stored-backups.js"; @@ -108,6 +122,7 @@ import { runWalletDevExperimentsTest } from "./test-wallet-dev-experiments.js"; import { runWalletExchangeUpdateTest } from "./test-wallet-exchange-update.js"; import { runWalletGenDbTest } from "./test-wallet-gendb.js"; import { runWalletInsufficientBalanceTest } from "./test-wallet-insufficient-balance.js"; +import { runWalletNetworkAvailabilityTest } from "./test-wallet-network-availability.js"; import { runWalletNotificationsTest } from "./test-wallet-notifications.js"; import { runWalletObservabilityTest } from "./test-wallet-observability.js"; import { runWalletRefreshErrorsTest } from "./test-wallet-refresh-errors.js"; @@ -118,13 +133,15 @@ import { runWithdrawalAbortBankTest } from "./test-withdrawal-abort-bank.js"; import { runWithdrawalAmountTest } from "./test-withdrawal-amount.js"; import { runWithdrawalBankIntegratedTest } from "./test-withdrawal-bank-integrated.js"; import { runWithdrawalConversionTest } from "./test-withdrawal-conversion.js"; +import { runWithdrawalExternalTest } from "./test-withdrawal-external.js"; import { runWithdrawalFakebankTest } from "./test-withdrawal-fakebank.js"; import { runWithdrawalFeesTest } from "./test-withdrawal-fees.js"; import { runWithdrawalFlexTest } from "./test-withdrawal-flex.js"; import { runWithdrawalHandoverTest } from "./test-withdrawal-handover.js"; import { runWithdrawalHugeTest } from "./test-withdrawal-huge.js"; +import { runWithdrawalIdempotentTest } from "./test-withdrawal-idempotent.js"; import { runWithdrawalManualTest } from "./test-withdrawal-manual.js"; -import { runWalletNetworkAvailabilityTest } from "./test-wallet-network-availability.js"; +import { runWithdrawalPrepareTest } from "./test-withdrawal-prepare.js"; /** * Test runner. @@ -240,6 +257,23 @@ const allTests: TestMainFunction[] = [ runWithdrawalFlexTest, runExchangeMasterPubChangeTest, runMerchantCategoriesTest, + runWithdrawalExternalTest, + runWithdrawalIdempotentTest, + runKycThresholdWithdrawalTest, + runKycExchangeWalletTest, + runKycPeerPushTest, + runKycPeerPullTest, + runKycDepositAggregateTest, + runKycFormWithdrawalTest, + runKycBalanceWithdrawalTest, + runKycNewMeasureTest, + runKycDepositDepositTest, + runKycMerchantDepositTest, + runKycMerchantAggregateTest, + runKycDepositDepositKyctransferTest, + runWithdrawalPrepareTest, + runAccountRestrictionsTest, + runRepurchaseTest, ]; export interface TestRunSpec { |