diff options
Diffstat (limited to 'packages/taler-harness/src/harness/environments.ts')
-rw-r--r-- | packages/taler-harness/src/harness/environments.ts | 1268 |
1 files changed, 1268 insertions, 0 deletions
diff --git a/packages/taler-harness/src/harness/environments.ts b/packages/taler-harness/src/harness/environments.ts new file mode 100644 index 000000000..d52474427 --- /dev/null +++ b/packages/taler-harness/src/harness/environments.ts @@ -0,0 +1,1268 @@ +/* + 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/> + */ + +/** + * Helpers to create typical test environments. + * + * @author Florian Dold <dold@taler.net> + */ + +/** + * Imports + */ +import { + AccountProperties, + AmlDecisionRequest, + AmlDecisionRequestWithoutSignature, + AmountString, + Configuration, + ConfirmPayResultType, + decodeCrock, + Duration, + encodeCrock, + HttpStatusCode, + LegitimizationRuleSet, + Logger, + MerchantApiClient, + NotificationType, + PartialWalletRunConfig, + PreparePayResultType, + signAmlDecision, + TalerCorebankApiClient, + TalerMerchantApi, + TalerProtocolTimestamp, + TransactionMajorState, + WalletNotification, +} from "@gnu-taler/taler-util"; +import { + createSyncCryptoApi, + EddsaKeyPairStrings, + WalletApiOperation, +} from "@gnu-taler/taler-wallet-core"; +import { CoinConfig, defaultCoinConfig } from "./denomStructures.js"; +import { + FaultInjectedExchangeService, + FaultInjectedMerchantService, +} from "./faultInjection.js"; +import { + BankService, + DbInfo, + ExchangeService, + ExchangeServiceInterface, + FakebankService, + getTestHarnessPaytoForLabel, + GlobalTestState, + HarnessAccountRestriction, + HarnessExchangeBankAccount, + harnessHttpLib, + LibeufinBankService, + MerchantService, + MerchantServiceInterface, + setupDb, + setupSharedDb, + useLibeufinBank, + WalletCli, + WalletClient, + WalletService, + WithAuthorization, +} from "./harness.js"; + +import * as fs from "fs"; + +const logger = new Logger("helpers.ts"); + +/** + * @deprecated + */ +export interface SimpleTestEnvironment { + commonDb: DbInfo; + bank: BankService; + exchange: ExchangeService; + exchangeBankAccount: HarnessExchangeBankAccount; + merchant: MerchantService; + wallet: WalletCli; +} + +/** + * Improved version of the simple test environment, + * with the daemonized wallet. + */ +export interface SimpleTestEnvironmentNg { + commonDb: DbInfo; + bank: FakebankService; + exchange: ExchangeService; + exchangeBankAccount: HarnessExchangeBankAccount; + merchant: MerchantService; + walletClient: WalletClient; + walletService: WalletService; +} + +/** + * Improved version of the simple test environment, + * passing bankClient instead of bank service. + */ +export interface SimpleTestEnvironmentNg3 { + commonDb: DbInfo; + bank: BankService; + bankClient: TalerCorebankApiClient; + exchange: ExchangeService; + exchangeBankAccount: HarnessExchangeBankAccount; + merchant: MerchantService; + walletClient: WalletClient; + walletService: WalletService; +} + +export interface EnvOptions { + /** + * If provided, enable age restrictions with the specified age mask string. + */ + ageMaskSpec?: string; + + mixedAgeRestriction?: boolean; + + skipWireFeeCreation?: boolean; + + walletTestObservability?: boolean; + + accountRestrictions?: HarnessAccountRestriction[]; + + additionalExchangeConfig?(e: ExchangeService): void; + additionalMerchantConfig?(m: MerchantService): void; + additionalBankConfig?(b: BankService): void; +} + +export function getSharedTestDir(): string { + return `/tmp/taler-harness@${process.env.USER}`; +} + +export async function useSharedTestkudosEnvironment(t: GlobalTestState) { + const coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")); + + const sharedDir = getSharedTestDir(); + + fs.mkdirSync(sharedDir, { recursive: true }); + + const db = await setupSharedDb(t); + + let bank: FakebankService; + + const prevSetupDone = fs.existsSync(sharedDir + "/setup-done"); + + logger.info(`previous setup done: ${prevSetupDone}`); + + // Wallet has longer startup-time and no dependencies, + // so we start it rather early. + const walletStartProm = createWalletDaemonWithClient(t, { name: "wallet" }); + + if (fs.existsSync(sharedDir + "/bank.conf")) { + logger.info("reusing existing bank"); + bank = FakebankService.fromExistingConfig(t, { + overridePath: sharedDir, + }); + } else { + logger.info("creating new bank config"); + bank = await FakebankService.create(t, { + allowRegistrations: true, + currency: "TESTKUDOS", + database: db.connStr, + httpPort: 8082, + overrideTestDir: sharedDir, + }); + } + + logger.info("setting up exchange"); + + const exchangeName = "testexchange-1"; + const exchangeConfigFilename = sharedDir + `/exchange-${exchangeName}.conf`; + + logger.info(`exchange config filename: ${exchangeConfigFilename}`); + + let exchange: ExchangeService; + + if (fs.existsSync(exchangeConfigFilename)) { + logger.info("reusing existing exchange config"); + exchange = ExchangeService.fromExistingConfig(t, exchangeName, { + overridePath: sharedDir, + }); + } else { + logger.info("creating new exchange config"); + exchange = ExchangeService.create(t, { + name: "testexchange-1", + currency: "TESTKUDOS", + httpPort: 8081, + database: db.connStr, + overrideTestDir: sharedDir, + }); + } + + logger.info("setting up merchant"); + + let merchant: MerchantService; + const merchantName = "testmerchant-1"; + const merchantConfigFilename = sharedDir + `/merchant-${merchantName}}`; + + if (fs.existsSync(merchantConfigFilename)) { + merchant = MerchantService.fromExistingConfig(t, merchantName, { + overridePath: sharedDir, + }); + } else { + merchant = await MerchantService.create(t, { + name: "testmerchant-1", + currency: "TESTKUDOS", + httpPort: 8083, + database: db.connStr, + overrideTestDir: sharedDir, + }); + } + + logger.info("creating bank account for exchange"); + + const exchangeBankAccount = await bank.createExchangeAccount( + "myexchange", + "password", + ); + + logger.info("creating exchange bank account"); + await exchange.addBankAccount("1", exchangeBankAccount); + + bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri); + + exchange.addCoinConfigList(coinConfig); + + merchant.addExchange(exchange); + + logger.info("basic setup done, starting services"); + + if (!prevSetupDone) { + // Must be done sequentially, due to a concurrency + // issue in the *-dbinit tools. + await exchange.dbinit(); + await merchant.dbinit(); + } + + const bankStart = async () => { + await bank.start(); + await bank.pingUntilAvailable(); + }; + + const exchangeStart = async () => { + await exchange.start({ + skipDbinit: true, + skipKeyup: prevSetupDone, + }); + await exchange.pingUntilAvailable(); + }; + + const merchStart = async () => { + await merchant.start({ + skipDbinit: true, + }); + await merchant.pingUntilAvailable(); + + if (!prevSetupDone) { + 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 }), + ), + }); + } + }; + + await bankStart(); + + const res = await Promise.all([ + exchangeStart(), + merchStart(), + undefined, + walletStartProm, + ]); + + const walletClient = res[3].walletClient; + const walletService = res[3].walletService; + + fs.writeFileSync(sharedDir + "/setup-done", "OK"); + + logger.info("setup done!"); + + return { + commonDb: db, + exchange, + merchant, + walletClient, + walletService, + bank, + exchangeBankAccount, + }; +} + +/** + * Run a test case with a simple TESTKUDOS Taler environment, consisting + * of one exchange, one bank and one merchant. + * + * V2 uses a daemonized wallet instead of the CLI wallet. + */ +export async function createSimpleTestkudosEnvironmentV2( + t: GlobalTestState, + coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")), + opts: EnvOptions = {}, +): Promise<SimpleTestEnvironmentNg> { + const db = await setupDb(t); + + const bank = await FakebankService.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, + }); + + const merchant = await MerchantService.create(t, { + name: "testmerchant-1", + currency: "TESTKUDOS", + httpPort: 8083, + database: db.connStr, + }); + + const exchangeBankAccount = await bank.createExchangeAccount( + "myexchange", + "password", + ); + await exchange.addBankAccount("1", exchangeBankAccount); + + bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri); + + if (opts.additionalBankConfig) { + opts.additionalBankConfig(bank); + } + await bank.start(); + + await bank.pingUntilAvailable(); + + 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); + } + + if (opts.additionalExchangeConfig) { + opts.additionalExchangeConfig(exchange); + } + await exchange.start(); + await exchange.pingUntilAvailable(); + + 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 }), + ), + }); + + const { walletClient, walletService } = await createWalletDaemonWithClient( + t, + { name: "wallet", persistent: true }, + ); + + console.log("setup done!"); + + return { + commonDb: db, + exchange, + merchant, + walletClient, + walletService, + bank, + exchangeBankAccount, + }; +} + +/** + * Run a test case with a simple TESTKUDOS Taler environment, consisting + * of one exchange, one bank and one merchant. + * + * V3 uses the unified Corebank API and allows to choose between + * Fakebank and Libeufin-bank. + */ +export async function createSimpleTestkudosEnvironmentV3( + t: GlobalTestState, + coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")), + opts: EnvOptions = {}, +): Promise<SimpleTestEnvironmentNg3> { + const db = await setupDb(t); + + const bc = { + allowRegistrations: true, + currency: "TESTKUDOS", + database: db.connStr, + httpPort: 8082, + }; + + const bank: BankService = useLibeufinBank + ? await LibeufinBankService.create(t, bc) + : await FakebankService.create(t, bc); + + const exchange = ExchangeService.create(t, { + name: "testexchange-1", + currency: "TESTKUDOS", + httpPort: 8081, + database: db.connStr, + }); + + const merchant = await MerchantService.create(t, { + name: "testmerchant-1", + currency: "TESTKUDOS", + httpPort: 8083, + database: db.connStr, + }); + + const receiverName = "Exchange"; + const exchangeBankUsername = "exchange"; + const exchangeBankPassword = "mypw-password"; + 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: opts.skipWireFeeCreation === true, + accountRestrictions: opts.accountRestrictions, + }; + + await exchange.addBankAccount("1", exchangeBankAccount); + + bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri); + + if (opts.additionalBankConfig) { + opts.additionalBankConfig(bank); + } + await bank.start(); + + await bank.pingUntilAvailable(); + + const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl, { + auth: { + username: "admin", + password: "admin-password", + }, + }); + + 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); + } + + if (opts.additionalExchangeConfig) { + opts.additionalExchangeConfig(exchange); + } + await exchange.start(); + + 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 }), + ), + }); + + const { walletClient, walletService } = await createWalletDaemonWithClient( + t, + { + name: "wallet", + persistent: true, + emitObservabilityEvents: !!opts.walletTestObservability, + }, + ); + + console.log("setup done!"); + + return { + commonDb: db, + exchange, + merchant, + walletClient, + walletService, + bank, + bankClient, + exchangeBankAccount, + }; +} + +export interface CreateWalletArgs { + handleNotification?(wn: WalletNotification): void; + name: string; + persistent?: boolean; + overrideDbPath?: string; + config?: PartialWalletRunConfig; + emitObservabilityEvents?: boolean; +} + +export async function createWalletDaemonWithClient( + t: GlobalTestState, + args: CreateWalletArgs, +): Promise<{ walletClient: WalletClient; walletService: WalletService }> { + const walletService = new WalletService(t, { + name: args.name, + useInMemoryDb: !args.persistent, + overrideDbPath: args.overrideDbPath, + }); + await walletService.start(); + await walletService.pingUntilAvailable(); + + const observabilityEventFile = t.testDir + `/wallet-${args.name}-notifs.log`; + + const onNotif = (notif: WalletNotification) => { + if (observabilityEventFile) { + fs.appendFileSync( + observabilityEventFile, + new Date().toISOString() + " " + JSON.stringify(notif) + "\n", + ); + } + if (args.handleNotification) { + args.handleNotification(notif); + } + }; + + const walletClient = new WalletClient({ + name: args.name, + unixPath: walletService.socketPath, + onNotification: onNotif, + }); + await walletClient.connect(); + const defaultRunConfig = { + testing: { + skipDefaults: true, + emitObservabilityEvents: + !!process.env["TALER_TEST_OBSERVABILITY"] || + !!args.emitObservabilityEvents, + }, + } satisfies PartialWalletRunConfig; + await walletClient.client.call(WalletApiOperation.InitWallet, { + config: args.config ?? defaultRunConfig, + }); + + return { walletClient, walletService }; +} + +export interface FaultyMerchantTestEnvironment { + commonDb: DbInfo; + bank: FakebankService; + exchange: ExchangeService; + faultyExchange: FaultInjectedExchangeService; + exchangeBankAccount: HarnessExchangeBankAccount; + merchant: MerchantService; + faultyMerchant: FaultInjectedMerchantService; + walletClient: WalletClient; +} + +export interface FaultyMerchantTestEnvironmentNg { + commonDb: DbInfo; + bankClient: TalerCorebankApiClient; + exchange: ExchangeService; + faultyExchange: FaultInjectedExchangeService; + merchant: MerchantService; + faultyMerchant: FaultInjectedMerchantService; + walletClient: WalletClient; +} + +/** + * Run a test case with a simple TESTKUDOS Taler environment, consisting + * of one exchange, one bank and one merchant. + */ +export async function createFaultInjectedMerchantTestkudosEnvironment( + t: GlobalTestState, +): Promise<FaultyMerchantTestEnvironment> { + const db = await setupDb(t); + + const bank = await FakebankService.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, + }); + + const merchant = await MerchantService.create(t, { + name: "testmerchant-1", + currency: "TESTKUDOS", + httpPort: 8083, + database: db.connStr, + }); + + const faultyMerchant = new FaultInjectedMerchantService(t, merchant, 9083); + const faultyExchange = new FaultInjectedExchangeService(t, exchange, 9081); + + // Base URL must contain port that the proxy is listening on. + await exchange.modifyConfig(async (config) => { + config.setString("exchange", "base_url", "http://localhost:9081/"); + }); + + const exchangeBankAccount = await bank.createExchangeAccount( + "myexchange", + "password", + ); + exchange.addBankAccount("1", exchangeBankAccount); + + bank.setSuggestedExchange( + faultyExchange, + exchangeBankAccount.accountPaytoUri, + ); + + await bank.start(); + + await bank.pingUntilAvailable(); + + exchange.addOfferedCoins(defaultCoinConfig); + + await exchange.start(); + await exchange.pingUntilAvailable(); + + merchant.addExchange(faultyExchange); + + await merchant.start(); + await merchant.pingUntilAvailable(); + + await merchant.addInstanceWithWireAccount({ + id: "default", + name: "Default Instance", + paytoUris: [getTestHarnessPaytoForLabel("merchant-default")], + }); + + await merchant.addInstanceWithWireAccount({ + id: "minst1", + name: "minst1", + paytoUris: [getTestHarnessPaytoForLabel("minst1")], + }); + + console.log("setup done!"); + + const { walletClient } = await createWalletDaemonWithClient(t, { + name: "default", + }); + + return { + commonDb: db, + exchange, + merchant, + walletClient, + bank, + exchangeBankAccount, + faultyMerchant, + faultyExchange, + }; +} + +export interface WithdrawViaBankResult { + /** + * Payto URI of the account that the withdrawal + * originated from. Typically a new account used for testing. + */ + accountPaytoUri: string; + + /** + * Helper promise that resolves when withdrawal has finished successfully. + */ + withdrawalFinishedCond: Promise<true>; + + /** + * The wallet-core withdrawal transaction ID. + */ + transactionId: string; +} + +/** + * Withdraw via a bank with the testing API enabled. + * Uses the new notification-based mechanism to wait for the + * operation to finish. + */ +export async function withdrawViaBankV2( + t: GlobalTestState, + p: { + walletClient: WalletClient; + bank: BankService; + exchange: ExchangeServiceInterface; + amount: AmountString | string; + restrictAge?: number; + }, +): Promise<WithdrawViaBankResult> { + const { walletClient: wallet, bank, exchange, amount } = p; + + const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl); + + const user = await bankClient.createRandomBankUser(); + const wop = await bankClient.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 bankClient.confirmWithdrawalOperation(user.username, { + withdrawalOperationId: wop.withdrawal_id, + }); + + return { + accountPaytoUri: user.accountPaytoUri, + withdrawalFinishedCond, + transactionId: acceptRes.transactionId, + }; +} + +/** + * Withdraw via a bank with the testing API enabled. + * Uses the new Corebank API. + */ +export async function withdrawViaBankV3( + t: GlobalTestState, + p: { + walletClient: WalletClient; + bankClient: TalerCorebankApiClient; + exchange: ExchangeServiceInterface; + amount: AmountString | string; + restrictAge?: number; + }, +): Promise<WithdrawViaBankResult> { + const { walletClient: wallet, bankClient, exchange, amount } = p; + + const user = await bankClient.createRandomBankUser(); + const bankClient2 = new TalerCorebankApiClient(bankClient.baseUrl); + bankClient2.setAuth({ + username: user.username, + password: user.password, + }); + + const wop = await bankClient2.createWithdrawalOperation( + user.username, + amount, + ); + + // Hand it to the wallet + + await wallet.client.call(WalletApiOperation.GetWithdrawalDetailsForUri, { + talerWithdrawUri: wop.taler_withdraw_uri, + restrictAge: p.restrictAge, + }); + + // Withdraw (AKA select) + + const acceptRes = await wallet.client.call( + WalletApiOperation.AcceptBankIntegratedWithdrawal, + { + exchangeBaseUrl: exchange.baseUrl, + talerWithdrawUri: wop.taler_withdraw_uri, + restrictAge: p.restrictAge, + }, + ); + + const withdrawalFinishedCond = wallet.waitForNotificationCond( + (x) => + x.type === NotificationType.TransactionStateTransition && + x.newTxState.major === TransactionMajorState.Done && + x.transactionId === acceptRes.transactionId, + ); + + // Confirm it + + await bankClient2.confirmWithdrawalOperation(user.username, { + withdrawalOperationId: wop.withdrawal_id, + }); + + return { + accountPaytoUri: user.accountPaytoUri, + withdrawalFinishedCond, + transactionId: acceptRes.transactionId, + }; +} + +export async function applyTimeTravelV2( + timetravelOffsetMs: number, + s: { + exchange?: ExchangeService; + merchant?: MerchantService; + walletClient?: WalletClient; + }, +): Promise<void> { + if (s.exchange) { + await s.exchange.stop(); + s.exchange.setTimetravel(timetravelOffsetMs); + await s.exchange.start(); + await s.exchange.pingUntilAvailable(); + } + + if (s.merchant) { + await s.merchant.stop(); + s.merchant.setTimetravel(timetravelOffsetMs); + await s.merchant.start(); + await s.merchant.pingUntilAvailable(); + } + + if (s.walletClient) { + await s.walletClient.call(WalletApiOperation.TestingSetTimetravel, { + offsetMs: timetravelOffsetMs, + }); + } +} + +/** + * Make a simple payment and check that it succeeded. + */ +export async function makeTestPaymentV2( + t: GlobalTestState, + args: { + merchant: MerchantServiceInterface; + walletClient: WalletClient; + order: TalerMerchantApi.Order; + instance?: string; + }, + auth: WithAuthorization = {}, +): Promise<void> { + // Set up order. + + const { walletClient, merchant, instance } = args; + + const merchantClient = new MerchantApiClient( + merchant.makeInstanceBaseUrl(instance), + ); + + const orderResp = await merchantClient.createOrder({ + order: args.order, + }); + + let orderStatus = await merchantClient.queryPrivateOrderStatus({ + orderId: orderResp.order_id, + }); + + t.assertTrue(orderStatus.order_status === "unpaid"); + + // Make wallet pay for the order + + const preparePayResult = await walletClient.call( + WalletApiOperation.PreparePayForUri, + { + talerPayUri: orderStatus.taler_pay_uri, + }, + ); + + t.assertTrue( + preparePayResult.status === PreparePayResultType.PaymentPossible, + ); + + const r2 = await walletClient.call(WalletApiOperation.ConfirmPay, { + transactionId: preparePayResult.transactionId, + }); + + t.assertTrue(r2.type === ConfirmPayResultType.Done); + + // Check if payment was successful. + + orderStatus = await merchantClient.queryPrivateOrderStatus({ + orderId: orderResp.order_id, + instance, + }); + + t.assertTrue(orderStatus.order_status === "paid"); +} + +/** + * Post an AML decision that no rules shall apply for the given account. + */ +export async function postAmlDecisionNoRules( + t: GlobalTestState, + req: { + exchangeBaseUrl: string; + paytoHash: string; + amlPriv: string; + amlPub: string; + }, +) { + const { exchangeBaseUrl, paytoHash, amlPriv, amlPub } = req; + + const sigData: AmlDecisionRequestWithoutSignature = { + decision_time: TalerProtocolTimestamp.now(), + h_payto: paytoHash, + justification: "Bla", + keep_investigating: false, + new_rules: { + custom_measures: {}, + expiration_time: TalerProtocolTimestamp.never(), + rules: [], + successor_measure: undefined, + }, + properties: { + foo: "42", + }, + }; + + const sig = signAmlDecision(decodeCrock(amlPriv), sigData); + + const reqBody: AmlDecisionRequest = { + ...sigData, + officer_sig: encodeCrock(sig), + }; + + const reqUrl = new URL(`aml/${amlPub}/decision`, exchangeBaseUrl); + + const resp = await harnessHttpLib.fetch(reqUrl.href, { + method: "POST", + body: reqBody, + }); + + console.log(`aml decision status: ${resp.status}`); + + t.assertDeepEqual(resp.status, HttpStatusCode.NoContent); +} + +/** + * Post an AML decision that no rules shall apply for the given account. + */ +export async function postAmlDecision( + t: GlobalTestState, + req: { + exchangeBaseUrl: string; + paytoHash: string; + amlPriv: string; + amlPub: string; + newRules: LegitimizationRuleSet; + newMeasure?: string | undefined; + properties?: AccountProperties; + }, +) { + const { exchangeBaseUrl, paytoHash, amlPriv, amlPub } = req; + + const sigData: AmlDecisionRequestWithoutSignature = { + decision_time: TalerProtocolTimestamp.now(), + h_payto: paytoHash, + justification: "Bla", + keep_investigating: false, + new_rules: req.newRules, + new_measures: req.newMeasure, + properties: req.properties ?? {}, + }; + + const sig = signAmlDecision(decodeCrock(amlPriv), sigData); + + const reqBody: AmlDecisionRequest = { + ...sigData, + officer_sig: encodeCrock(sig), + }; + + const reqUrl = new URL(`aml/${amlPub}/decision`, exchangeBaseUrl); + + const resp = await harnessHttpLib.fetch(reqUrl.href, { + method: "POST", + body: reqBody, + }); + + console.log(`aml decision status: ${resp.status}`); + + t.assertDeepEqual(resp.status, HttpStatusCode.NoContent); +} + +export interface KycEnvOptions { + coinConfig?: CoinConfig[]; + + adjustExchangeConfig?(config: Configuration): void; +} + +export interface KycTestEnv { + commonDb: DbInfo; + bankClient: TalerCorebankApiClient; + exchange: ExchangeService; + exchangeBankAccount: HarnessExchangeBankAccount; + walletClient: WalletClient; + walletService: WalletService; + amlKeypair: EddsaKeyPairStrings; + merchant: MerchantService; +} + +export async function createKycTestkudosEnvironment( + t: GlobalTestState, + opts: KycEnvOptions = {}, +): Promise<KycTestEnv> { + const db = await setupDb(t); + + let coinConfig: CoinConfig[]; + if (opts.coinConfig) { + coinConfig = opts.coinConfig; + } else { + coinConfig = defaultCoinConfig.map((x) => x("TESTKUDOS")); + } + + 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-password"; + 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: "admin-password", + }, + }); + + await bankClient.registerAccountExtended({ + name: receiverName, + password: exchangeBankPassword, + username: exchangeBankUsername, + is_taler_exchange: true, + payto_uri: exchangePaytoUri, + }); + + exchange.addCoinConfigList(coinConfig); + + const adjustExchangeConfig = opts.adjustExchangeConfig; + if (adjustExchangeConfig) { + await exchange.modifyConfig(async (config) => { + adjustExchangeConfig(config); + }); + } + + 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("wallet-core 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); + + await merchant.start(); + + 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 }), + ), + }); + + t.logStep("env-setup-done"); + + return { + commonDb: db, + exchange, + amlKeypair, + walletClient, + walletService, + bankClient, + exchangeBankAccount: { + accountName: exchangeBankUsername, + accountPassword: exchangeBankPassword, + accountPaytoUri: exchangePaytoUri, + wireGatewayApiBaseUrl, + }, + merchant, + }; +} |