/* 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 */ /** * Imports. */ import { AbsoluteTime, AmountString, Duration, Logger, TalerBankConversionApi, TalerCorebankApiClient, TransactionType, WireGatewayApiClient, WithdrawalType, j2s, } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import * as http from "node:http"; import { defaultCoinConfig } from "../harness/denomStructures.js"; import { BankService, ExchangeService, GlobalTestState, HarnessExchangeBankAccount, MerchantService, getTestHarnessPaytoForLabel, setupDb, } from "../harness/harness.js"; import { createWalletDaemonWithClient } from "../harness/environments.js"; const logger = new Logger("test-withdrawal-conversion.ts"); interface TestfakeConversionService { stop: () => void; } function splitInTwoAt(s: string, separator: string): [string, string] { const idx = s.indexOf(separator); if (idx === -1) { return [s, ""]; } return [s.slice(0, idx), s.slice(idx + 1)]; } /** * Testfake for the kyc service that the exchange talks to. */ async function runTestfakeConversionService(): Promise { const server = http.createServer((req, res) => { const requestUrl = req.url!; logger.info(`kyc: got ${req.method} request, ${requestUrl}`); const [path, query] = splitInTwoAt(requestUrl, "?"); const qp = new URLSearchParams(query); if (path === "/config") { res.writeHead(200, { "Content-Type": "application/json" }); res.end( JSON.stringify({ version: "0:0:0", name: "taler-conversion-info", regional_currency: "FOO", fiat_currency: "BAR", regional_currency_specification: { alt_unit_names: {}, name: "FOO", num_fractional_input_digits: 2, num_fractional_normal_digits: 2, num_fractional_trailing_zero_digits: 2, }, fiat_currency_specification: { alt_unit_names: {}, name: "BAR", num_fractional_input_digits: 2, num_fractional_normal_digits: 2, num_fractional_trailing_zero_digits: 2, }, conversion_rate: { cashin_fee: "A:1" as AmountString, cashin_min_amount: "A:0.1" as AmountString, cashin_ratio: "1", cashin_rounding_mode: "zero", cashin_tiny_amount: "A:1" as AmountString, cashout_fee: "A:1" as AmountString, cashout_min_amount: "A:0.1" as AmountString, cashout_ratio: "1", cashout_rounding_mode: "zero", cashout_tiny_amount: "A:1" as AmountString, }, } satisfies TalerBankConversionApi.TalerConversionInfoConfig), ); } else if (path === "/cashin-rate") { res.writeHead(200, { "Content-Type": "application/json" }); res.end( JSON.stringify({ amount_debit: "FOO:123", amount_credit: "BAR:123", }), ); } else { res.writeHead(400, { "Content-Type": "application/json" }); res.end(JSON.stringify({ code: 1, message: "bad request" })); } }); await new Promise((resolve, reject) => { server.listen(8071, () => resolve()); }); return { stop() { server.close(); }, }; } /** * Test for currency conversion during manual withdrawal. */ export async function runWithdrawalConversionTest(t: GlobalTestState) { // Set up test environment 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, }); const merchant = await MerchantService.create(t, { name: "testmerchant-1", currency: "TESTKUDOS", httpPort: 8083, database: db.connStr, }); let exchangeBankAccount: HarnessExchangeBankAccount = { wireGatewayApiBaseUrl: new URL( "accounts/myexchange/taler-wire-gateway/", bank.corebankApiBaseUrl, ).href, accountName: "myexchange", accountPassword: "password", accountPaytoUri: getTestHarnessPaytoForLabel("myexchange"), conversionUrl: "http://localhost:8071/", }; await exchange.addBankAccount("1", exchangeBankAccount); await bank.start(); await bank.pingUntilAvailable(); const bankClientAuth = { username: "admin", password: "admin-password", }; const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl, { auth: bankClientAuth, }); await bankClient.registerAccountExtended({ name: exchangeBankAccount.accountName, username: exchangeBankAccount.accountName, password: exchangeBankAccount.accountPassword, is_taler_exchange: true, payto_uri: exchangeBankAccount.accountPaytoUri, }); exchange.addOfferedCoins(defaultCoinConfig); await exchange.start(); await exchange.pingUntilAvailable(); merchant.addExchange(exchange); 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 } = await createWalletDaemonWithClient(t, { name: "wallet", }); await runTestfakeConversionService(); // Create a withdrawal operation const user = await bankClient.createRandomBankUser(); await walletClient.call(WalletApiOperation.AddExchange, { exchangeBaseUrl: exchange.baseUrl, }); const infoRes = await walletClient.call( WalletApiOperation.GetWithdrawalDetailsForAmount, { exchangeBaseUrl: exchange.baseUrl, amount: "TESTKUDOS:20" as AmountString, }, ); console.log(`withdrawal details: ${j2s(infoRes)}`); const checkTransferAmount = infoRes.withdrawalAccountsList[0].transferAmount; t.assertTrue(checkTransferAmount != null); t.assertAmountEquals(checkTransferAmount, "FOO:123"); const tStart = AbsoluteTime.now(); logger.info("starting AcceptManualWithdrawal request"); // We expect this to return immediately. const wres = await walletClient.call( WalletApiOperation.AcceptManualWithdrawal, { exchangeBaseUrl: exchange.baseUrl, amount: "TESTKUDOS:10" as AmountString, }, ); logger.info("AcceptManualWithdrawal finished"); logger.info(`result: ${j2s(wres)}`); const acceptedTransferAmount = wres.withdrawalAccountsList[0].transferAmount; t.assertTrue(acceptedTransferAmount != null); t.assertAmountEquals(acceptedTransferAmount, "FOO:123"); const txInfo = await walletClient.call( WalletApiOperation.GetTransactionById, { transactionId: wres.transactionId, }, ); t.assertDeepEqual(txInfo.type, TransactionType.Withdrawal); t.assertDeepEqual( txInfo.withdrawalDetails.type, WithdrawalType.ManualTransfer, ); t.assertTrue(!!txInfo.withdrawalDetails.exchangeCreditAccountDetails); t.assertDeepEqual( txInfo.withdrawalDetails.exchangeCreditAccountDetails[0].transferAmount, "FOO:123", ); // Check that the request did not go into long-polling. const duration = AbsoluteTime.difference(tStart, AbsoluteTime.now()); if (typeof duration.d_ms !== "number" || duration.d_ms > 5 * 1000) { throw Error("withdrawal took too long (longpolling issue)"); } const reservePub: string = wres.reservePub; const wireGatewayApiClient = new WireGatewayApiClient( exchangeBankAccount.wireGatewayApiBaseUrl, { auth: bankClientAuth, }, ); await wireGatewayApiClient.adminAddIncoming({ amount: "TESTKUDOS:10", debitAccountPayto: user.accountPaytoUri, reservePub: reservePub, }); await exchange.runWirewatchOnce(); await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); // Check balance const balResp = await walletClient.call(WalletApiOperation.GetBalances, {}); t.assertAmountEquals("TESTKUDOS:9.72", balResp.balances[0].available); } runWithdrawalConversionTest.suites = ["wallet"];