taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit 5c33ef95ba30ec44c5a9b3c044bb2b4f3c6e1eb6
parent e9668264a6a6a0fb0522de46ecbd89178054db02
Author: Florian Dold <florian@dold.me>
Date:   Mon,  8 Sep 2025 15:38:30 +0200

wallet-core,harness: conversion test, no conversion for p2p withdrawal groups

Diffstat:
Mpackages/taler-harness/src/harness/harness.ts | 150++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
Apackages/taler-harness/src/integrationtests/test-libeufin-conversion.ts | 236+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-harness/src/integrationtests/testrunner.ts | 2++
Mpackages/taler-wallet-core/src/pay-peer-pull-credit.ts | 2++
Mpackages/taler-wallet-core/src/pay-peer-push-credit.ts | 1+
Mpackages/taler-wallet-core/src/withdraw.ts | 12++++++++++++
6 files changed, 368 insertions(+), 35 deletions(-)

diff --git a/packages/taler-harness/src/harness/harness.ts b/packages/taler-harness/src/harness/harness.ts @@ -693,7 +693,7 @@ class BankServiceBase { protected globalTestState: GlobalTestState, protected bankConfig: BankConfig, protected configFile: string, - ) { } + ) {} getAdminAuth(): { username: string; password: string } { // Bank admin PW is brutally hard-coded in tests right now. @@ -727,7 +727,8 @@ export interface HarnessExchangeBankAccount { */ export class FakebankService extends BankServiceBase - implements BankServiceHandle { + implements BankServiceHandle +{ proc: ProcessWrapper | undefined; http = createPlatformHttpLib({ enableThrottling: false }); @@ -889,12 +890,56 @@ export class FakebankService } } +export interface NexusConfig { + currency: string; + /** DB connection string */ + database: string; +} + +export class LibeufinNexusService { + /** + * Create a new fakebank service handle. + * + * First generates the configuration for the fakebank and + * then creates a fakebank handle, but doesn't start the fakebank + * service yet. + */ + static async create( + gc: GlobalTestState, + bc: NexusConfig, + ): Promise<LibeufinNexusService> { + const config = new Configuration(); + const testDir = gc.testDir; + setTalerPaths(config, testDir + "/talerhome"); + config.setString("libeufin-nexus", "currency", bc.currency); + config.setString("libeufin-nexusdb-postgres", "config", bc.database); + const cfgFilename = testDir + "/nexus.conf"; + config.writeTo(cfgFilename, { excludeDefaults: true }); + return new LibeufinNexusService(gc, bc, cfgFilename); + } + + constructor( + private gc: GlobalTestState, + private bc: NexusConfig, + private configFile: string, + ) {} + + async dbinit(): Promise<void> { + await sh( + this.gc, + "libeufin-nexus-dbinit", + `libeufin-nexus dbinit -c "${this.configFile}"`, + ); + } +} + /** * Implementation of the bank service using the libeufin-bank implementation. */ export class LibeufinBankService extends BankServiceBase - implements BankServiceHandle { + implements BankServiceHandle +{ proc: ProcessWrapper | undefined; http = createPlatformHttpLib({ enableThrottling: false }); @@ -1014,18 +1059,32 @@ export class LibeufinBankService return this.bankConfig.httpPort; } - async start(): Promise<void> { + async start( + opts: { + noReset?: boolean; + } = {}, + ): Promise<void> { logger.info("starting libeufin-bank"); if (this.proc) { logger.info("libeufin-bank already running, not starting again"); return; } - await sh( - this.globalTestState, - "libeufin-bank-dbinit", - `libeufin-bank dbinit -r -c "${this.configFile}"`, - ); + if (opts.noReset) { + await sh( + this.globalTestState, + "libeufin-bank-dbinit", + `libeufin-bank dbinit -c "${this.configFile}"`, + ); + } else { + // By default, reset database, since that's + // what fakebank does (fakebank is only in-memory). + await sh( + this.globalTestState, + "libeufin-bank-dbinit", + `libeufin-bank dbinit -r -c "${this.configFile}"`, + ); + } await sh( this.globalTestState, @@ -1461,7 +1520,7 @@ export class ExchangeService implements ExchangeServiceInterface { private exchangeConfig: ExchangeConfig, private configFilename: string, private keyPair: EddsaKeyPair, - ) { } + ) {} get name() { return this.exchangeConfig.name; @@ -1894,14 +1953,14 @@ export interface PartialMerchantInstanceConfig { */ export const MERCHANT_DEFAULT_AUTH: InstanceAuthConfigurationMessage = { method: MerchantAuthMethod.TOKEN, - password: "123" -} + password: "123", +}; export const MERCHANT_DEFAULT_LOGIN_SCOPE: LoginTokenRequest = { scope: LoginTokenScope.All_Refreshable, description: "testing", - duration: { d_us: "forever" } -} + duration: { d_us: "forever" }, +}; export class MerchantService implements MerchantServiceInterface { static fromExistingConfig( @@ -1931,7 +1990,7 @@ export class MerchantService implements MerchantServiceInterface { private globalState: GlobalTestState, private merchantConfig: MerchantConfig, private configFilename: string, - ) { } + ) {} private currentTimetravelOffsetMs: number | undefined; @@ -2169,7 +2228,7 @@ export class MerchantService implements MerchantServiceInterface { */ async addInstanceWithWireAccount( instanceConfig: PartialMerchantInstanceConfig, - { adminAccessToken }: { adminAccessToken?: AccessToken } = {} + { adminAccessToken }: { adminAccessToken?: AccessToken } = {}, ): Promise<{ accessToken: AccessToken }> { if (!this.procHttpd) { throw Error("merchant must be running to add instance"); @@ -2197,20 +2256,37 @@ export class MerchantService implements MerchantServiceInterface { instanceConfig.defaultPayDelay ?? Duration.toTalerProtocolDuration(Duration.getForever()), }; - const headers: Record<string, string> = {} + const headers: Record<string, string> = {}; if (adminAccessToken) { - headers["Authorization"] = `Bearer ${adminAccessToken}` - console.log("ASDASDSAD,", adminAccessToken) + headers["Authorization"] = `Bearer ${adminAccessToken}`; + console.log("ASDASDSAD,", adminAccessToken); } - console.log("CREATING", body, headers) - const resp = await harnessHttpLib.fetch(url, { method: "POST", body, headers }); + console.log("CREATING", body, headers); + const resp = await harnessHttpLib.fetch(url, { + method: "POST", + body, + headers, + }); await expectSuccessResponseOrThrow(resp); - this.configFilename - const merchantApi = new TalerMerchantInstanceHttpClient(this.makeInstanceBaseUrl(instanceConfig.id)); + this.configFilename; + const merchantApi = new TalerMerchantInstanceHttpClient( + this.makeInstanceBaseUrl(instanceConfig.id), + ); - const { access_token } = succeedOrThrow(await merchantApi.createAccessToken(instanceConfig.id, auth.password, MERCHANT_DEFAULT_LOGIN_SCOPE)) - console.log("CREATED", instanceConfig.id, auth.password, MERCHANT_DEFAULT_LOGIN_SCOPE) + const { access_token } = succeedOrThrow( + await merchantApi.createAccessToken( + instanceConfig.id, + auth.password, + MERCHANT_DEFAULT_LOGIN_SCOPE, + ), + ); + console.log( + "CREATED", + instanceConfig.id, + auth.password, + MERCHANT_DEFAULT_LOGIN_SCOPE, + ); const accountCreateUrl = `http://localhost:${this.merchantConfig.httpPort}/instances/${instanceConfig.id}/private/accounts`; for (const paytoUri of instanceConfig.paytoUris) { @@ -2221,12 +2297,12 @@ export class MerchantService implements MerchantServiceInterface { method: "POST", body: accountReq, headers: { - Authorization: `Bearer ${access_token}` - } + Authorization: `Bearer ${access_token}`, + }, }); await expectSuccessResponseOrThrow(acctResp); } - return { accessToken: access_token } + return { accessToken: access_token }; } makeInstanceBaseUrl(instanceName?: string): string { @@ -2510,7 +2586,7 @@ export class WalletClient { return client.call(operation, payload); } - constructor(private args: WalletClientArgs) { } + constructor(private args: WalletClientArgs) {} async connect(): Promise<void> { const waiter = this.waiter; @@ -2592,9 +2668,11 @@ export class WalletCli { ? `--crypto-worker=${cliOpts.cryptoWorkerType}` : ""; const logName = `wallet-${self.name}`; - const command = `taler-wallet-cli ${self.timetravelArg ?? "" - } ${cryptoWorkerArg} --no-throttle -LTRACE --skip-defaults --wallet-db '${self.dbfile - }' api '${op}' ${shellWrap(JSON.stringify(payload))}`; + const command = `taler-wallet-cli ${ + self.timetravelArg ?? "" + } ${cryptoWorkerArg} --no-throttle -LTRACE --skip-defaults --wallet-db '${ + self.dbfile + }' api '${op}' ${shellWrap(JSON.stringify(payload))}`; const resp = await sh(self.globalTestState, logName, command); logger.info("--- wallet core response ---"); logger.info(resp); @@ -2724,8 +2802,8 @@ export async function doMerchantKycAuth( let accountPub: string; const headers = { - Authorization: `Bearer ${merchantAdminAccessToken}` - } + Authorization: `Bearer ${merchantAdminAccessToken}`, + }; { const instanceUrl = new URL("private", merchant.makeInstanceBaseUrl()); const resp = await harnessHttpLib.fetch(instanceUrl.href, { headers }); @@ -2793,7 +2871,9 @@ export async function doMerchantKycAuth( merchant.makeInstanceBaseUrl(), ); kycStatusLongpollUrl.searchParams.set("lpt", "1"); - const resp = await harnessHttpLib.fetch(kycStatusLongpollUrl.href, { headers }); + const resp = await harnessHttpLib.fetch(kycStatusLongpollUrl.href, { + headers, + }); t.assertDeepEqual(resp.status, 200); const parsedResp = await readSuccessResponseJsonOrThrow( resp, diff --git a/packages/taler-harness/src/integrationtests/test-libeufin-conversion.ts b/packages/taler-harness/src/integrationtests/test-libeufin-conversion.ts @@ -0,0 +1,236 @@ +/* + 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, + TalerBankConversionHttpClient, + TalerCoreBankHttpClient, + TalerCorebankApiClient, + TalerErrorCode, + j2s, + succeedOrThrow, +} from "@gnu-taler/taler-util"; +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { defaultCoinConfig } from "../harness/denomStructures.js"; +import { createWalletDaemonWithClient } from "../harness/environments.js"; +import { + ExchangeService, + GlobalTestState, + LibeufinBankService, + LibeufinNexusService, + MerchantService, + getTestHarnessPaytoForLabel, + setupDb, +} from "../harness/harness.js"; + +const logger = new Logger("test-libeufin-bank.ts"); + +/** + * Run test for conversion functionality of libeufin-bank. + */ +export async function runLibeufinConversionTest(t: GlobalTestState) { + // Set up test environment + + const db = await setupDb(t); + + const bank = await LibeufinBankService.create(t, { + currency: "TESTKUDOS", + httpPort: 8082, + database: db.connStr, + allowRegistrations: true, + }); + + const exchange = ExchangeService.create(t, { + name: "testexchange-1", + currency: "TESTKUDOS", + httpPort: 8081, + database: db.connStr, + }); + + const merchant = await MerchantService.create(t, { + name: "testmerchant-1", + httpPort: 8083, + database: db.connStr, + }); + + const nexus = await LibeufinNexusService.create(t, { + currency: "FOO", + database: db.connStr, + }); + + await nexus.dbinit(); + + const exchangeBankUsername = "exchange"; + const exchangeBankPw = "mypw-password"; + const exchangePayto = getTestHarnessPaytoForLabel(exchangeBankUsername); + const wireGatewayApiBaseUrl = new URL( + "accounts/exchange/taler-wire-gateway/", + bank.baseUrl, + ).href; + + logger.info("creating bank account for the exchange"); + + exchange.addBankAccount("1", { + wireGatewayApiBaseUrl, + wireGatewayAuth: { + username: exchangeBankUsername, + password: exchangeBankPw, + }, + accountPaytoUri: exchangePayto, + conversionUrl: new URL("conversion-info/", bank.baseUrl).href, + }); + + exchange.addBankAccount("conv", { + wireGatewayApiBaseUrl, + wireGatewayAuth: { + username: exchangeBankUsername, + password: exchangeBankPw, + }, + accountPaytoUri: exchangePayto, + conversionUrl: new URL("conversion-info/", bank.baseUrl).href, + }); + + bank.setSuggestedExchange(exchange); + + await bank.start(); + + await bank.pingUntilAvailable(); + + exchange.addOfferedCoins(defaultCoinConfig); + + await exchange.start(); + await exchange.pingUntilAvailable(); + + merchant.addExchange(exchange); + + await merchant.start(); + await merchant.pingUntilAvailable(); + + const { accessToken: adminAccessToken } = + await merchant.addInstanceWithWireAccount({ + id: "admin", + name: "Default Instance", + paytoUris: [getTestHarnessPaytoForLabel("merchant-default")], + }); + + await merchant.addInstanceWithWireAccount( + { + id: "minst1", + name: "minst1", + paytoUris: [getTestHarnessPaytoForLabel("minst1")], + }, + { adminAccessToken }, + ); + + const { walletClient } = await createWalletDaemonWithClient(t, { + name: "wallet", + }); + + console.log("setup done!"); + + const adminUser = "admin"; + const adminPassword = "admin-password"; + + const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl, { + auth: { + username: adminUser, + password: adminPassword, + }, + }); + + // register exchange bank account + await bankClient.registerAccountExtended({ + name: "exchange", + password: exchangeBankPw, + username: exchangeBankUsername, + is_taler_exchange: true, + payto_uri: exchangePayto, + }); + + const bankUser = await bankClient.registerAccount("user1", "password1"); + bankClient.setAuth({ + username: "user1", + password: "password1", + }); + + const cbc = new TalerCoreBankHttpClient(bank.baseUrl); + const adminTokResp = succeedOrThrow( + await cbc.createAccessToken( + adminUser, + { + type: "basic", + password: adminPassword, + }, + { + scope: "readwrite", + }, + ), + ); + + await bank.stop(); + + // libeufin needs the exchange account to be created *before* conversion + // is activated. + bank.changeConfig((conf) => { + conf.setString("libeufin-bank", "allow_conversion", "yes"); + conf.setString("libeufin-bank", "fiat_currency", "FOO"); + }); + + await bank.start({ + noReset: true, + }); + + const adminTok = adminTokResp.access_token; + + const cc = new TalerBankConversionHttpClient( + bank.baseUrl + `conversion-info/`, + ); + + succeedOrThrow( + await cc.updateConversionRate(adminTok, { + cashin_fee: "TESTKUDOS:0", + cashin_min_amount: "FOO:5", + cashin_ratio: "1", + cashin_rounding_mode: "nearest", + cashin_tiny_amount: "TESTKUDOS:0.01", + cashout_fee: "FOO:0", + cashout_min_amount: "TESTKUDOS:0", + cashout_ratio: "1", + cashout_rounding_mode: "nearest", + cashout_tiny_amount: "FOO:0.01", + }), + ); + + const detRes = await walletClient.call( + WalletApiOperation.GetWithdrawalDetailsForAmount, + { + amount: "TESTKUDOS:1", + exchangeBaseUrl: exchange.baseUrl, + }, + ); + + console.log(j2s(detRes)); + + t.assertDeepEqual( + detRes.withdrawalAccountsList[0].conversionError?.code, + TalerErrorCode.BANK_CONVERSION_AMOUNT_TO_SMALL, + ); +} + +runLibeufinConversionTest.suites = ["fakebank"]; diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts @@ -93,6 +93,7 @@ import { runKycTwoFormsTest } from "./test-kyc-two-forms.js"; import { runKycWalletDepositAbortTest } from "./test-kyc-wallet-deposit-abort.js"; import { runKycWithdrawalVerbotenTest } from "./test-kyc-withdrawal-verboten.js"; import { runLibeufinBankTest } from "./test-libeufin-bank.js"; +import { runLibeufinConversionTest } from "./test-libeufin-conversion.js"; import { runMerchantAcctselTest } from "./test-merchant-acctsel.js"; import { runMerchantCategoriesTest } from "./test-merchant-categories.js"; import { runMerchantExchangeConfusionTest } from "./test-merchant-exchange-confusion.js"; @@ -373,6 +374,7 @@ const allTests: TestMainFunction[] = [ runDepositLargeTest, runDepositTooLargeTest, runMerchantAcctselTest, + runLibeufinConversionTest, ]; export interface TestRunSpec { diff --git a/packages/taler-wallet-core/src/pay-peer-pull-credit.ts b/packages/taler-wallet-core/src/pay-peer-pull-credit.ts @@ -1001,6 +1001,7 @@ export async function internalCheckPeerPullCredit( wex, exchangeUrl, Amounts.parseOrThrow(req.amount), + WithdrawalRecordType.PeerPullCredit, undefined, ); @@ -1080,6 +1081,7 @@ export async function initiatePeerPullPayment( wex, exchangeBaseUrl, Amounts.parseOrThrow(req.partialContractTerms.amount), + WithdrawalRecordType.PeerPullCredit, undefined, ); if (wi.selectedDenoms.selectedDenoms.length === 0) { diff --git a/packages/taler-wallet-core/src/pay-peer-push-credit.ts b/packages/taler-wallet-core/src/pay-peer-push-credit.ts @@ -552,6 +552,7 @@ export async function preparePeerPushCredit( wex, exchangeBaseUrl, Amounts.parseOrThrow(purseStatus.balance), + WithdrawalRecordType.PeerPushCredit, undefined, ); diff --git a/packages/taler-wallet-core/src/withdraw.ts b/packages/taler-wallet-core/src/withdraw.ts @@ -2821,6 +2821,7 @@ export async function getExchangeWithdrawalInfo( wex: WalletExecutionContext, exchangeBaseUrl: string, instructedAmount: AmountJson, + withdrawalType: WithdrawalRecordType, ageRestricted: number | undefined, ): Promise<ExchangeWithdrawalDetails> { logger.trace("updating exchange"); @@ -2839,6 +2840,7 @@ export async function getExchangeWithdrawalInfo( const withdrawalAccountsList = await fetchWithdrawalAccountInfo(wex, { exchange, instructedAmount, + withdrawalType, }); logger.trace("updating withdrawal denoms"); @@ -3903,6 +3905,7 @@ export async function confirmWithdrawal( let withdrawalAccountList: WithdrawalExchangeAccountDetails[] = []; if (instructedAmount) { withdrawalAccountList = await fetchWithdrawalAccountInfo(wex, { + withdrawalType: withdrawalGroup.wgInfo.withdrawalType, exchange, instructedAmount, }); @@ -4243,8 +4246,15 @@ async function fetchWithdrawalAccountInfo( exchange: ReadyExchangeSummary; instructedAmount: AmountJson; reservePub?: string; + withdrawalType: WithdrawalRecordType; }, ): Promise<WithdrawalExchangeAccountDetails[]> { + switch (req.withdrawalType) { + case WithdrawalRecordType.PeerPullCredit: + case WithdrawalRecordType.PeerPushCredit: + case WithdrawalRecordType.Recoup: + return []; + } const { exchange } = req; const withdrawalAccounts: WithdrawalExchangeAccountDetails[] = []; for (let acct of exchange.wireInfo.accounts) { @@ -4316,6 +4326,7 @@ export async function createManualWithdrawal( exchange, instructedAmount: amount, reservePub: reserveKeyPair.pub, + withdrawalType: WithdrawalRecordType.BankManual, }); const withdrawalGroup = await internalCreateWithdrawalGroup(wex, { @@ -4431,6 +4442,7 @@ export async function internalGetWithdrawalDetailsForAmount( wex, exchangeBaseUrl, Amounts.parseOrThrow(req.amount), + WithdrawalRecordType.BankManual, req.restrictAge, ); let numCoins = 0;