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:
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;