taler-typescript-core

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

commit c1556862b503e3460cb8903a4404dd41c818e348
parent 6a3448f1233bdfb9da24369e8ae8e2e14ad3c5c0
Author: Florian Dold <florian@dold.me>
Date:   Tue,  6 May 2025 22:01:37 +0200

harness: TOPS measures test

Diffstat:
Mpackages/taler-harness/src/harness/harness.ts | 21++++++++++++++++++++-
Mpackages/taler-harness/src/harness/tops.ts | 30------------------------------
Mpackages/taler-harness/src/integrationtests/test-account-restrictions.ts | 2+-
Mpackages/taler-harness/src/integrationtests/test-bank-api.ts | 2+-
Mpackages/taler-harness/src/integrationtests/test-exchange-management-fault.ts | 4++--
Mpackages/taler-harness/src/integrationtests/test-exchange-master-pub-change.ts | 2+-
Mpackages/taler-harness/src/integrationtests/test-payment-claim.ts | 4++--
Mpackages/taler-harness/src/integrationtests/test-peer-pull.ts | 10+++++-----
Mpackages/taler-harness/src/integrationtests/test-peer-push.ts | 4++--
Mpackages/taler-harness/src/integrationtests/test-tops-aml-custom-addr-sms.ts | 6+++---
Apackages/taler-harness/src/integrationtests/test-tops-aml-legi.ts | 83+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/taler-harness/src/integrationtests/test-tops-aml-measures.ts | 467+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-harness/src/integrationtests/test-wallet-devexp-fakeprotover.ts | 4++--
Mpackages/taler-harness/src/integrationtests/test-wallet-insufficient-balance.ts | 4++--
Mpackages/taler-harness/src/integrationtests/test-withdrawal-abort-bank.ts | 2+-
Mpackages/taler-harness/src/integrationtests/test-withdrawal-bank-integrated.ts | 4++--
Mpackages/taler-harness/src/integrationtests/testrunner.ts | 4++++
Mpackages/taler-util/src/http-client/exchange.ts | 35++++++++++++++++++++++++++++++++++-
Mpackages/taler-util/src/time.ts | 12++++++++++++
Mpackages/taler-util/src/types-taler-exchange.ts | 55++++++++++++++++++++++++++++++-------------------------
20 files changed, 674 insertions(+), 81 deletions(-)

