commit b0a97e5fd6c41446b16b844911eb14b2bbcc4168
parent 37f9380ff0547523ea4b7588c2b58d14747b93ea
Author: Florian Dold <florian@dold.me>
Date: Mon, 26 May 2025 10:28:40 +0200
wallet-core: fix aborting deposit transactions
Diffstat:
5 files changed, 167 insertions(+), 272 deletions(-)
diff --git a/packages/taler-harness/src/harness/harness.ts b/packages/taler-harness/src/harness/harness.ts
@@ -2610,6 +2610,7 @@ export function generateRandomTestIban(salt: string | null = null): string {
}
export function getTestHarnessPaytoForLabel(label: string): string {
+ // FIXME: This should also support iban!
return `payto://x-taler-bank/localhost/${label}?receiver-name=${label}`;
}
diff --git a/packages/taler-harness/src/integrationtests/test-kyc-merchant-deposit-form.ts b/packages/taler-harness/src/integrationtests/test-kyc-merchant-deposit-form.ts
@@ -1,255 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2024 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 {
- codecForAccountKycRedirects,
- codecForKycProcessClientInformation,
- codecForQueryInstancesResponse,
- Configuration,
- j2s,
- Logger,
- MerchantAccountKycRedirectsResponse,
- MerchantAccountKycStatus,
- succeedOrThrow,
- TalerMerchantApi,
-} from "@gnu-taler/taler-util";
-import {
- readResponseJsonOrThrow,
- readSuccessResponseJsonOrThrow,
-} from "@gnu-taler/taler-util/http";
-import {
- configureCommonKyc,
- createKycTestkudosEnvironment,
-} from "../harness/environments.js";
-import {
- delayMs,
- GlobalTestState,
- harnessHttpLib,
-} from "../harness/harness.js";
-
-const logger = new Logger(`test-kyc-merchant-deposit.ts`);
-
-const myAmlConfig = `
-# Fallback measure on errors.
-[kyc-measure-freeze-investigate]
-CHECK_NAME = skip
-PROGRAM = freeze-investigate
-VOLUNTARY = NO
-CONTEXT = {}
-
-[aml-program-freeze-investigate]
-DESCRIPTION = "Fallback measure on errors that freezes the account and asks AML staff to investigate the system failure."
-COMMAND = taler-exchange-helper-measure-freeze
-ENABLED = YES
-FALLBACK = freeze-investigate
-
-[aml-program-inform-investigate]
-DESCRIPTION = "Measure that asks AML staff to investigate an account and informs the account owner about it."
-COMMAND = taler-exchange-helper-measure-inform-investigate
-ENABLED = YES
-FALLBACK = freeze-investigate
-
-[kyc-check-form-gls-merchant-onboarding]
-TYPE = FORM
-FORM_NAME = gls-merchant-onboarding
-DESCRIPTION = "GLS Merchant Onboarding"
-DESCRIPTION_I18N = {}
-OUTPUTS =
-FALLBACK = freeze-investigate
-
-[kyc-measure-merchant-onboarding]
-CHECK_NAME = form-gls-merchant-onboarding
-PROGRAM = inform-investigate
-CONTEXT = {}
-VOLUNTARY = NO
-
-[kyc-rule-deposit-limit-zero]
-OPERATION_TYPE = DEPOSIT
-NEXT_MEASURES = merchant-onboarding
-EXPOSED = YES
-ENABLED = YES
-THRESHOLD = TESTKUDOS:0
-TIMEFRAME = "1 days"
-`;
-
-function adjustExchangeConfig(config: Configuration) {
- configureCommonKyc(config);
- config.loadFromString(myAmlConfig);
-}
-
-export async function runKycMerchantDepositFormTest(t: GlobalTestState) {
- // Set up test environment
-
- const {
- merchant,
- bankClient,
- exchange,
- exchangeBankAccount,
- amlKeypair,
- wireGatewayApi,
- } = await createKycTestkudosEnvironment(t, {
- adjustExchangeConfig,
- });
-
- let accountPub: string;
-
- {
- const instanceUrl = new URL("private", merchant.makeInstanceBaseUrl());
- const resp = await harnessHttpLib.fetch(instanceUrl.href);
- const parsedResp = await readSuccessResponseJsonOrThrow(
- resp,
- codecForQueryInstancesResponse(),
- );
- accountPub = parsedResp.merchant_pub;
- }
-
- // Withdraw digital cash into the wallet.
-
- let kycRespOne: MerchantAccountKycRedirectsResponse | undefined = undefined;
-
- while (1) {
- const kycStatusUrl = new URL("private/kyc", merchant.makeInstanceBaseUrl())
- .href;
- logger.info(`requesting GET ${kycStatusUrl}`);
- const resp = await harnessHttpLib.fetch(kycStatusUrl);
- if (resp.status === 200) {
- kycRespOne = await readSuccessResponseJsonOrThrow(
- resp,
- codecForAccountKycRedirects(),
- );
- break;
- }
- // Wait 500ms
- await delayMs(500);
- }
-
- t.assertTrue(!!kycRespOne);
-
- logger.info(`mechant kyc status: ${j2s(kycRespOne)}`);
-
- t.assertDeepEqual(
- kycRespOne.kyc_data[0].status,
- MerchantAccountKycStatus.KYC_WIRE_REQUIRED,
- );
-
- t.assertDeepEqual(kycRespOne.kyc_data[0].exchange_http_status, 404);
-
- t.assertTrue(
- (kycRespOne.kyc_data[0].limits?.length ?? 0) > 0,
- "kyc status should contain non-empty limits",
- );
-
- // Order creation should fail!
- await t.runSpanAsync("order creation should be rejected", async () => {
- let url = new URL("private/orders", merchant.makeInstanceBaseUrl());
- const order = {
- summary: "Test",
- amount: "TESTKUDOS:5",
- fulfillment_url: "taler://fulfillment-success/thx",
- } satisfies TalerMerchantApi.Order;
- const resp = await harnessHttpLib.fetch(url.href, {
- method: "POST",
- body: {
- order,
- },
- });
-
- logger.info(`order creation status: ${resp.status}`);
- t.assertTrue(resp.status !== 200);
- });
-
- await bankClient.registerAccountExtended({
- name: "merchant-default",
- password: "merchant-default",
- username: "merchant-default",
- payto_uri: kycRespOne.kyc_data[0].payto_uri, //this bank user needs to have the same payto that the exchange is asking from
- });
- succeedOrThrow(
- await wireGatewayApi.addKycAuth({
- auth: exchangeBankAccount.wireGatewayAuth,
- body: {
- amount: "TESTKUDOS:0.1",
- account_pub: accountPub,
- debit_account: kycRespOne.kyc_data[0].payto_uri,
- },
- }),
- );
-
- let kycRespTwo: MerchantAccountKycRedirectsResponse | undefined = undefined;
-
- // We do this in a loop as a work-around.
- // Not exactly the correct behavior from the merchant right now.
- while (true) {
- const kycStatusLongpollUrl = new URL(
- "private/kyc",
- merchant.makeInstanceBaseUrl(),
- );
- kycStatusLongpollUrl.searchParams.set("lpt", "1");
- const resp = await harnessHttpLib.fetch(kycStatusLongpollUrl.href);
- t.assertDeepEqual(resp.status, 200);
- const parsedResp = await readSuccessResponseJsonOrThrow(
- resp,
- codecForAccountKycRedirects(),
- );
- logger.info(`kyc resp 2: ${j2s(parsedResp)}`);
- if (parsedResp.kyc_data[0].payto_kycauths == null) {
- kycRespTwo = parsedResp;
- break;
- }
- // Wait 500ms
- await delayMs(500);
- }
-
- // FIXME: Use exchange client!
-
- const infoResp = await harnessHttpLib.fetch(
- new URL(`kyc-info/${kycRespTwo.kyc_data[0].access_token}`, exchange.baseUrl)
- .href,
- );
-
- const clientInfo = await readResponseJsonOrThrow(
- infoResp,
- codecForKycProcessClientInformation(),
- );
-
- console.log(j2s(clientInfo));
-
- const kycId = clientInfo.requirements[0].id;
- t.assertTrue(typeof kycId === "string");
-
- const uploadResp = await harnessHttpLib.fetch(
- new URL(`kyc-upload/${kycId}`, exchange.baseUrl).href,
- {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: {
- full_name: "Alice Abc",
- birthdate: "2000-01-01",
- },
- },
- );
-
- console.log("resp status", uploadResp.status);
-
- t.assertTrue(uploadResp.status >= 200 && uploadResp.status < 300);
-}
-
-runKycMerchantDepositFormTest.suites = ["wallet", "merchant", "kyc"];
diff --git a/packages/taler-harness/src/integrationtests/test-kyc-wallet-deposit-abort.ts b/packages/taler-harness/src/integrationtests/test-kyc-wallet-deposit-abort.ts
@@ -0,0 +1,146 @@
+/*
+ This file is part of GNU Taler
+ (C) 2024 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 {
+ Configuration,
+ j2s,
+ Logger,
+ TransactionMajorState,
+ TransactionMinorState,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import {
+ configureCommonKyc,
+ createKycTestkudosEnvironment,
+ withdrawViaBankV3,
+} from "../harness/environments.js";
+import { GlobalTestState } from "../harness/harness.js";
+
+const logger = new Logger(`test-kyc-merchant-deposit.ts`);
+
+const myAmlConfig = `
+# Fallback measure on errors.
+[kyc-measure-freeze-investigate]
+CHECK_NAME = skip
+PROGRAM = freeze-investigate
+VOLUNTARY = NO
+CONTEXT = {}
+
+[aml-program-freeze-investigate]
+DESCRIPTION = "Fallback measure on errors that freezes the account and asks AML staff to investigate the system failure."
+COMMAND = taler-exchange-helper-measure-freeze
+ENABLED = YES
+FALLBACK = freeze-investigate
+
+[aml-program-inform-investigate]
+DESCRIPTION = "Measure that asks AML staff to investigate an account and informs the account owner about it."
+COMMAND = taler-exchange-helper-measure-inform-investigate
+ENABLED = YES
+FALLBACK = freeze-investigate
+
+[kyc-check-form-gls-merchant-onboarding]
+TYPE = FORM
+FORM_NAME = gls-merchant-onboarding
+DESCRIPTION = "GLS Merchant Onboarding"
+DESCRIPTION_I18N = {}
+OUTPUTS =
+FALLBACK = freeze-investigate
+
+[kyc-measure-merchant-onboarding]
+CHECK_NAME = form-gls-merchant-onboarding
+PROGRAM = inform-investigate
+CONTEXT = {}
+VOLUNTARY = NO
+
+[kyc-rule-deposit-limit-zero]
+OPERATION_TYPE = DEPOSIT
+NEXT_MEASURES = merchant-onboarding
+EXPOSED = YES
+ENABLED = YES
+THRESHOLD = TESTKUDOS:0
+TIMEFRAME = "1 days"
+`;
+
+function adjustExchangeConfig(config: Configuration) {
+ configureCommonKyc(config);
+ config.loadFromString(myAmlConfig);
+}
+
+export async function runKycWalletDepositAbortTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const {
+ merchant,
+ bankClient,
+ exchange,
+ exchangeBankAccount,
+ wireGatewayApi,
+ walletClient,
+ } = await createKycTestkudosEnvironment(t, {
+ adjustExchangeConfig,
+ });
+
+ const wres = await withdrawViaBankV3(t, {
+ walletClient,
+ bankClient,
+ exchange,
+ amount: "TESTKUDOS:20",
+ });
+ await wres.withdrawalFinishedCond;
+
+ const depositPaytoUri = wres.accountPaytoUri;
+
+ const depositResp = await walletClient.call(
+ WalletApiOperation.CreateDepositGroup,
+ {
+ amount: "TESTKUDOS:5",
+ depositPaytoUri,
+ },
+ );
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionState, {
+ transactionId: depositResp.transactionId,
+ txState: {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.KycRequired,
+ },
+ });
+
+ await walletClient.call(WalletApiOperation.AbortTransaction, {
+ transactionId: depositResp.transactionId,
+ });
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionState, {
+ transactionId: depositResp.transactionId,
+ txState: {
+ major: TransactionMajorState.Aborted,
+ minor: "*",
+ },
+ });
+
+ // Also wait for potential refreshes
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {});
+
+ const bal = await walletClient.call(WalletApiOperation.GetBalances, {});
+ console.log(j2s(bal));
+
+ t.assertDeepEqual(bal.balances[0].available, "TESTKUDOS:18.93");
+}
+
+runKycWalletDepositAbortTest.suites = ["wallet", "kyc"];
diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts
@@ -74,7 +74,6 @@ import { runKycFormBadMeasureTest } from "./test-kyc-form-bad-measure.js";
import { runKycFormWithdrawalTest } from "./test-kyc-form-withdrawal.js";
import { runKycMerchantActivateBankAccountTest } from "./test-kyc-merchant-activate-bank-account.js";
import { runKycMerchantAggregateTest } from "./test-kyc-merchant-aggregate.js";
-import { runKycMerchantDepositFormTest } from "./test-kyc-merchant-deposit-form.js";
import { runKycMerchantDepositRewriteTest } from "./test-kyc-merchant-deposit-rewrite.js";
import { runKycMerchantDepositTest } from "./test-kyc-merchant-deposit.js";
import { runKycNewMeasureTest } from "./test-kyc-new-measure.js";
@@ -84,6 +83,7 @@ import { runKycPeerPushTest } from "./test-kyc-peer-push.js";
import { runKycSkipExpirationTest } from "./test-kyc-skip-expiration.js";
import { runKycThresholdWithdrawalTest } from "./test-kyc-threshold-withdrawal.js";
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 { runMerchantCategoriesTest } from "./test-merchant-categories.js";
@@ -344,7 +344,7 @@ const allTests: TestMainFunction[] = [
runKycFormBadMeasureTest,
runKycBalanceWithdrawalChangeManualTest,
runUtilMerchantClientTest,
- runKycMerchantDepositFormTest,
+ runKycWalletDepositAbortTest,
];
export interface TestRunSpec {
diff --git a/packages/taler-wallet-core/src/deposits.ts b/packages/taler-wallet-core/src/deposits.ts
@@ -26,7 +26,6 @@ import {
AmountJson,
Amounts,
BatchDepositRequestCoin,
- CancellationToken,
CheckDepositRequest,
CheckDepositResponse,
CoinRefreshRequest,
@@ -42,8 +41,8 @@ import {
KycAuthTransferInfo,
Logger,
MerchantContractTermsV0,
- NotificationType,
MerchantContractVersion,
+ NotificationType,
RefreshReason,
ScopeInfo,
SelectedProspectiveCoin,
@@ -855,13 +854,16 @@ async function refundDepositGroup(
}
}
- let isDone = true;
+ // Check if we are done trying to refund.
+ let refundsAllDone = true;
for (let i = 0; i < newTxPerCoin.length; i++) {
- if (
- newTxPerCoin[i] != DepositElementStatus.RefundFailed &&
- newTxPerCoin[i] != DepositElementStatus.RefundSuccess
- ) {
- isDone = false;
+ switch (newTxPerCoin[i]) {
+ case DepositElementStatus.RefundFailed:
+ case DepositElementStatus.RefundNotFound:
+ case DepositElementStatus.RefundSuccess:
+ break;
+ default:
+ refundsAllDone = false;
}
}
@@ -893,7 +895,7 @@ async function refundDepositGroup(
});
}
let refreshRes: CreateRefreshGroupResult | undefined = undefined;
- if (isDone) {
+ if (refundsAllDone) {
refreshRes = await createRefreshGroup(
wex,
tx,
@@ -1104,7 +1106,7 @@ async function processDepositGroupPendingKycAuth(
// lpt=1 => wait for the KYC auth transfer (access token available)
url.searchParams.set("lpt", "1");
logger.info(`kyc url ${url.href}`);
- const kycStatusRes = await cancelableLongPoll(wex, url,{
+ const kycStatusRes = await cancelableLongPoll(wex, url, {
headers: {
["Account-Owner-Signature"]: sigResp.sig,
},
@@ -1494,7 +1496,7 @@ async function processDepositGroupTrack(
async function processDepositGroupPendingDeposit(
wex: WalletExecutionContext,
- depositGroup: DepositGroupRecord
+ depositGroup: DepositGroupRecord,
): Promise<TaskRunResult> {
logger.info("processing deposit group in pending(deposit)");
const depositGroupId = depositGroup.depositGroupId;
@@ -1516,7 +1518,7 @@ async function processDepositGroupPendingDeposit(
);
if (
contractData.version !== undefined &&
- contractData.version !== MerchantContractVersion.V0
+ contractData.version !== MerchantContractVersion.V0
) {
throw Error("assertion failed");
}
@@ -1550,7 +1552,7 @@ async function processDepositGroupPendingDeposit(
const payCoinSel = await selectPayCoinsInTx(wex, tx, {
restrictExchanges: {
auditors: [],
- exchanges: contractData.exchanges.map(ex => ({
+ exchanges: contractData.exchanges.map((ex) => ({
exchangeBaseUrl: ex.url,
exchangePub: ex.master_pub,
})),
@@ -1672,7 +1674,7 @@ async function processDepositGroupPendingDeposit(
logger.trace(`deposit request: ${j2s(batchReq)}`);
const httpResp = await cancelableFetch(wex, url, {
method: "POST",
- body: batchReq
+ body: batchReq,
});
logger.info(`deposit result status ${httpResp.status}`);
@@ -2159,7 +2161,8 @@ export async function createDepositGroup(
"",
);
- if (contractData.version !== undefined &&
+ if (
+ contractData.version !== undefined &&
contractData.version !== MerchantContractVersion.V0
) {
throw Error(`unsupported contract version ${contractData.version}`);