commit e1e8d150e3eb6d832f65810cc3fb189f943b8466
parent 5c0b8673e85c3136a994f1645b28a825e63783d0
Author: Florian Dold <dold@taler.net>
Date: Tue, 3 Nov 2026 09:35:40 +0100
wallet-core: handle denomination errors on KYC-blocked planchets
Diffstat:
3 files changed, 79 insertions(+), 2 deletions(-)
diff --git a/packages/taler-harness/src/integrationtests/test-kyc-withdrawal-recovery.ts b/packages/taler-harness/src/integrationtests/test-kyc-withdrawal-recovery.ts
@@ -29,6 +29,8 @@ import {
KycTestEnv,
configureSimpleKycInfoRule,
createKycTestkudosEnvironmentFull,
+ postAmlDecisionNoRules,
+ applyTimeTravelV2,
} from "../harness/environments.js";
async function withdraw(
env: KycTestEnv,
@@ -117,4 +119,71 @@ export async function runKycWithdrawalCumulativeHardTest(t: GlobalTestState) {
);
}
+export async function runKycWithdrawalExpiryTest(t: GlobalTestState) {
+ t.allowLogs({
+ file: "wallet-wallet-stderr.log",
+ level: "WARNING",
+ messages: [
+ [
+ /^marking denomination [0-9A-Z]+ of http:\/\/localhost:8081\/ as lost$/,
+ 7,
+ ],
+ [/^redenomination required for withdrawal [0-9A-Z]+$/, 1],
+ ],
+ justification:
+ "Time travel expires the seven original denomination families while KYC is pending.",
+ });
+ t.allowLog({
+ file: "exchange-httpd-testexchange-1-stderr.log",
+ level: "WARNING",
+ message:
+ /^External protocol violation detected at \.\.\/src\/exchange\/taler-exchange-httpd_post-withdraw\.c:\d+\.$/,
+ maxOccurrences: 1,
+ justification:
+ "The initial retry deliberately submits expired denominations before wallet redenomination.",
+ });
+ const env = await createKycTestkudosEnvironmentFull(t, {
+ walletPersistent: true,
+ adjustExchangeConfig(config) {
+ configureSimpleKycInfoRule(config, "withdraw");
+ },
+ });
+ const user = await env.bankClient.createRandomBankUser();
+ env.bankClient.setAuth({ username: user.username, password: user.password });
+ const id = await withdraw(env, user, "TESTKUDOS:20", {
+ major: Major.Pending,
+ minor: Minor.KycRequired,
+ });
+ const blocked = await env.walletClient.call(Op.GetTransactionById, {
+ transactionId: id,
+ });
+ await env.walletClient.call(Op.SuspendTransaction, { transactionId: id });
+ await postAmlDecisionNoRules(t, {
+ exchangeBaseUrl: env.exchange.baseUrl,
+ paytoHash: blocked.kycPaytoHash!,
+ amlPub: env.amlKeypair.pub,
+ amlPriv: env.amlKeypair.priv,
+ });
+ await env.merchant.stop();
+ await applyTimeTravelV2(14 * 86400000, {
+ exchange: env.exchange,
+ walletClient: env.walletClient,
+ });
+ await env.walletClient.call(Op.ResumeTransaction, { transactionId: id });
+ await env.walletClient.call(Op.TestingWaitTransactionState, {
+ transactionId: id,
+ txState: { major: Major.Done },
+ });
+ const tx = await env.walletClient.call(Op.GetTransactionById, {
+ transactionId: id,
+ });
+
+ assert.equal(
+ tx.txState.major,
+ Major.Done,
+ "Cleared KYC withdrawal should redenominate expired coins and complete",
+ );
+}
+
runKycWithdrawalCumulativeHardTest.suites = ["wallet"];
+runKycWithdrawalExpiryTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts
@@ -115,7 +115,10 @@ import { runKycSkipExpirationTest } from "./test-kyc-skip-expiration.js";
import { runKycTwoFormsTest } from "./test-kyc-two-forms.js";
import { runKycWalletDepositAbortTest } from "./test-kyc-wallet-deposit-abort.js";
import { runKycWithdrawalCumulativeTest } from "./test-kyc-withdrawal-cumulative.js";
-import { runKycWithdrawalCumulativeHardTest } from "./test-kyc-withdrawal-recovery.js";
+import {
+ runKycWithdrawalCumulativeHardTest,
+ runKycWithdrawalExpiryTest,
+} from "./test-kyc-withdrawal-recovery.js";
import { runKycWithdrawalVerbotenTest } from "./test-kyc-withdrawal-verboten.js";
import { runLibeufinBankTest } from "./test-libeufin-bank.js";
import { runLibeufinConversionTest } from "./test-libeufin-conversion.js";
@@ -475,6 +478,8 @@ const allTests: TestMainFunction[] = [
runKycBalanceWithdrawalZeroLimitTest,
withWalletDbBackend(runKycWithdrawalCumulativeHardTest, "default"),
withWalletDbBackend(runKycWithdrawalCumulativeHardTest, "indexeddb"),
+ withWalletDbBackend(runKycWithdrawalExpiryTest, "default"),
+ withWalletDbBackend(runKycWithdrawalExpiryTest, "indexeddb"),
withWalletDbBackend(runKycWithdrawalCumulativeTest, "default"),
withWalletDbBackend(runKycWithdrawalCumulativeTest, "indexeddb"),
runKycNewMeasureTest,
diff --git a/packages/taler-wallet-core/src/withdraw.ts b/packages/taler-wallet-core/src/withdraw.ts
@@ -3307,7 +3307,10 @@ async function processWithdrawalGroupPendingReady(
await wex.runWalletDbTx(async (tx) => {
const planchets = await tx.getPlanchetsByGroup(withdrawalGroupId);
for (const p of planchets) {
- if (p.planchetStatus !== PlanchetStatus.Pending) {
+ if (
+ p.planchetStatus !== PlanchetStatus.Pending &&
+ p.planchetStatus !== PlanchetStatus.KycRequired
+ ) {
continue;
}
if (!p.lastError) {