commit dd52fc10287f49edad0e0c590db5d893ac8b90cf
parent 1fa86832fe20cc201bb18d4757eb55dc6f99a363
Author: Florian Dold <florian@dold.me>
Date: Wed, 30 Jul 2025 13:33:16 +0200
wallet-core: fix kyc-auth handling in deposit, fix test
Diffstat:
4 files changed, 83 insertions(+), 27 deletions(-)
diff --git a/packages/taler-harness/src/integrationtests/test-kyc-deposit-kycauth.ts b/packages/taler-harness/src/integrationtests/test-kyc-deposit-kycauth.ts
@@ -3,21 +3,24 @@ import {
AmountString,
Configuration,
Duration,
+ j2s,
+ TalerWireGatewayHttpClient,
TransactionMajorState,
TransactionMinorState,
TransactionType,
} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import {
configureCommonKyc,
createKycTestkudosEnvironment,
createWalletDaemonWithClient,
+ postAmlDecisionNoRules,
withdrawViaBankV3,
} from "../harness/environments.js";
import {
- GlobalTestState,
getTestHarnessPaytoForLabel,
+ GlobalTestState,
} from "../harness/harness.js";
-import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
function adjustExchangeConfig(config: Configuration) {
configureCommonKyc(config);
@@ -43,20 +46,27 @@ export async function runKycDepositKycauthTest(t: GlobalTestState) {
// Set up test environment
// wallet that will send p2p transfer
- const { walletClient: w0, bankClient, exchange } =
- await createKycTestkudosEnvironment(t, { adjustExchangeConfig });
+ const {
+ walletClient: w0,
+ bankClient,
+ exchange,
+ bank,
+ exchangeBankAccount,
+ amlKeypair,
+ } = await createKycTestkudosEnvironment(t, { adjustExchangeConfig });
// wallet that will receive p2p transfer and do kyc + deposit
const w1 = await createWalletDaemonWithClient(t, {
name: "w0",
});
- await withdrawViaBankV3(t, {
+ const wres = await withdrawViaBankV3(t, {
bankClient,
amount: "TESTKUDOS:50",
exchange: exchange,
walletClient: w0,
});
+ await wres.withdrawalFinishedCond;
await w1.walletClient.call(WalletApiOperation.AddExchange, {
exchangeBaseUrl: exchange.baseUrl,
@@ -75,7 +85,7 @@ export async function runKycDepositKycauthTest(t: GlobalTestState) {
purse_expiration: AbsoluteTime.toProtocolTimestamp(
AbsoluteTime.addDuration(
AbsoluteTime.now(),
- Duration.fromSpec({ days: 2}),
+ Duration.fromSpec({ days: 2 }),
),
),
},
@@ -132,16 +142,6 @@ export async function runKycDepositKycauthTest(t: GlobalTestState) {
transactionId: depositTxId,
txState: {
major: TransactionMajorState.Pending,
- minor: TransactionMinorState.KycInit,
- },
- });
-
- t.logStep("deposit pending(kyc-init)");
-
- await w1.walletClient.call(WalletApiOperation.TestingWaitTransactionState, {
- transactionId: depositTxId,
- txState: {
- major: TransactionMajorState.Pending,
minor: TransactionMinorState.KycAuthRequired,
},
});
@@ -154,12 +154,52 @@ export async function runKycDepositKycauthTest(t: GlobalTestState) {
);
t.assertDeepEqual(depositTx.type, TransactionType.Deposit);
- t.assertDeepEqual(depositTx.txState.minor,
- TransactionMinorState.KycAuthRequired);
+ t.assertDeepEqual(
+ depositTx.txState.minor,
+ TransactionMinorState.KycAuthRequired,
+ );
t.assertTrue(depositTx.kycAuthTransferInfo !== undefined);
t.assertTrue(depositTx.kycAuthTransferInfo.creditPaytoUris.length > 0);
- // TODO: fulfill kyc-auth
+ // fulfill kyc-auth
+
+ console.log(`kyc info: ${j2s(depositTx.kycAuthTransferInfo)}`);
+
+ const wireGatewayApiClient = new TalerWireGatewayHttpClient(
+ exchangeBankAccount.wireGatewayApiBaseUrl,
+ );
+
+ await wireGatewayApiClient.addKycAuth({
+ auth: bank.getAdminAuth(),
+ body: {
+ amount: "TESTKUDOS:0.1",
+ debit_account: depositTx.kycAuthTransferInfo.debitPaytoUri,
+ account_pub: depositTx.kycAuthTransferInfo.accountPub,
+ },
+ });
+
+ await w1.walletClient.call(WalletApiOperation.TestingWaitTransactionState, {
+ transactionId: depositTxId,
+ txState: {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.KycRequired,
+ },
+ });
+
+ await postAmlDecisionNoRules(t, {
+ amlPriv: amlKeypair.priv,
+ amlPub: amlKeypair.pub,
+ exchangeBaseUrl: exchange.baseUrl,
+ paytoHash: depositTx.kycPaytoHash!,
+ });
+
+ await w1.walletClient.call(WalletApiOperation.TestingWaitTransactionState, {
+ transactionId: depositTxId,
+ txState: {
+ major: TransactionMajorState.Finalizing,
+ minor: TransactionMinorState.Track,
+ },
+ });
}
runKycDepositKycauthTest.suites = ["wallet"];
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts
@@ -2093,6 +2093,7 @@ export interface DepositKycInfo {
lastRuleGen?: number | undefined;
lastAmlReview?: boolean | undefined;
lastDeny?: DbPreciseTimestamp | undefined;
+ lastBadKycAuth?: boolean;
}
export interface TombstoneRecord {
diff --git a/packages/taler-wallet-core/src/deposits.ts b/packages/taler-wallet-core/src/deposits.ts
@@ -1119,6 +1119,7 @@ async function processDepositGroupPendingKyc(
lastDeny: maybeKycInfo.lastDeny,
lastRuleGen: maybeKycInfo.lastRuleGen,
haveAccessToken: maybeKycInfo.accessToken != null,
+ lastBadKycAuth: maybeKycInfo.lastBadKycAuth,
};
}
@@ -1151,15 +1152,21 @@ async function processDepositGroupPendingKyc(
const kycInfo = maybeKycInfo;
+ // FIXME: Brittle, can't we use the same data structure for all txns and copy it?
kycInfo.lastAmlReview = algoRes.updatedStatus.lastAmlReview;
kycInfo.lastCheckStatus = algoRes.updatedStatus.lastCheckStatus;
kycInfo.lastCheckCode = algoRes.updatedStatus.lastCheckCode;
kycInfo.lastDeny = algoRes.updatedStatus.lastDeny;
kycInfo.lastRuleGen = algoRes.updatedStatus.lastRuleGen;
kycInfo.accessToken = algoRes.updatedStatus.accessToken;
+ kycInfo.lastBadKycAuth = algoRes.updatedStatus.lastBadKycAuth;
const requiresAuth = algoRes.requiresAuth;
+ if (logger.shouldLogTrace()) {
+ logger.trace(`kyc check algo result: ${j2s(algoRes)}`);
+ }
+
// Now store the result.
return await wex.db.runReadWriteTx(
@@ -1288,7 +1295,7 @@ async function transitionToKycRequired(
args: {
kycPaytoHash: string;
exchangeUrl: string;
- kycAuth: boolean;
+ badKycAuth: boolean;
},
): Promise<TaskRunResult> {
const { depositGroupId } = depositGroup;
@@ -1306,21 +1313,21 @@ async function transitionToKycRequired(
switch (dg.operationStatus) {
case DepositOperationStatus.LegacyPendingTrack:
case DepositOperationStatus.FinalizingTrack:
- if (args.kycAuth) {
+ if (args.badKycAuth) {
throw Error("not yet supported");
} else {
dg.operationStatus = DepositOperationStatus.PendingAggregateKyc;
}
break;
case DepositOperationStatus.PendingDeposit:
- if (args.kycAuth) {
+ if (args.badKycAuth) {
dg.operationStatus = DepositOperationStatus.PendingDepositKycAuth;
} else {
dg.operationStatus = DepositOperationStatus.PendingDepositKyc;
}
break;
case DepositOperationStatus.PendingDepositKycAuth:
- if (!args.kycAuth) {
+ if (!args.badKycAuth) {
dg.operationStatus = DepositOperationStatus.PendingDepositKyc;
}
break;
@@ -1329,12 +1336,14 @@ async function transitionToKycRequired(
}
if (dg.kycInfo && dg.kycInfo.exchangeBaseUrl === args.exchangeUrl) {
dg.kycInfo.lastDeny = timestampPreciseToDb(TalerPreciseTimestamp.now());
+ dg.kycInfo.lastBadKycAuth = args.badKycAuth;
} else {
// Reset other info when new exchange is involved.
dg.kycInfo = {
exchangeBaseUrl: args.exchangeUrl,
paytoHash: args.kycPaytoHash,
lastDeny: timestampPreciseToDb(TalerPreciseTimestamp.now()),
+ lastBadKycAuth: args.badKycAuth,
};
}
await tx.depositGroups.put(dg);
@@ -1407,7 +1416,7 @@ async function processDepositGroupTrack(
return transitionToKycRequired(wex, depositGroup, {
exchangeUrl: exchangeBaseUrl,
kycPaytoHash: paytoHash,
- kycAuth: false, // ??
+ badKycAuth: false, // ??
});
} else {
updatedTxStatus = DepositElementStatus.Tracking;
@@ -1701,7 +1710,7 @@ async function submitDepositBatch(
return transitionToKycRequired(wex, depositGroup, {
exchangeUrl: exchangeBaseUrl,
kycPaytoHash: kycLegiNeededResp.h_payto,
- kycAuth: kycLegiNeededResp.bad_kyc_auth ?? false,
+ badKycAuth: kycLegiNeededResp.bad_kyc_auth ?? false,
});
}
}
diff --git a/packages/taler-wallet-core/src/kyc.ts b/packages/taler-wallet-core/src/kyc.ts
@@ -324,6 +324,7 @@ export interface GenericKycStatusReq {
readonly lastRuleGen?: number | undefined;
readonly lastAmlReview?: boolean | undefined;
readonly lastDeny?: DbPreciseTimestamp | undefined;
+ readonly lastBadKycAuth?: boolean;
readonly haveAccessToken: boolean;
}
@@ -337,6 +338,8 @@ export interface GenericKycStatusResp {
lastCheckCode?: number | undefined;
lastRuleGen?: number | undefined;
lastAmlReview?: boolean | undefined;
+ lastBadKycAuth: boolean;
+
lastDeny?: DbPreciseTimestamp | undefined;
/** New account public key, only present if updated. */
accountPub?: string;
@@ -387,7 +390,8 @@ export async function runKycCheckAlgo(
doLongpoll = false;
} else if (
st.lastCheckStatus === HttpStatusCode.Forbidden ||
- st.lastCheckStatus === HttpStatusCode.Conflict
+ st.lastCheckStatus === HttpStatusCode.Conflict ||
+ (st.lastCheckStatus === HttpStatusCode.NotFound && st.lastBadKycAuth)
) {
doLongpoll = true;
url.searchParams.set("lpt", "1");
@@ -442,6 +446,7 @@ export async function runKycCheckAlgo(
lastCheckStatus: kycStatusRes.status,
lastDeny: st.lastDeny,
lastRuleGen: st.lastRuleGen,
+ lastBadKycAuth: st.lastBadKycAuth ?? false,
};
const rst: GenericKycStatusResp = {
@@ -450,7 +455,8 @@ export async function runKycCheckAlgo(
updatedStatus,
requiresAuth:
kycStatusRes.status === HttpStatusCode.Conflict ||
- kycStatusRes.status === HttpStatusCode.Forbidden,
+ kycStatusRes.status === HttpStatusCode.Forbidden ||
+ (kycStatusRes.status === HttpStatusCode.NotFound && st.lastBadKycAuth),
};
let exposedLimits: AccountLimit[] | undefined = undefined;