taler-typescript-core

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

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:
Mpackages/taler-harness/src/integrationtests/test-kyc-deposit-kycauth.ts | 78+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
Mpackages/taler-wallet-core/src/db.ts | 1+
Mpackages/taler-wallet-core/src/deposits.ts | 21+++++++++++++++------
Mpackages/taler-wallet-core/src/kyc.ts | 10++++++++--
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;