commit 8676f8716f6f9e5e944f062f72feb487d90e6430
parent 1ce3866c9b6d1055d682b4a92d8cef902c0e95e9
Author: Florian Dold <florian@dold.me>
Date: Wed, 11 Mar 2026 14:12:25 +0100
harness: test for wire metadata
Diffstat:
5 files changed, 458 insertions(+), 12 deletions(-)
diff --git a/packages/taler-harness/src/harness/environments.ts b/packages/taler-harness/src/harness/environments.ts
@@ -28,6 +28,7 @@ import {
AccountProperties,
AmlDecisionRequest,
AmlDecisionRequestWithoutSignature,
+ Amounts,
AmountString,
assertUnreachable,
Configuration,
@@ -914,6 +915,8 @@ export interface WithdrawViaBankResult {
/**
* Withdraw via a bank with the testing API enabled.
* Uses the new Corebank API.
+ *
+ * @deprecated use withdrawViaBankV4
*/
export async function withdrawViaBankV3(
t: GlobalTestState,
@@ -985,6 +988,105 @@ export async function withdrawViaBankV3(
};
}
+/**
+ * Withdraw via a bank that supports the corebank testing API.
+ */
+export async function withdrawViaBankV4(
+ t: GlobalTestState,
+ p: {
+ walletClient: WalletClient;
+ exchange: ExchangeServiceInterface;
+ bank: BankService;
+ amount: AmountString | string;
+ restrictAge?: number;
+ bankAdminTok: AccessToken;
+ },
+): Promise<WithdrawViaBankResult> {
+ const { walletClient: wallet, exchange, amount, bank, bankAdminTok } = p;
+
+ const bankClient = new TalerCoreBankHttpClient(bank.corebankApiBaseUrl);
+
+ const username = "user-" + encodeCrock(getRandomBytes(10)).toLowerCase();
+ const password = "pw-" + encodeCrock(getRandomBytes(10)).toLowerCase();
+
+ const user = succeedOrThrow(
+ await bankClient.createAccount(bankAdminTok, {
+ name: username,
+ password: password,
+ username: username,
+ }),
+ );
+
+ const token = succeedOrThrow(
+ await bankClient.createAccessToken(
+ username,
+ {
+ type: "basic",
+ password,
+ },
+ {
+ scope: "readwrite",
+ },
+ ),
+ ).access_token;
+
+ const userAuth = {
+ username,
+ token,
+ };
+
+ const wop = succeedOrThrow(
+ await bankClient.createWithdrawal(userAuth, {
+ amount: Amounts.stringify(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,
+ );
+
+ await wallet.call(WalletApiOperation.TestingWaitTransactionState, {
+ transactionId: acceptRes.transactionId,
+ txState: {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.BankConfirmTransfer,
+ },
+ });
+
+ // Confirm it
+
+ succeedOrThrow(
+ await bankClient.confirmWithdrawalById(userAuth, {}, wop.withdrawal_id),
+ );
+
+ return {
+ accountPaytoUri: user.internal_payto_uri,
+ withdrawalFinishedCond,
+ transactionId: acceptRes.transactionId,
+ };
+}
+
export async function applyTimeTravelV2(
timetravelOffsetMs: number,
s: {
@@ -1024,17 +1126,16 @@ export async function makeTestPaymentV2(
merchantAdminAccessToken: AccessToken;
walletClient: WalletClient;
order: TalerMerchantApi.Order;
- instance?: string;
refundDelay?: Duration;
},
auth: WithAuthorization = {},
): Promise<{ transactionId: TransactionIdStr; orderId: string }> {
// Set up order.
- const { walletClient, merchant, instance, merchantAdminAccessToken } = args;
+ const { walletClient, merchant, merchantAdminAccessToken } = args;
const merchantClient = new TalerMerchantInstanceHttpClient(
- merchant.makeInstanceBaseUrl(instance),
+ merchant.makeInstanceBaseUrl(),
);
const orderResp = succeedOrThrow(
diff --git a/packages/taler-harness/src/harness/harness.ts b/packages/taler-harness/src/harness/harness.ts
@@ -31,6 +31,7 @@ import {
ConfigSources,
Configuration,
CoreApiResponse,
+ Credentials,
Duration,
EddsaKeyPair,
InstanceAuthConfigurationMessage,
@@ -934,6 +935,11 @@ export class LibeufinNexusService {
}
}
+export const harnessBankAdminCreds: Credentials = {
+ type: "basic",
+ password: "admin-password",
+};
+
/**
* Implementation of the bank service using the libeufin-bank implementation.
*/
diff --git a/packages/taler-harness/src/integrationtests/test-wire-metadata.ts b/packages/taler-harness/src/integrationtests/test-wire-metadata.ts
@@ -0,0 +1,314 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 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,
+ Duration,
+ j2s,
+ LoginTokenScope,
+ MerchantAuthMethod,
+ PreparePayResultType,
+ succeedOrThrow,
+ TalerCoreBankHttpClient,
+ TalerMerchantInstanceHttpClient,
+ TalerMerchantManagementHttpClient,
+ TalerProtocolTimestamp,
+ UserAndToken,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { defaultCoinConfig } from "../harness/denomStructures.js";
+import {
+ applyTimeTravelV2,
+ createWalletDaemonWithClient,
+ withdrawViaBankV4,
+} from "../harness/environments.js";
+import {
+ BankService,
+ ExchangeService,
+ getTestHarnessPaytoForLabel,
+ GlobalTestState,
+ harnessBankAdminCreds,
+ HarnessExchangeBankAccount,
+ LibeufinBankService,
+ MerchantService,
+ setupDb,
+ waitMs,
+} from "../harness/harness.js";
+
+export async function runWireMetadataTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const db = await setupDb(t);
+
+ const exchange = ExchangeService.create(t, {
+ name: "testexchange-1",
+ currency: "TESTKUDOS",
+ httpPort: 8081,
+ database: db.connStr,
+ });
+
+ const bc = {
+ allowRegistrations: true,
+ currency: "TESTKUDOS",
+ database: db.connStr,
+ httpPort: 8082,
+ };
+
+ const bank: BankService = await LibeufinBankService.create(t, bc);
+
+ 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,
+ wireGatewayAuth: {
+ username: exchangeBankUsername,
+ password: exchangeBankPassword,
+ },
+ accountPaytoUri: exchangePaytoUri,
+ };
+
+ await exchange.addBankAccount("1", exchangeBankAccount);
+
+ exchange.addOfferedCoins(defaultCoinConfig);
+
+ bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri);
+
+ await bank.start();
+
+ const bankClient = new TalerCoreBankHttpClient(bank.corebankApiBaseUrl);
+ const bankAdminTok = succeedOrThrow(
+ await bankClient.createAccessToken("admin", harnessBankAdminCreds, {
+ scope: "readwrite",
+ }),
+ ).access_token;
+
+ await bankClient.createAccount(bankAdminTok, {
+ name: receiverName,
+ password: exchangeBankPassword,
+ username: exchangeBankUsername,
+ is_taler_exchange: true,
+ payto_uri: exchangePaytoUri,
+ });
+
+ await exchange.start();
+
+ const merchant = await MerchantService.create(t, {
+ name: "testmerchant-1",
+ httpPort: 8083,
+ database: db.connStr,
+ });
+ const merchantMgmtClient = new TalerMerchantManagementHttpClient(
+ merchant.makeInstanceBaseUrl(),
+ );
+
+ merchant.addExchange(exchange);
+
+ await merchant.start();
+
+ succeedOrThrow(
+ await merchantMgmtClient.createInstance(undefined, {
+ id: "admin",
+ address: {},
+ auth: {
+ method: MerchantAuthMethod.TOKEN,
+ password: "123",
+ },
+ jurisdiction: {},
+ name: "Admin",
+ use_stefan: true,
+ }),
+ );
+
+ const merchantAdminClient = new TalerMerchantInstanceHttpClient(
+ merchant.makeInstanceBaseUrl(),
+ );
+
+ const merchantAdminTok = succeedOrThrow(
+ await merchantAdminClient.createAccessToken("admin", "123", {
+ scope: LoginTokenScope.All,
+ }),
+ ).access_token;
+
+ console.log(`merchantAdminTok: ${merchantAdminTok}`);
+
+ succeedOrThrow(
+ await merchantMgmtClient.createInstance(merchantAdminTok, {
+ id: "minst1",
+ address: {},
+ auth: {
+ method: MerchantAuthMethod.TOKEN,
+ password: "123",
+ },
+ jurisdiction: {},
+ name: "My Instance One",
+ use_stefan: true,
+ }),
+ );
+
+ const merchantMinst1Client = new TalerMerchantInstanceHttpClient(
+ merchant.makeInstanceBaseUrl("minst1"),
+ );
+
+ const merchantMinst1Tok = succeedOrThrow(
+ await merchantMinst1Client.createAccessToken("minst1", "123", {
+ scope: LoginTokenScope.All,
+ }),
+ ).access_token;
+
+ succeedOrThrow(
+ await bankClient.createAccount(bankAdminTok, {
+ name: "minst1",
+ password: "minst1-password",
+ username: "minst1",
+ payto_uri: getTestHarnessPaytoForLabel("minst1"),
+ }),
+ );
+
+ succeedOrThrow(
+ await merchantMinst1Client.addBankAccount(merchantMinst1Tok, {
+ payto_uri: getTestHarnessPaytoForLabel("minst1"),
+ extra_wire_subject_metadata: "foo",
+ }),
+ );
+
+ const { walletClient, walletService } = await createWalletDaemonWithClient(
+ t,
+ {
+ name: "wallet",
+ persistent: true,
+ },
+ );
+
+ const wres = await withdrawViaBankV4(t, {
+ walletClient,
+ bankAdminTok,
+ amount: "TESTKUDOS:20",
+ bank,
+ exchange,
+ });
+ await wres.withdrawalFinishedCond;
+
+ {
+ const orderResp = succeedOrThrow(
+ await merchantMinst1Client.createOrder(merchantMinst1Tok, {
+ order: {
+ amount: "TESTKUDOS:5",
+ summary: "Hello",
+ wire_transfer_deadline: TalerProtocolTimestamp.now(),
+ },
+ }),
+ );
+
+ let orderStatus = succeedOrThrow(
+ await merchantMinst1Client.getOrderDetails(
+ merchantMinst1Tok,
+ orderResp.order_id,
+ ),
+ );
+
+ t.assertTrue(orderStatus.order_status === "unpaid");
+
+ 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.assertDeepEqual(r2.type, ConfirmPayResultType.Done);
+
+ // Check if payment was successful.
+
+ orderStatus = succeedOrThrow(
+ await merchantMinst1Client.getOrderDetails(
+ merchantMinst1Tok,
+ orderResp.order_id,
+ ),
+ );
+
+ t.assertDeepEqual(orderStatus.order_status, "paid");
+ }
+
+ await applyTimeTravelV2(
+ Duration.toMilliseconds(
+ Duration.fromSpec({
+ days: 14,
+ }),
+ ),
+ {
+ merchant,
+ exchange,
+ walletClient,
+ },
+ );
+
+ {
+ const client = new TalerCoreBankHttpClient(bank.corebankApiBaseUrl);
+ const btok = succeedOrThrow(
+ await client.createAccessToken(
+ "exchange",
+ {
+ type: "basic",
+ password: "mypw-password",
+ },
+ {
+ scope: "readwrite",
+ },
+ ),
+ ).access_token;
+ const bankAuth: UserAndToken = {
+ token: btok,
+ username: "exchange",
+ };
+
+ while (1) {
+ const txns = succeedOrThrow(await client.getTransactions(bankAuth));
+ console.log(`bank txns for exchange: ${j2s(txns)}`);
+ if (txns.transactions.length >= 2) {
+ const myTx = txns.transactions.find((x) => x.direction === "debit");
+ if (!myTx) {
+ throw Error("unexpected transaction");
+ }
+ t.assertTrue(myTx.subject.includes("foo"));
+ break;
+ }
+ console.log("waiting for transaction...");
+ await waitMs(1000);
+ await exchange.runAggregatorOnce();
+ await exchange.runTransferOnce();
+ }
+ }
+}
+
+runWireMetadataTest.suites = ["merchant"];
diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts
@@ -117,6 +117,7 @@ import { runMerchantLongpollingTest } from "./test-merchant-longpolling.js";
import { runMerchantRefundApiTest } from "./test-merchant-refund-api.js";
import { runMerchantReportsTest } from "./test-merchant-reports.js";
import { runMerchantSelfProvisionActivationAndLoginTest } from "./test-merchant-self-provision-activation-and-login.js";
+import { runMerchantSelfProvisionActivationTwoBankAccountsTest } from "./test-merchant-self-provision-activation-two-bank-account.js";
import { runMerchantSelfProvisionActivationTest } from "./test-merchant-self-provision-activation.js";
import { runMerchantSelfProvisionForgotPasswordTest } from "./test-merchant-self-provision-forgot-password.js";
import { runMerchantSelfProvisionInactiveAccountPermissionsTest } from "./test-merchant-self-provision-inactive-account-permissions.js";
@@ -208,6 +209,7 @@ import { runWalletWirefeesTest } from "./test-wallet-wirefees.js";
import { runWalletWithdrawalRedenominateTest } from "./test-wallet-withdrawal-redenominate.js";
import { runWallettestingTest } from "./test-wallettesting.js";
import { runWebMerchantLoginTest } from "./test-web-merchant-login.js";
+import { runWireMetadataTest } from "./test-wire-metadata.js";
import { runWithdrawalAbortBankTest } from "./test-withdrawal-abort-bank.js";
import { runWithdrawalAmountTest } from "./test-withdrawal-amount.js";
import { runWithdrawalBankIntegratedTest } from "./test-withdrawal-bank-integrated.js";
@@ -223,7 +225,6 @@ import { runWithdrawalHugeTest } from "./test-withdrawal-huge.js";
import { runWithdrawalIdempotentTest } from "./test-withdrawal-idempotent.js";
import { runWithdrawalManualTest } from "./test-withdrawal-manual.js";
import { runWithdrawalPrepareTest } from "./test-withdrawal-prepare.js";
-import { runMerchantSelfProvisionActivationTwoBankAccountsTest } from "./test-merchant-self-provision-activation-two-bank-account.js";
/**
* Test runner.
@@ -433,6 +434,7 @@ const allTests: TestMainFunction[] = [
runTopsMerchantTosTest,
runWalletWithdrawalRedenominateTest,
runMerchantDepositLargeTest,
+ runWireMetadataTest,
];
export interface TestRunSpec {
diff --git a/packages/taler-util/src/http-client/bank-core.ts b/packages/taler-util/src/http-client/bank-core.ts
@@ -17,23 +17,21 @@
import {
AbsoluteTime,
AccessToken,
+ ChallengeResponse,
HttpStatusCode,
LibtoolVersion,
LongPollParams,
+ OperationAlternative,
OperationFail,
OperationOk,
PaginationParams,
- TalerError,
TalerErrorCode,
TokenRequest,
UserAndToken,
assertUnreachable,
carefullyParseConfig,
- codecForTalerCommonConfigResponse,
codecForTokenInfoList,
codecForTokenSuccessResponse,
- codecOptional,
- codecOptionalDefault,
opKnownAlternativeHttpFailure,
opKnownHttpFailure,
opKnownTalerFailure,
@@ -41,7 +39,6 @@ import {
import {
HttpRequestLibrary,
createPlatformHttpLib,
- readSuccessResponseJsonOrThrow,
readTalerErrorResponse,
} from "@gnu-taler/taler-util/http";
import {
@@ -63,7 +60,7 @@ import {
CreateTransactionRequest,
MonitorTimeframeParam,
RegisterAccountRequest,
- TalerCorebankConfigResponse,
+ RegisterAccountResponse,
codecForAccountData,
codecForBankAccountCreateWithdrawalResponse,
codecForBankAccountTransactionInfo,
@@ -314,7 +311,23 @@ export class TalerCoreBankHttpClient {
async createAccount(
auth: AccessToken | undefined,
body: RegisterAccountRequest,
- ) {
+ ): Promise<
+ | OperationOk<RegisterAccountResponse>
+ | OperationFail<HttpStatusCode.Unauthorized>
+ | OperationFail<HttpStatusCode.BadRequest>
+ | OperationFail<TalerErrorCode.BANK_REGISTER_USERNAME_REUSE>
+ | OperationFail<TalerErrorCode.BANK_REGISTER_PAYTO_URI_REUSE>
+ | OperationFail<TalerErrorCode.BANK_UNALLOWED_DEBIT>
+ | OperationFail<TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT>
+ | OperationFail<TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT>
+ | OperationFail<TalerErrorCode.BANK_NON_ADMIN_SET_CONVERSION_RATE_CLASS>
+ | OperationFail<TalerErrorCode.BANK_NON_ADMIN_SET_TAN_CHANNEL>
+ | OperationFail<TalerErrorCode.BANK_TAN_CHANNEL_NOT_SUPPORTED>
+ | OperationFail<TalerErrorCode.BANK_MISSING_TAN_INFO>
+ | OperationFail<TalerErrorCode.BANK_PASSWORD_TOO_SHORT>
+ | OperationFail<TalerErrorCode.BANK_PASSWORD_TOO_LONG>
+ | OperationFail<TalerErrorCode.BANK_CONVERSION_RATE_CLASS_UNKNOWN>
+ > {
const url = new URL(`accounts`, this.baseUrl);
const headers: Record<string, string> = {};
if (auth) {
@@ -810,7 +823,17 @@ export class TalerCoreBankHttpClient {
body: BankAccountConfirmWithdrawalRequest,
wid: string,
params: { challengeIds?: string[] } = {},
- ) {
+ ): Promise<
+ | OperationFail<HttpStatusCode.NotFound>
+ | OperationAlternative<HttpStatusCode.Accepted, ChallengeResponse>
+ | OperationOk<void>
+ | OperationFail<HttpStatusCode.BadRequest>
+ | OperationFail<TalerErrorCode.BANK_UNALLOWED_DEBIT>
+ | OperationFail<TalerErrorCode.BANK_CONFIRM_ABORT_CONFLICT>
+ | OperationFail<TalerErrorCode.BANK_CONFIRM_INCOMPLETE>
+ | OperationFail<TalerErrorCode.BANK_AMOUNT_DIFFERS>
+ | OperationFail<TalerErrorCode.BANK_AMOUNT_REQUIRED>
+ > {
const url = new URL(
`accounts/${auth.username}/withdrawals/${wid}/confirm`,
this.baseUrl,