taler-typescript-core

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

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:
Mpackages/taler-harness/src/harness/harness.ts | 1+
Dpackages/taler-harness/src/integrationtests/test-kyc-merchant-deposit-form.ts | 255-------------------------------------------------------------------------------
Apackages/taler-harness/src/integrationtests/test-kyc-wallet-deposit-abort.ts | 146+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-harness/src/integrationtests/testrunner.ts | 4++--
Mpackages/taler-wallet-core/src/deposits.ts | 33++++++++++++++++++---------------
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}`);