diff --git a/packages/taler-harness/src/harness/harness.ts b/packages/taler-harness/src/harness/harness.ts @@ -323,7 +323,10 @@ export class GlobalTestState { logger.info(`STEP: ${stepEnd}`); } - async assertThrowsTalerErrorAsync( + /** + * @deprecated use {@link assertThrowsAsync} instead + */ + async assertThrowsTalerErrorAsyncLegacy( block: Promise<unknown>, ): Promise<TalerError> { try { @@ -339,6 +342,22 @@ export class GlobalTestState { ); } + async assertThrowsTalerErrorAsync( + block: () => Promise<unknown>, + ): Promise<TalerError> { + try { + await block(); + } catch (e) { + if (e instanceof TalerError) { + return e; + } + throw Error(`expected TalerError to be thrown, but got ${e}`); + } + throw Error( + `expected TalerError to be thrown, but block finished without throwing`, + ); + } + async assertThrowsAsync(block: () => Promise<void>): Promise<any> { try { await block(); diff --git a/packages/taler-harness/src/harness/tops.ts b/packages/taler-harness/src/harness/tops.ts @@ -487,36 +487,6 @@ DESCRIPTION = "Program that checks if the 'Controlling entity 3rd persion' check COMMAND = taler-exchange-helper-measure-tops-3rdparty-check ENABLED = YES FALLBACK = freeze-investigate - -# ------------------ -# Config for testing - -# Note: For *testing* KYC processes. -[kyc-rule-balance-testing-limit1] -OPERATION_TYPE = BALANCE -NEXT_MEASURES = sms-registration -EXPOSED = YES -ENABLED = YES -THRESHOLD = CHF:1 -TIMEFRAME = "1 days" - -# Note: For *testing* KYC processes. -[kyc-rule-balance-testing-limit5] -OPERATION_TYPE = BALANCE -NEXT_MEASURES = kyx -EXPOSED = YES -ENABLED = YES -THRESHOLD = CHF:5 -TIMEFRAME = "1 days" - -# Note: For *testing* KYC processes. -[kyc-rule-balance-testing-limit10] -OPERATION_TYPE = BALANCE -NEXT_MEASURES = sms-registration postal-registration -EXPOSED = YES -ENABLED = YES -THRESHOLD = CHF:10 -TIMEFRAME = "1 days" `; const topsProvidersTestConf = ` diff --git a/packages/taler-harness/src/integrationtests/test-account-restrictions.ts b/packages/taler-harness/src/integrationtests/test-account-restrictions.ts @@ -90,7 +90,7 @@ export async function runAccountRestrictionsTest(t: GlobalTestState) { }); // Invalid account, does not start with "foo-" - const err = await t.assertThrowsTalerErrorAsync( + const err = await t.assertThrowsTalerErrorAsyncLegacy( walletClient.call(WalletApiOperation.CheckDeposit, { amount: "TESTKUDOS:5", depositPaytoUri: "payto://x-taler-bank/localhost/bar-42", diff --git a/packages/taler-harness/src/integrationtests/test-bank-api.ts b/packages/taler-harness/src/integrationtests/test-bank-api.ts @@ -130,7 +130,7 @@ export async function runBankApiTest(t: GlobalTestState) { // Make sure that registering twice results in a 409 Conflict { - const e = await t.assertThrowsTalerErrorAsync( + const e = await t.assertThrowsTalerErrorAsyncLegacy( bankClient.registerAccount("user1", "password2"), ); t.assertTrue(e.errorDetail.httpStatusCode === 409); diff --git a/packages/taler-harness/src/integrationtests/test-exchange-management-fault.ts b/packages/taler-harness/src/integrationtests/test-exchange-management-fault.ts @@ -191,7 +191,7 @@ export async function runExchangeManagementFaultTest( }, }); - const err1 = await t.assertThrowsTalerErrorAsync( + const err1 = await t.assertThrowsTalerErrorAsyncLegacy( wallet.client.call(WalletApiOperation.AddExchange, { exchangeBaseUrl: faultyExchange.baseUrl, }), @@ -239,7 +239,7 @@ export async function runExchangeManagementFaultTest( }, }); - const err2 = await t.assertThrowsTalerErrorAsync( + const err2 = await t.assertThrowsTalerErrorAsyncLegacy( wallet.client.call(WalletApiOperation.AddExchange, { exchangeBaseUrl: faultyExchange.baseUrl, }), diff --git a/packages/taler-harness/src/integrationtests/test-exchange-master-pub-change.ts b/packages/taler-harness/src/integrationtests/test-exchange-master-pub-change.ts @@ -85,7 +85,7 @@ export async function runExchangeMasterPubChangeTest( t.logStep("exchange-restarted"); - const err = await t.assertThrowsTalerErrorAsync( + const err = await t.assertThrowsTalerErrorAsyncLegacy( walletClient.call(WalletApiOperation.UpdateExchangeEntry, { exchangeBaseUrl: exchange.baseUrl, force: true, diff --git a/packages/taler-harness/src/integrationtests/test-payment-claim.ts b/packages/taler-harness/src/integrationtests/test-payment-claim.ts @@ -90,7 +90,7 @@ export async function runPaymentClaimTest(t: GlobalTestState) { preparePayResult.status === PreparePayResultType.PaymentPossible, ); - const errOne = t.assertThrowsTalerErrorAsync( + const errOne = t.assertThrowsTalerErrorAsyncLegacy( w2.walletClient.call(WalletApiOperation.PreparePayForUri, { talerPayUri, }) @@ -112,7 +112,7 @@ export async function runPaymentClaimTest(t: GlobalTestState) { await w2.walletClient.call(WalletApiOperation.ClearDb, {}); - const err = await t.assertThrowsTalerErrorAsync( + const err = await t.assertThrowsTalerErrorAsyncLegacy( w2.walletClient.call(WalletApiOperation.PreparePayForUri, { talerPayUri, }) diff --git a/packages/taler-harness/src/integrationtests/test-peer-pull.ts b/packages/taler-harness/src/integrationtests/test-peer-pull.ts @@ -138,13 +138,13 @@ export async function runPeerPullTest(t: GlobalTestState) { t.logStep("P2P pull errors"); { const tx = await initPeerPullCredit("confirm", "TESTKUDOS:1000"); - const insufficient_balance = await t.assertThrowsTalerErrorAsync(wallet1.call( + const insufficient_balance = await t.assertThrowsTalerErrorAsyncLegacy(wallet1.call( WalletApiOperation.PreparePeerPullDebit, { talerUri: tx.talerUri! } )); t.assertTrue(insufficient_balance.errorDetail.code === TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE); - const unknown_purse = await t.assertThrowsTalerErrorAsync(wallet1.call( + const unknown_purse = await t.assertThrowsTalerErrorAsyncLegacy(wallet1.call( WalletApiOperation.PreparePeerPullDebit, { talerUri: "taler+http://pay-pull/localhost:8081/MQP1DP1J94ZZWNQS7TRDF1KJZ7V8H74CZF41V90FKXBPN5GNRN6G" } )); @@ -214,7 +214,7 @@ export async function runPeerPullTest(t: GlobalTestState) { }), ]); - const completed_purse = await t.assertThrowsTalerErrorAsync(wallet1.call( + const completed_purse = await t.assertThrowsTalerErrorAsyncLegacy(wallet1.call( WalletApiOperation.PreparePeerPullDebit, { talerUri: tx.talerUri! } )); @@ -357,7 +357,7 @@ export async function runPeerPullTest(t: GlobalTestState) { }), ]); - const aborted_contract = await t.assertThrowsTalerErrorAsync(wallet1.call( + const aborted_contract = await t.assertThrowsTalerErrorAsyncLegacy(wallet1.call( WalletApiOperation.PreparePeerPullDebit, { talerUri: tx.talerUri! } )); @@ -452,7 +452,7 @@ export async function runPeerPullTest(t: GlobalTestState) { }), ]); - const expired_purse = await t.assertThrowsTalerErrorAsync(wallet1.call( + const expired_purse = await t.assertThrowsTalerErrorAsyncLegacy(wallet1.call( WalletApiOperation.PreparePeerPullDebit, { talerUri: tx.talerUri! } )); diff --git a/packages/taler-harness/src/integrationtests/test-peer-push.ts b/packages/taler-harness/src/integrationtests/test-peer-push.ts @@ -124,7 +124,7 @@ export async function runPeerPushTest(t: GlobalTestState) { t.logStep("P2P push errors"); { - const ex1 = await t.assertThrowsTalerErrorAsync( + const ex1 = await t.assertThrowsTalerErrorAsyncLegacy( wallet1.call(WalletApiOperation.InitiatePeerPushDebit, { partialContractTerms: { summary: "(this will fail)", @@ -139,7 +139,7 @@ export async function runPeerPushTest(t: GlobalTestState) { // FIXME propagate the error correctly // t.assertTrue(ex1.errorDetail.code === TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE); - const unknown_purse = await t.assertThrowsTalerErrorAsync( + const unknown_purse = await t.assertThrowsTalerErrorAsyncLegacy( wallet1.call(WalletApiOperation.PreparePeerPushCredit, { talerUri: "taler+http://pay-push/localhost:8081/MQP1DP1J94ZZWNQS7TRDF1KJZ7V8H74CZF41V90FKXBPN5GNRN6G", diff --git a/packages/taler-harness/src/integrationtests/test-tops-aml-custom-addr-sms.ts b/packages/taler-harness/src/integrationtests/test-tops-aml-custom-addr-sms.ts @@ -53,7 +53,7 @@ export async function runTopsAmlCustomAddrSmsTest(t: GlobalTestState) { wireGatewayApi, } = await createTopsEnvironment(t); - const challenger = await startFakeChallenger({ + const challengerSms = await startFakeChallenger({ port: 6002, addressType: "phone", }); @@ -250,7 +250,7 @@ export async function runTopsAmlCustomAddrSmsTest(t: GlobalTestState) { t.assertTrue(typeof nonce === "string"); const proofRedirectUrl = respJson.redirect_url; - challenger.fakeVerification(nonce, { + challengerSms.fakeVerification(nonce, { CONTACT_PHONE: "+4123456789", }); @@ -263,7 +263,7 @@ export async function runTopsAmlCustomAddrSmsTest(t: GlobalTestState) { console.log("proof status:", proofResp.status); t.assertDeepEqual(proofResp.status, 303); - const setupReq = challenger.getSetupRequest(nonce); + const setupReq = challengerSms.getSetupRequest(nonce); console.log(`setup request: ${j2s(setupReq)}`); t.assertDeepEqual(setupReq.CONTACT_PHONE, "+4123456789"); diff --git a/packages/taler-harness/src/integrationtests/test-tops-aml-legi.ts b/packages/taler-harness/src/integrationtests/test-tops-aml-legi.ts @@ -0,0 +1,83 @@ +/* + This file is part of GNU Taler + (C) 2025 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 { + j2s, + Logger, + succeedOrThrow, + TalerExchangeHttpClient, + TalerMerchantInstanceHttpClient, +} from "@gnu-taler/taler-util"; +import { startFakeChallenger } from "../harness/fake-challenger.js"; +import { GlobalTestState, harnessHttpLib } from "../harness/harness.js"; +import { + createTopsEnvironment, + doTopsAcceptTos, + doTopsKycAuth, +} from "../harness/tops.js"; + +export const logger = new Logger("test-tops-aml.ts"); + +export async function runTopsAmlLegiTest(t: GlobalTestState) { + // Set up test environment + const { + exchange, + officerAcc, + merchant, + exchangeBankAccount, + wireGatewayApi, + } = await createTopsEnvironment(t); + + const challenger = await startFakeChallenger({ + port: 6001, + addressType: "postal-ch", + }); + + const merchantClient = new TalerMerchantInstanceHttpClient( + merchant.makeInstanceBaseUrl(), + ); + const exchangeClient = new TalerExchangeHttpClient(exchange.baseUrl, { + httpClient: harnessHttpLib, + }); + + // Do KYC auth transfer + const { accessToken, merchantPaytoHash } = await doTopsKycAuth(t, { + merchantClient, + exchangeBankAccount, + wireGatewayApi, + }); + + // Accept ToS + await doTopsAcceptTos(t, { + accessToken, + exchangeClient, + merchantClient, + merchant, + }); + + const legis = succeedOrThrow( + await exchangeClient.getAmlLegitimizations({ + officerAcc, + }), + ); + + console.log(j2s(legis)); +} + +runTopsAmlLegiTest.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/test-tops-aml-measures.ts b/packages/taler-harness/src/integrationtests/test-tops-aml-measures.ts @@ -0,0 +1,467 @@ +/* + This file is part of GNU Taler + (C) 2025 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 { + AmlDecision, + Amounts, + AmountString, + ConfigSources, + Configuration, + Duration, + j2s, + KycRule, + LimitOperationType, + Logger, + OfficerAccount, + succeedOrThrow, + TalerExchangeHttpClient, + TalerMerchantInstanceHttpClient, + TalerProtocolTimestamp, +} from "@gnu-taler/taler-util"; +import { + startFakeChallenger, + TestfakeChallengerService, +} from "../harness/fake-challenger.js"; +import { GlobalTestState, harnessHttpLib } from "../harness/harness.js"; +import { + createTopsEnvironment, + doTopsKycAuth, + topsKycRulesConf, +} from "../harness/tops.js"; + +export const logger = new Logger("test-tops-aml-measures.ts"); + +/** + * Test that invokes all measures defined for the TOPS deployment. + */ +export async function runTopsAmlMeasuresTest(t: GlobalTestState) { + // Setup is done, now the real testing can start! + + const { + myTriggerMeasure, + myTriggerReset, + submitForm, + challengerPostal, + challengerSms, + expectInfo, + fakeChallenger, + } = await setupMeasuresTestEnvironment(t); + + { + await myTriggerMeasure("kyx"); + await submitForm("vqf_902_1_customer", { + FORM_ID: "vqf_902_1_customer", + FORM_VERSION: 1, + CUSTOMER_TYPE: "NATURAL_PERSON", + CUSTOMER_TYPE_VQF: "NATURAL_PERSON", + FULL_NAME: "Alice A", + DOMICILE_ADDRESS: "Castle St. 1\nWondertown", + }); + } + + await t.assertThrowsTalerErrorAsync(async () => { + await myTriggerMeasure("foobar"); + }); + + { + const res = await myTriggerMeasure("freeze-investigate"); + console.log("after freeze-investigate:", j2s(res)); + t.assertTrue(isFrozen(res.currentDecision)); + } + + await myTriggerReset(); + + { + const res = await myTriggerMeasure("inform-investigate"); + console.log("after inform-investigate:", j2s(res)); + t.assertTrue(!isFrozen(res.currentDecision)); + } + + await myTriggerReset(); + + { + await myTriggerMeasure("sms-registration"); + await fakeChallenger(challengerSms, { + CONTACT_PHONE: "+4123456789", + }); + } + + { + const res = await myTriggerMeasure("postal-registration"); + await fakeChallenger(challengerPostal, { + CONTACT_NAME: "Richard Stallman", + ADDRESS_LINES: "Bundesgasse 1\n1234 Bern", + }); + } + + { + await myTriggerMeasure("accept-tos"); + await submitForm("accept-tos", { + FORM_ID: "accept-tos", + FORM_VERSION: 1, + ACCEPTED_TERMS_OF_SERVICE: "v1", + }); + } + + { + await myTriggerMeasure("form-vqf-902.9"); + await submitForm("vqf_902_9_customer", { + // Form is not validated yet + FORM_ID: "vqf_902_9_customer", + FORM_VERSION: 1, + }); + } + + { + await myTriggerMeasure("form-vqf-902.11"); + // Form is not validated yet + await submitForm("vqf_902_11_customer", { + FORM_ID: "vqf_902_11_customer", + THIRD_PARTY_OWNERSHIP: true, + FORM_VERSION: 1, + }); + await submitForm("vqf_902_9_customer", { + // Form is not validated yet + FORM_ID: "vqf_902_9_customer", + FORM_VERSION: 1, + }); + } + + await myTriggerReset(); + + { + await myTriggerMeasure("form-vqf-902.11"); + // Form is not validated yet + await submitForm("vqf_902_11_customer", { + FORM_ID: "vqf_902_11_customer", + THIRD_PARTY_OWNERSHIP: false, + FORM_VERSION: 1, + }); + // No third party ownership => Waiting for AML officer + await expectInfo(); + } +} + +/** + * Helpers for the test. + */ +interface MeasuresTestEnvironment { + expectInfo: () => Promise<void>; + fakeChallenger: ( + challenger: TestfakeChallengerService, + address: any, + ) => Promise<void>; + submitForm: (form: string, data: any) => Promise<void>; + myTriggerMeasure: (measure: string) => Promise<{ + currentDecision: AmlDecision; + }>; + myTriggerReset: () => Promise<void>; + challengerPostal: TestfakeChallengerService; + challengerSms: TestfakeChallengerService; +} + +async function setupMeasuresTestEnvironment( + t: GlobalTestState, +): Promise<MeasuresTestEnvironment> { + // Set up test environment + const { + exchange, + officerAcc, + merchant, + exchangeBankAccount, + wireGatewayApi, + } = await createTopsEnvironment(t); + + const challengerPostal = await startFakeChallenger({ + port: 6001, + addressType: "postal-ch", + }); + const challengerSms = await startFakeChallenger({ + port: 6002, + addressType: "phone", + }); + + const merchantClient = new TalerMerchantInstanceHttpClient( + merchant.makeInstanceBaseUrl(), + ); + const exchangeClient = new TalerExchangeHttpClient(exchange.baseUrl, { + httpClient: harnessHttpLib, + }); + + // Do KYC auth transfer + const { accessToken, merchantPaytoHash } = await doTopsKycAuth(t, { + merchantClient, + exchangeBankAccount, + wireGatewayApi, + }); + + const myTriggerMeasure = (measure: string) => + triggerMeasure(t, { + officerAcc, + exchangeClient, + merchantPaytoHash, + measure, + }); + + const myTriggerReset = () => + triggerReset(t, { + officerAcc, + exchangeClient, + merchantPaytoHash, + }); + + const submitForm = async (form: string, data: any) => { + const kycInfoResp = await exchangeClient.checkKycInfo( + accessToken, + undefined, + undefined, + ); + t.assertDeepEqual(kycInfoResp.case, "ok"); + t.assertDeepEqual(kycInfoResp.body.requirements[0].form, form); + const requirementId = kycInfoResp.body.requirements[0].id; + t.assertTrue(typeof requirementId === "string"); + const uploadRes = await exchangeClient.uploadKycForm(requirementId, data); + console.log("upload res", uploadRes); + t.assertDeepEqual(uploadRes.case, "ok"); + }; + + const fakeChallenger = async ( + challenger: TestfakeChallengerService, + address: any, + ) => { + { + const kycInfoResp = await exchangeClient.checkKycInfo( + accessToken, + undefined, + undefined, + ); + console.log( + `kyc info after my-postal-registration measure`, + j2s(kycInfoResp), + ); + t.assertDeepEqual(kycInfoResp.case, "ok"); + const kycInfo = kycInfoResp.body; + t.assertDeepEqual(kycInfo.requirements[0].form, "LINK"); + t.assertTrue(typeof kycInfo.requirements[0].id === "string"); + + const startResp = succeedOrThrow( + await exchangeClient.startExternalKycProcess( + kycInfo.requirements[0].id, + {}, + ), + ); + console.log(`start resp`, j2s(startResp)); + + let challengerRedirectUrl = startResp.redirect_url; + + const resp = await harnessHttpLib.fetch(challengerRedirectUrl); + const respJson = await resp.json(); + console.log(`challenger resp: ${j2s(respJson)}`); + + const nonce = respJson.nonce; + t.assertTrue(typeof nonce === "string"); + const proofRedirectUrl = respJson.redirect_url; + + challenger.fakeVerification(nonce, address); + + console.log("nonce", nonce); + console.log("proof redirect URL", proofRedirectUrl); + + const proofResp = await harnessHttpLib.fetch(proofRedirectUrl, { + redirect: "manual", + }); + console.log("proof status:", proofResp.status); + t.assertDeepEqual(proofResp.status, 303); + + const setupReq = challenger.getSetupRequest(nonce); + console.log(`setup request: ${j2s(setupReq)}`); + } + }; + + const expectInfo = async () => { + const kycInfoResp = await exchangeClient.checkKycInfo( + accessToken, + undefined, + undefined, + ); + t.assertDeepEqual(kycInfoResp.case, "ok"); + t.assertDeepEqual(kycInfoResp.body.requirements[0].form, "INFO"); + }; + + return { + challengerPostal, + challengerSms, + expectInfo, + fakeChallenger, + myTriggerMeasure, + myTriggerReset, + submitForm, + }; +} + +function isFrozen(decision: AmlDecision): boolean { + const txTypes = new Set<string>(); + for (const r of decision.limits.rules) { + if (!Amounts.isZero(r.threshold)) { + return false; + } + txTypes.add(r.operation_type); + } + if ( + !( + txTypes.has("WITHDRAW") && + txTypes.has("DEPOSIT") && + txTypes.has("AGGREGATE") && + txTypes.has("MERGE") && + txTypes.has("BALANCE") && + txTypes.has("CLOSE") && + txTypes.has("TRANSACTION") && + txTypes.has("REFUND") + ) + ) { + return false; + } + return true; +} + +async function triggerReset( + t: GlobalTestState, + args: { + officerAcc: OfficerAccount; + exchangeClient: TalerExchangeHttpClient; + merchantPaytoHash: string; + }, +): Promise<void> { + const { officerAcc, exchangeClient, merchantPaytoHash } = args; + const config = new Configuration(ConfigSources["taler-exchange"]); + config.loadFromString(topsKycRulesConf); + const rules: KycRule[] = []; + for (const secName of config.getSectionNames()) { + const rulePrefix = "kyc-rule-"; + if (!secName.toLowerCase().startsWith(rulePrefix)!) { + continue; + } + const enabled = config.getYesNo(secName, "ENABLED").required(); + if (!enabled) { + continue; + } + console.log("duration", config.getString(secName, "TIMEFRAME").required()); + rules.push({ + display_priority: 0, + measures: config + .getString(secName, "NEXT_MEASURES") + .required() + .split(/[ ]+/), + operation_type: config + .getString(secName, "OPERATION_TYPE") + .required() as LimitOperationType, + threshold: config + .getString(secName, "THRESHOLD") + .required() as AmountString, + timeframe: Duration.toTalerProtocolDuration( + Duration.fromPrettyString( + config.getString(secName, "TIMEFRAME").required(), + ), + ), + exposed: config.getYesNo(secName, "EXPOSED").required(), + is_and_combinator: config + .getYesNo(secName, "IS_AND_COMBINATIOR") + .orDefault(false), + rule_name: secName.substring(rulePrefix.length), + }); + } + succeedOrThrow( + await exchangeClient.makeAmlDesicion(officerAcc, { + decision_time: TalerProtocolTimestamp.now(), + h_payto: merchantPaytoHash, + justification: "reset", + properties: {}, + keep_investigating: false, + new_rules: { + custom_measures: {}, + expiration_time: TalerProtocolTimestamp.never(), + rules, + }, + }), + ); +} + +async function triggerMeasure( + t: GlobalTestState, + args: { + officerAcc: OfficerAccount; + exchangeClient: TalerExchangeHttpClient; + merchantPaytoHash: string; + measure: string; + }, +): Promise<{ currentDecision: AmlDecision }> { + const { officerAcc, exchangeClient, merchantPaytoHash } = args; + const decisionsResp = succeedOrThrow( + await exchangeClient.getAmlDecisions(officerAcc, { + active: true, + }), + ); + console.log(j2s(decisionsResp)); + + let toInvestigate: boolean; + let properties; + let rules: KycRule[]; + + if (decisionsResp.records.length == 0) { + toInvestigate = false; + properties = {}; + rules = []; + } else { + t.assertDeepEqual(decisionsResp.records.length, 1); + const rec = decisionsResp.records[0]; + t.assertDeepEqual(merchantPaytoHash, rec.h_payto); + toInvestigate = rec.to_investigate; + properties = rec.properties ?? {}; + rules = rec.limits.rules; + } + + succeedOrThrow( + await exchangeClient.makeAmlDesicion(officerAcc, { + decision_time: TalerProtocolTimestamp.now(), + h_payto: merchantPaytoHash, + justification: "bla", + properties: properties, + keep_investigating: toInvestigate, + new_measures: args.measure, + new_rules: { + custom_measures: {}, + expiration_time: TalerProtocolTimestamp.never(), + rules, + }, + }), + ); + + const decisionsRespAfter = succeedOrThrow( + await exchangeClient.getAmlDecisions(officerAcc, { + active: true, + }), + ); + + t.assertDeepEqual(decisionsRespAfter.records.length, 1); + return { + currentDecision: decisionsRespAfter.records[0], + }; +} + +runTopsAmlMeasuresTest.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/test-wallet-devexp-fakeprotover.ts b/packages/taler-harness/src/integrationtests/test-wallet-devexp-fakeprotover.ts @@ -77,7 +77,7 @@ export async function runWalletDevexpFakeprotoverTest(t: GlobalTestState) { logger.info("updating exchange entry after dev experiment"); - const err1 = await t.assertThrowsTalerErrorAsync( + const err1 = await t.assertThrowsTalerErrorAsyncLegacy( walletClient.call(WalletApiOperation.UpdateExchangeEntry, { exchangeBaseUrl: exchange.baseUrl, force: true, @@ -95,7 +95,7 @@ export async function runWalletDevexpFakeprotoverTest(t: GlobalTestState) { logger.info("done updating exchange entry after dev experiment"); - const err2 = await t.assertThrowsTalerErrorAsync( + const err2 = await t.assertThrowsTalerErrorAsyncLegacy( walletClient.call(WalletApiOperation.GetWithdrawalDetailsForAmount, { amount: "TESTKUDOS:10", exchangeBaseUrl: exchange.baseUrl, diff --git a/packages/taler-harness/src/integrationtests/test-wallet-insufficient-balance.ts b/packages/taler-harness/src/integrationtests/test-wallet-insufficient-balance.ts @@ -134,7 +134,7 @@ export async function runWalletInsufficientBalanceTest(t: GlobalTestState) { await wres.withdrawalFinishedCond; { - const exc = await t.assertThrowsTalerErrorAsync( + const exc = await t.assertThrowsTalerErrorAsyncLegacy( walletClient.call(WalletApiOperation.CheckDeposit, { amount: "TESTKUDOS:5" as AmountString, depositPaytoUri: "payto://x-taler-bank/localhost/foobar", @@ -172,7 +172,7 @@ export async function runWalletInsufficientBalanceTest(t: GlobalTestState) { }); await wres2.withdrawalFinishedCond; - const exc = await t.assertThrowsTalerErrorAsync( + const exc = await t.assertThrowsTalerErrorAsyncLegacy( walletClient.call(WalletApiOperation.CheckPeerPushDebit, { amount: "TESTKUDOS:20" as AmountString, }), diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-abort-bank.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-abort-bank.ts @@ -60,7 +60,7 @@ export async function runWithdrawalAbortBankTest(t: GlobalTestState) { // // WHY ?! // - const e = await t.assertThrowsTalerErrorAsync( + const e = await t.assertThrowsTalerErrorAsyncLegacy( walletClient.call(WalletApiOperation.AcceptBankIntegratedWithdrawal, { exchangeBaseUrl: exchange.baseUrl, talerWithdrawUri: wop.taler_withdraw_uri, diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-bank-integrated.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-bank-integrated.ts @@ -192,7 +192,7 @@ export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) { ); // FIXME #9683 wallet should already know withdrawal already completed by this wallet - const e = await t.assertThrowsTalerErrorAsync( + const e = await t.assertThrowsTalerErrorAsyncLegacy( wallet1.call( WalletApiOperation.AcceptBankIntegratedWithdrawal, { @@ -216,7 +216,7 @@ export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) { ); // FIXME #9683 wallet should already know withdrawal already completed by wallet1 - const e = await t.assertThrowsTalerErrorAsync( + const e = await t.assertThrowsTalerErrorAsyncLegacy( wallet2.call( WalletApiOperation.AcceptBankIntegratedWithdrawal, { diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts @@ -122,7 +122,9 @@ import { runTimetravelWithdrawTest } from "./test-timetravel-withdraw.js"; import { runTopsAmlBasicTest } from "./test-tops-aml-basic.js"; import { runTopsAmlCustomAddrPostalTest } from "./test-tops-aml-custom-addr-postal.js"; import { runTopsAmlCustomAddrSmsTest } from "./test-tops-aml-custom-addr-sms.js"; +import { runTopsAmlMeasuresTest } from "./test-tops-aml-measures.js"; import { runTopsAmlKyxNaturalTest } from "./test-tops-aml-kyx-natural.js"; +import { runTopsAmlLegiTest } from "./test-tops-aml-legi.js"; import { runTermOfServiceFormatTest } from "./test-tos-format.js"; import { runWalletBackupBasicTest } from "./test-wallet-backup-basic.js"; import { runWalletBackupDoublespendTest } from "./test-wallet-backup-doublespend.js"; @@ -326,6 +328,8 @@ const allTests: TestMainFunction[] = [ runTopsAmlCustomAddrPostalTest, runTopsAmlCustomAddrSmsTest, runTopsAmlKyxNaturalTest, + runTopsAmlMeasuresTest, + runTopsAmlLegiTest, ]; export interface TestRunSpec { diff --git a/packages/taler-util/src/http-client/exchange.ts b/packages/taler-util/src/http-client/exchange.ts @@ -48,6 +48,7 @@ import { } from "../types-taler-common.js"; import { AmlDecisionRequest, + AmlDecisionsResponse, BatchWithdrawResponse, ExchangeKycUploadFormRequest, ExchangeLegacyBatchWithdrawRequest, @@ -1041,7 +1042,7 @@ export class TalerExchangeHttpClient { * https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-measures * */ - async getAmlMesasures(auth: OfficerAccount) { + async getAmlMeasures(auth: OfficerAccount) { const url = new URL(`aml/${auth.id}/measures`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { method: "GET", @@ -1156,6 +1157,38 @@ export class TalerExchangeHttpClient { } /** + * https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-decisions + * + */ + async getAmlLegitimizations(args: { + officerAcc: OfficerAccount; + }): Promise<OperationOk<AmlDecisionsResponse>> { + const url = new URL( + `aml/${args.officerAcc.id}/legitimizations`, + this.baseUrl, + ); + + const resp = await this.httpLib.fetch(url.href, { + headers: { + "Taler-AML-Officer-Signature": encodeCrock( + signAmlQuery(args.officerAcc.signingKey), + ), + }, + }); + + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForAmlDecisionsResponse()); + case HttpStatusCode.NoContent: + return opFixedSuccess({ + records: [], + }); + default: + return opUnknownHttpFailure(resp); + } + } + + /** * https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-attributes-$H_PAYTO * */ diff --git a/packages/taler-util/src/time.ts b/packages/taler-util/src/time.ts @@ -242,12 +242,24 @@ export namespace Duration { } if (s[i] === "s") { + if (s.startsWith("seconds", i)) { + i += "seconds".length - 1; + } dMs += 1000 * Number.parseInt(currentNum, 10); } else if (s[i] === "m") { + if (s.startsWith("minutes", i)) { + i += "minutes".length - 1; + } dMs += 60 * 1000 * Number.parseInt(currentNum, 10); } else if (s[i] === "h") { + if (s.startsWith("hours", i)) { + i += "hours".length - 1; + } dMs += 60 * 60 * 1000 * Number.parseInt(currentNum, 10); } else if (s[i] === "d") { + if (s.startsWith("days", i)) { + i += "days".length - 1; + } dMs += 24 * 60 * 60 * 1000 * Number.parseInt(currentNum, 10); } else { throw Error("invalid duration, unsupported unit"); diff --git a/packages/taler-util/src/types-taler-exchange.ts b/packages/taler-util/src/types-taler-exchange.ts @@ -2068,6 +2068,29 @@ export interface AmlDecisionsResponse { records: AmlDecision[]; } +export interface LegitimizationMeasuresList { + // Legitimization measures. + measures: LegitimizationMeasureDetails; +} + +export interface LegitimizationMeasureDetails { + // Hash of the normalized payto:// URI of the account the + // measure applies to. + h_payto: HashCode; + + // Row of the measure in the exchange database. + rowid: Integer; + + // When was the measure started? + start_time: Timestamp; + + // The the actual measures. + measures: LegitimizationMeasureDetails; + + // Was this measure finished by the customer? + is_finished: boolean; +} + export interface AmlDecision { // Which payto-address is this record about. // Identifies a GNU Taler wallet or an affected bank account. @@ -2269,6 +2292,12 @@ export interface KycRule { // measure what to do next. // Default (if missing) is false. is_and_combinator?: boolean; + + // Name of the configuration section this rule + // originates from. Not available for all rules. + // Primarily informational, but also useful to + // explicitly manipulate rules by-name in AML programs. + rule_name?: string; } export interface KycAttributes { @@ -2443,31 +2472,6 @@ export interface AggregateTransferFee { sig: EddsaSignatureString; } -interface ExchangePartnerListEntry { - // Base URL of the partner exchange. - partner_base_url: string; - - // Public master key of the partner exchange. - partner_master_pub: EddsaPublicKeyString; - - // Per exchange-to-exchange transfer (wad) fee. - wad_fee: AmountString; - - // Exchange-to-exchange wad (wire) transfer frequency. - wad_frequency: RelativeTime; - - // When did this partnership begin (under these conditions)? - start_date: Timestamp; - - // How long is this partnership expected to last? - end_date: Timestamp; - - // Signature using the exchange's offline key over - // TALER_WadPartnerSignaturePS - // with purpose TALER_SIGNATURE_MASTER_PARTNER_DETAILS. - master_sig: EddsaSignatureString; -} - // Binary representation of the age groups. // The bits set in the mask mark the edges at the beginning of a next age // group. F.e. for the age groups @@ -2668,6 +2672,7 @@ export const codecForKycRules = (): Codec<KycRule> => .property("display_priority", codecForNumber()) .property("exposed", codecOptional(codecForBoolean())) .property("is_and_combinator", codecOptional(codecForBoolean())) + .property("rule_name", codecOptional(codecForString())) .build("TalerExchangeApi.KycRule"); export const codecForAmlKycAttributes = (): Codec<KycAttributes> =>