taler-typescript-core

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

commit cca3196ed9c61d3e3d92dcd18bea7a97d4526948
parent 0f58e593047b5b5041688f76e2be4c576cb158ce
Author: Florian Dold <florian@dold.me>
Date:   Tue,  3 Jun 2025 20:29:55 +0200

harness: add test for exchange kyc auth

Issue: https://bugs.taler.net/n/10044

Diffstat:
Mpackages/taler-harness/src/harness/environments.ts | 6+++---
Apackages/taler-harness/src/integrationtests/test-exchange-kyc-auth.ts | 302++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-harness/src/integrationtests/testrunner.ts | 2++
Mpackages/taler-util/src/http-client/exchange-client.ts | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 361 insertions(+), 3 deletions(-)

diff --git a/packages/taler-harness/src/harness/environments.ts b/packages/taler-harness/src/harness/environments.ts @@ -46,7 +46,7 @@ import { succeedOrThrow, TalerCorebankApiClient, TalerCoreBankHttpClient, - TalerExchangeHttpClient, + TalerExchangeHttpClient2, TalerMerchantApi, TalerMerchantInstanceHttpClient, TalerProtocolTimestamp, @@ -1098,7 +1098,7 @@ export interface KycTestEnv { amlKeypair: EddsaKeyPairStrings; merchant: MerchantService; bankApi: TalerCoreBankHttpClient; - exchangeApi: TalerExchangeHttpClient; + exchangeApi: TalerExchangeHttpClient2; wireGatewayApi: TalerWireGatewayHttpClient; merchantApi: TalerMerchantInstanceHttpClient; } @@ -1272,7 +1272,7 @@ export async function createKycTestkudosEnvironment( harnessHttpLib, ); - const exchangeApi = new TalerExchangeHttpClient(exchange.baseUrl, { + const exchangeApi = new TalerExchangeHttpClient2(exchange.baseUrl, { httpClient: harnessHttpLib, }); diff --git a/packages/taler-harness/src/integrationtests/test-exchange-kyc-auth.ts b/packages/taler-harness/src/integrationtests/test-exchange-kyc-auth.ts @@ -0,0 +1,302 @@ +/* + 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, + createEddsaKeyPair, + encodeCrock, + hashNormalizedPaytoUri, + HttpStatusCode, + j2s, + TalerWireGatewayHttpClient, + TransactionMajorState, + TransactionMinorState, +} from "@gnu-taler/taler-util"; +import { + createSyncCryptoApi, + WalletApiOperation, +} from "@gnu-taler/taler-wallet-core"; +import { + configureCommonKyc, + createKycTestkudosEnvironment, + withdrawViaBankV3, +} from "../harness/environments.js"; +import { + getTestHarnessPaytoForLabel, + GlobalTestState, +} from "../harness/harness.js"; + +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:1 +TIMEFRAME = "1 days" +`; + +function adjustExchangeConfig(config: Configuration) { + configureCommonKyc(config); + config.loadFromString(myAmlConfig); +} + +/** + * Test for KYC auth, based on withdrawals and/or + * KYC auth transfers. + */ +export async function runExchangeKycAuthTest(t: GlobalTestState) { + // Set up test environment + + // FIXME: Reduced test environment without merchant suffices + const { + walletClient, + bankClient, + exchange, + amlKeypair, + exchangeBankAccount, + exchangeApi, + } = await createKycTestkudosEnvironment(t, { adjustExchangeConfig }); + + const merchantPayto = getTestHarnessPaytoForLabel("merchant-default"); + + const cryptoApi = createSyncCryptoApi(); + + const wireGatewayApiClient = new TalerWireGatewayHttpClient( + exchangeBankAccount.wireGatewayApiBaseUrl, + ); + + const merchantPair = await cryptoApi.createEddsaKeypair({}); + + const wres = await withdrawViaBankV3(t, { + walletClient, + exchange, + bankClient, + amount: "TESTKUDOS:20", + }); + + const kycPaytoHash = encodeCrock(hashNormalizedPaytoUri(merchantPayto)); + + await wres.withdrawalFinishedCond; + + // Use the wallet to trigger KYC + + const depositResp = await walletClient.call( + WalletApiOperation.CreateDepositGroup, + { + amount: "TESTKUDOS:5", + depositPaytoUri: merchantPayto, + testingFixedPriv: merchantPair.priv, + }, + ); + + // Wait until KYC got triggered. + await walletClient.call(WalletApiOperation.TestingWaitTransactionState, { + transactionId: depositResp.transactionId, + txState: { + major: TransactionMajorState.Pending, + minor: TransactionMinorState.KycAuthRequired, + }, + }); + + await wireGatewayApiClient.addKycAuth({ + body: { + account_pub: merchantPair.pub, + amount: "TESTKUDOS:0.1", + debit_account: merchantPayto, + }, + auth: exchangeBankAccount.wireGatewayAuth, + }); + + await walletClient.call(WalletApiOperation.TestingWaitTransactionState, { + transactionId: depositResp.transactionId, + txState: { + major: TransactionMajorState.Pending, + minor: TransactionMinorState.KycRequired, + }, + }); + + { + const sigResp = await cryptoApi.signWalletKycAuth({ + accountPriv: merchantPair.priv, + accountPub: merchantPair.pub, + }); + const checkResp1 = await exchangeApi.checkKycStatus({ + accountPub: merchantPair.pub, + accountSig: sigResp.sig, + paytoHash: kycPaytoHash, + }); + + console.log(j2s(checkResp1)); + + t.assertDeepEqual(checkResp1.case, HttpStatusCode.Accepted); + } + + const reservePair2 = createEddsaKeyPair(); + const reservePair3 = createEddsaKeyPair(); + const reservePair4 = createEddsaKeyPair(); + + { + const sigResp = await cryptoApi.signWalletKycAuth({ + accountPriv: encodeCrock(reservePair2.eddsaPriv), + accountPub: encodeCrock(reservePair2.eddsaPub), + }); + const checkResp = await exchangeApi.checkKycStatus({ + accountPub: merchantPair.pub, + accountSig: sigResp.sig, + paytoHash: kycPaytoHash, + }); + + console.log(j2s(checkResp)); + + t.assertDeepEqual(checkResp.case, HttpStatusCode.Forbidden); + } + + await wireGatewayApiClient.addIncoming({ + body: { + reserve_pub: encodeCrock(reservePair2.eddsaPub), + amount: "TESTKUDOS:5", + debit_account: merchantPayto, + }, + auth: exchangeBankAccount.wireGatewayAuth, + }); + + await wireGatewayApiClient.addIncoming({ + body: { + reserve_pub: encodeCrock(reservePair3.eddsaPub), + amount: "TESTKUDOS:5", + debit_account: merchantPayto, + }, + auth: exchangeBankAccount.wireGatewayAuth, + }); + + await wireGatewayApiClient.addIncoming({ + body: { + reserve_pub: encodeCrock(reservePair4.eddsaPub), + amount: "TESTKUDOS:5", + debit_account: getTestHarnessPaytoForLabel("bob"), + }, + auth: exchangeBankAccount.wireGatewayAuth, + }); + + await exchange.runWirewatchOnce(); + + // Even when no account pub is specified, the last reserve pub for + // the account must work. + + { + const sigResp = await cryptoApi.signWalletKycAuth({ + accountPriv: encodeCrock(reservePair3.eddsaPriv), + accountPub: encodeCrock(reservePair3.eddsaPub), + }); + const checkResp = await exchangeApi.testingCheckKycStatusNoPub({ + accountSig: sigResp.sig, + paytoHash: kycPaytoHash, + }); + + console.log(j2s(checkResp)); + + t.assertDeepEqual(checkResp.case, HttpStatusCode.Accepted); + } + + // Now, kyc auth must work with explicit account key and both reserve pubs! + + { + const sigResp = await cryptoApi.signWalletKycAuth({ + accountPriv: merchantPair.priv, + accountPub: merchantPair.pub, + }); + const checkResp = await exchangeApi.checkKycStatus({ + accountPub: merchantPair.pub, + accountSig: sigResp.sig, + paytoHash: kycPaytoHash, + }); + + console.log(j2s(checkResp)); + + t.assertDeepEqual(checkResp.case, HttpStatusCode.Accepted); + } + + { + const sigResp = await cryptoApi.signWalletKycAuth({ + accountPriv: encodeCrock(reservePair3.eddsaPriv), + accountPub: encodeCrock(reservePair3.eddsaPub), + }); + const checkResp = await exchangeApi.checkKycStatus({ + accountPub: encodeCrock(reservePair3.eddsaPub), + accountSig: sigResp.sig, + paytoHash: kycPaytoHash, + }); + + console.log(j2s(checkResp)); + + t.assertDeepEqual(checkResp.case, HttpStatusCode.Accepted); + } + + { + const sigResp = await cryptoApi.signWalletKycAuth({ + accountPriv: encodeCrock(reservePair2.eddsaPriv), + accountPub: encodeCrock(reservePair2.eddsaPub), + }); + const checkResp = await exchangeApi.checkKycStatus({ + accountPub: encodeCrock(reservePair2.eddsaPub), + accountSig: sigResp.sig, + paytoHash: kycPaytoHash, + }); + + console.log(j2s(checkResp)); + + t.assertDeepEqual(checkResp.case, HttpStatusCode.Accepted); + } +} + +runExchangeKycAuthTest.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts @@ -49,6 +49,7 @@ import { runDepositFaultTest } from "./test-deposit-fault.js"; import { runDepositMergeTest } from "./test-deposit-merge.js"; import { runDepositTest } from "./test-deposit.js"; import { runExchangeDepositTest } from "./test-exchange-deposit.js"; +import { runExchangeKycAuthTest } from "./test-exchange-kyc-auth.js"; import { runExchangeManagementFaultTest } from "./test-exchange-management-fault.js"; import { runExchangeManagementTest } from "./test-exchange-management.js"; import { runExchangeMasterPubChangeTest } from "./test-exchange-master-pub-change.js"; @@ -347,6 +348,7 @@ const allTests: TestMainFunction[] = [ runUtilMerchantClientTest, runKycWalletDepositAbortTest, runKycMerchantDepositFormTest, + runExchangeKycAuthTest, ]; export interface TestRunSpec { diff --git a/packages/taler-util/src/http-client/exchange-client.ts b/packages/taler-util/src/http-client/exchange-client.ts @@ -665,6 +665,60 @@ export class TalerExchangeHttpClient2 { } /** + * Do a /kyc-check request, but don't specify + * the account pub explicitly. + * + * Deprecated, but used in tests. + */ + async testingCheckKycStatusNoPub(args: { + paytoHash: string; + accountSig: EddsaSignatureString; + longpoll?: boolean; + awaitAuth?: boolean; + }): Promise< + | OperationOk<void> + | OperationAlternative<HttpStatusCode.Ok, AccountKycStatus> + | OperationAlternative<HttpStatusCode.Accepted, AccountKycStatus> + | OperationFail<HttpStatusCode.Forbidden> + | OperationFail<HttpStatusCode.NotFound> + | OperationFail<HttpStatusCode.Conflict> + > { + const { paytoHash, accountSig, longpoll, awaitAuth } = args; + const url = new URL(`kyc-check/${paytoHash}`, this.baseUrl); + if (awaitAuth !== undefined) { + url.searchParams.set("await_auth", awaitAuth ? "YES" : "NO"); + } + + const resp = await this.fetch( + url, + { + headers: { + "Account-Owner-Signature": accountSig, + }, + }, + longpoll, + ); + + switch (resp.status) { + case HttpStatusCode.Ok: + case HttpStatusCode.Accepted: + return opKnownAlternativeHttpFailure( + resp, + resp.status, + codecForAccountKycStatus(), + ); + case HttpStatusCode.NoContent: + return opEmptySuccess(); + case HttpStatusCode.Forbidden: + case HttpStatusCode.NotFound: + case HttpStatusCode.Conflict: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownHttpFailure(resp); + } + } + + /** * https://docs.taler.net/core/api-exchange.html#get--kyc-info-$ACCESS_TOKEN * */