commit 084fa34900ac5303db42fd121cb461055ad2eb1e
parent a86cae90440002dfcd2528aa79c31e6168c1c80c
Author: Florian Dold <florian@dold.me>
Date: Mon, 23 Sep 2024 20:05:43 +0200
harness: expand merchant KYC test
Diffstat:
2 files changed, 232 insertions(+), 20 deletions(-)
diff --git a/packages/taler-harness/src/integrationtests/test-kyc-merchant-deposit.ts b/packages/taler-harness/src/integrationtests/test-kyc-merchant-deposit.ts
@@ -18,14 +18,22 @@
* Imports.
*/
import {
- LegacyAccountKycRedirects,
- codecForLegacyAccountKycRedirects,
+ codecForKycProcessClientInformation,
+ codecForMerchantAccountKycRedirectsResponse,
+ codecForQueryInstancesResponse,
Duration,
+ encodeCrock,
+ hashPaytoUri,
j2s,
Logger,
+ MerchantAccountKycRedirectsResponse,
TalerCorebankApiClient,
+ WireGatewayApiClient,
} from "@gnu-taler/taler-util";
-import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http";
+import {
+ readResponseJsonOrThrow,
+ readSuccessResponseJsonOrThrow,
+} from "@gnu-taler/taler-util/http";
import {
createSyncCryptoApi,
EddsaKeyPairStrings,
@@ -45,7 +53,11 @@ import {
WalletClient,
WalletService,
} from "../harness/harness.js";
-import { EnvOptions, withdrawViaBankV3 } from "../harness/helpers.js";
+import {
+ EnvOptions,
+ postAmlDecisionNoRules,
+ withdrawViaBankV3,
+} from "../harness/helpers.js";
const logger = new Logger(`test-kyc-merchant-deposit.ts`);
@@ -86,13 +98,15 @@ async function createKycTestkudosEnvironment(
let exchangeBankPassword = "mypw";
let exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername);
+ const wireGatewayApiBaseUrl = new URL(
+ `accounts/${exchangeBankUsername}/taler-wire-gateway/`,
+ bank.corebankApiBaseUrl,
+ ).href;
+
await exchange.addBankAccount("1", {
accountName: exchangeBankUsername,
accountPassword: exchangeBankPassword,
- wireGatewayApiBaseUrl: new URL(
- "accounts/exchange/taler-wire-gateway/",
- bank.baseUrl,
- ).href,
+ wireGatewayApiBaseUrl,
accountPaytoUri: exchangePaytoUri,
});
@@ -239,7 +253,7 @@ async function createKycTestkudosEnvironment(
accountName: "",
accountPassword: "",
accountPaytoUri: "",
- wireGatewayApiBaseUrl: "",
+ wireGatewayApiBaseUrl,
},
};
}
@@ -247,8 +261,36 @@ async function createKycTestkudosEnvironment(
export async function runKycMerchantDepositTest(t: GlobalTestState) {
// Set up test environment
- const { merchant, walletClient, bankClient, exchange, amlKeypair } =
- await createKycTestkudosEnvironment(t);
+ const {
+ merchant,
+ walletClient,
+ bankClient,
+ exchange,
+ exchangeBankAccount,
+ amlKeypair,
+ } = await createKycTestkudosEnvironment(t);
+
+ let accountPub: string;
+
+ {
+ const instanceUrl = new URL("private", merchant.makeInstanceBaseUrl());
+ const resp = await harnessHttpLib.fetch(instanceUrl.href);
+ const parsedResp = await readSuccessResponseJsonOrThrow(
+ resp,
+ codecForQueryInstancesResponse(),
+ );
+ accountPub = parsedResp.merchant_pub;
+ }
+
+ const wireGatewayApiClient = new WireGatewayApiClient(
+ exchangeBankAccount.wireGatewayApiBaseUrl,
+ {
+ auth: {
+ username: "admin",
+ password: "adminpw",
+ },
+ },
+ );
// Withdraw digital cash into the wallet.
@@ -261,20 +303,17 @@ export async function runKycMerchantDepositTest(t: GlobalTestState) {
await wres.withdrawalFinishedCond;
- const kycStatusUrl = new URL("private/kyc", merchant.makeInstanceBaseUrl())
- .href;
-
- let kycResp: LegacyAccountKycRedirects | undefined = undefined;
+ let kycRespOne: MerchantAccountKycRedirectsResponse | undefined = undefined;
while (1) {
+ const kycStatusUrl = new URL("private/kyc", merchant.makeInstanceBaseUrl())
+ .href;
logger.info(`requesting GET ${kycStatusUrl}`);
const resp = await harnessHttpLib.fetch(kycStatusUrl);
- logger.info(`mechant kyc status: ${resp.status}`);
if (resp.status === 200) {
- console.log(j2s(await resp.json()));
- kycResp = await readSuccessResponseJsonOrThrow(
+ kycRespOne = await readSuccessResponseJsonOrThrow(
resp,
- codecForLegacyAccountKycRedirects(),
+ codecForMerchantAccountKycRedirectsResponse(),
);
break;
}
@@ -284,7 +323,91 @@ export async function runKycMerchantDepositTest(t: GlobalTestState) {
});
}
- t.assertTrue(!!kycResp);
+ t.assertTrue(!!kycRespOne);
+
+ logger.info(`mechant kyc status: ${j2s(kycRespOne)}`);
+
+ await wireGatewayApiClient.adminAddKycauth({
+ amount: "TESTKUDOS:0.1",
+ debitAccountPayto: kycRespOne.kyc_data[0].payto_uri,
+ accountPub,
+ });
+
+ let kycRespTwo: MerchantAccountKycRedirectsResponse | undefined = undefined;
+
+ // We do this in a loop as a work-around.
+ // Not exactly the correct behavior from the merchant right now.
+ while (true) {
+ const kycStatusLongpollUrl = new URL(
+ "private/kyc",
+ merchant.makeInstanceBaseUrl(),
+ );
+ kycStatusLongpollUrl.searchParams.set("lpt", "1");
+ const resp = await harnessHttpLib.fetch(kycStatusLongpollUrl.href);
+ t.assertDeepEqual(resp.status, 200);
+ const parsedResp = await readSuccessResponseJsonOrThrow(
+ resp,
+ codecForMerchantAccountKycRedirectsResponse(),
+ );
+ logger.info(`kyc resp 2: ${j2s(parsedResp)}`);
+ if (parsedResp.kyc_data[0].payto_kycauths == null) {
+ kycRespTwo = parsedResp;
+ break;
+ }
+ // Wait 500ms
+ await new Promise<void>((resolve) => {
+ setTimeout(() => resolve(), 500);
+ });
+ }
+
+ t.assertTrue(!!kycRespTwo);
+
+ await postAmlDecisionNoRules(t, {
+ amlPriv: amlKeypair.priv,
+ amlPub: amlKeypair.pub,
+ exchangeBaseUrl: exchange.baseUrl,
+ paytoHash: encodeCrock(hashPaytoUri(kycRespTwo.kyc_data[0].payto_uri)),
+ });
+
+ // We do this in a loop as a work-around.
+ // Not exactly the correct behavior from the merchant right now.
+ while (true) {
+ const kycStatusLongpollUrl = new URL(
+ "private/kyc",
+ merchant.makeInstanceBaseUrl(),
+ );
+ kycStatusLongpollUrl.searchParams.set("lpt", "3");
+ const resp = await harnessHttpLib.fetch(kycStatusLongpollUrl.href);
+ t.assertDeepEqual(resp.status, 200);
+ const parsedResp = await readSuccessResponseJsonOrThrow(
+ resp,
+ codecForMerchantAccountKycRedirectsResponse(),
+ );
+ logger.info(`kyc resp 3: ${j2s(parsedResp)}`);
+ if ((parsedResp.kyc_data[0].limits?.length ?? 0) == 0) {
+ break;
+ }
+
+ const accessToken = parsedResp.kyc_data[0].access_token;
+
+ t.assertTrue(!!accessToken);
+
+ const infoResp = await harnessHttpLib.fetch(
+ new URL(`kyc-info/${accessToken}`, exchange.baseUrl).href,
+ );
+
+ const clientInfo = await readResponseJsonOrThrow(
+ infoResp,
+ codecForKycProcessClientInformation(),
+ );
+
+ logger.info(`kyc-info: ${j2s(clientInfo)}`);
+
+ // Wait 500ms
+ await new Promise<void>((resolve) => {
+ setTimeout(() => resolve(), 500);
+ });
+ }
}
runKycMerchantDepositTest.suites = ["wallet", "merchant", "kyc"];
diff --git a/packages/taler-util/src/types-taler-merchant.ts b/packages/taler-util/src/types-taler-merchant.ts
@@ -1313,6 +1313,75 @@ export interface QueryInstancesResponse {
};
}
+export interface MerchantAccountKycRedirectsResponse {
+ // Array of KYC status information for
+ // the exchanges and bank accounts selected
+ // by the query.
+ kyc_data: MerchantAccountKycRedirect[];
+}
+
+export interface MerchantAccountKycRedirect {
+ // Our bank wire account this is about.
+ payto_uri: string;
+
+ // Base URL of the exchange this is about.
+ exchange_url: string;
+
+ // HTTP status code returned by the exchange when we asked for
+ // information about the KYC status.
+ // Since protocol **v17**.
+ exchange_http_status: number;
+
+ // Set to true if we did not get a /keys response from
+ // the exchange and thus cannot do certain checks, such as
+ // determining default account limits or account eligibility.
+ no_keys: boolean;
+
+ // Set to true if the given account cannot to KYC at the
+ // given exchange because no wire method exists that could
+ // be used to do the KYC auth wire transfer.
+ auth_conflict: boolean;
+
+ // Numeric error code indicating errors the exchange
+ // returned, or TALER_EC_INVALID for none.
+ // Optional (as there may not always have
+ // been an error code). Since protocol **v17**.
+ exchange_code?: number;
+
+ // Access token needed to open the KYC SPA and/or
+ // access the /kyc-info/ endpoint.
+ access_token?: string;
+
+ // Array with limitations that currently apply to this
+ // account and that may be increased or lifted if the
+ // KYC check is passed.
+ // Note that additional limits *may* exist and not be
+ // communicated to the client. If such limits are
+ // reached, this *may* be indicated by the account
+ // going into aml_review state. However, it is
+ // also possible that the exchange may legally have
+ // to deny operations without being allowed to provide
+ // any justification.
+ // The limits should be used by the client to
+ // possibly structure their operations (e.g. withdraw
+ // what is possible below the limit, ask the user to
+ // pass KYC checks or withdraw the rest after the time
+ // limit is passed, warn the user to not withdraw too
+ // much or even prevent the user from generating a
+ // request that would cause it to exceed hard limits).
+ limits?: AccountLimit[];
+
+ // Array of wire transfer instructions (including
+ // optional amount and subject) for a KYC auth wire
+ // transfer. Set only if this is required
+ // to get the given exchange working.
+ // Array because the exchange may have multiple
+ // bank accounts, in which case any of these
+ // accounts will do.
+ // Optional. Since protocol **v17**.
+ payto_kycauths?: string[];
+}
+
/**
* @deprecated
*/
@@ -2988,6 +3057,26 @@ export const codecForQueryInstancesResponse =
)
.build("TalerMerchantApi.QueryInstancesResponse");
+export const codecForMerchantAccountKycRedirectsResponse =
+ (): Codec<MerchantAccountKycRedirectsResponse> =>
+ buildCodecForObject<MerchantAccountKycRedirectsResponse>()
+ .property("kyc_data", codecForList(codecForMerchantAccountKycRedirect()))
+ .build("MerchantAccountKycRedirectsResponse");
+
+export const codecForMerchantAccountKycRedirect =
+ (): Codec<MerchantAccountKycRedirect> =>
+ buildCodecForObject<MerchantAccountKycRedirect>()
+ .property("limits", codecOptional(codecForList(codecForAccountLimit())))
+ .property("exchange_url", codecForURLString())
+ .property("exchange_code", codecOptional(codecForNumber()))
+ .property("exchange_http_status", codecForNumber())
+ .property("payto_uri", codecForPaytoString())
+ .property("payto_kycauths", codecOptional(codecForList(codecForString())))
+ .property("access_token", codecOptional(codecForString()))
+ .property("auth_conflict", codecForBoolean())
+ .property("no_keys", codecForBoolean())
+ .build("MerchantAccountKycRedirect");
+
/**
* @deprecated
*/