/*
This file is part of GNU Taler
(C) 2020 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
*/
/**
* Imports.
*/
import {
codecForAccountKycRedirects,
codecForKycProcessClientInformation,
codecForQueryInstancesResponse,
Duration,
encodeCrock,
hashPaytoUri,
j2s,
Logger,
MerchantAccountKycRedirectsResponse,
TalerCorebankApiClient,
WireGatewayApiClient,
} from "@gnu-taler/taler-util";
import {
readResponseJsonOrThrow,
readSuccessResponseJsonOrThrow,
} from "@gnu-taler/taler-util/http";
import {
createSyncCryptoApi,
EddsaKeyPairStrings,
WalletApiOperation,
} from "@gnu-taler/taler-wallet-core";
import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js";
import {
BankService,
DbInfo,
ExchangeService,
getTestHarnessPaytoForLabel,
GlobalTestState,
HarnessExchangeBankAccount,
harnessHttpLib,
MerchantService,
setupDb,
WalletClient,
WalletService,
} from "../harness/harness.js";
import {
EnvOptions,
postAmlDecisionNoRules,
withdrawViaBankV3,
} from "../harness/helpers.js";
const logger = new Logger(`test-kyc-merchant-deposit.ts`);
interface KycTestEnv {
commonDb: DbInfo;
bankClient: TalerCorebankApiClient;
exchange: ExchangeService;
exchangeBankAccount: HarnessExchangeBankAccount;
walletClient: WalletClient;
walletService: WalletService;
amlKeypair: EddsaKeyPairStrings;
merchant: MerchantService;
}
async function createKycTestkudosEnvironment(
t: GlobalTestState,
coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")),
opts: EnvOptions = {},
): Promise {
const db = await setupDb(t);
const bank = await BankService.create(t, {
allowRegistrations: true,
currency: "TESTKUDOS",
database: db.connStr,
httpPort: 8082,
});
const exchange = ExchangeService.create(t, {
name: "testexchange-1",
currency: "TESTKUDOS",
httpPort: 8081,
database: db.connStr,
});
let receiverName = "Exchange";
let exchangeBankUsername = "exchange";
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,
accountPaytoUri: exchangePaytoUri,
});
bank.setSuggestedExchange(exchange, exchangePaytoUri);
await bank.start();
const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl, {
auth: {
username: "admin",
password: "adminpw",
},
});
await bankClient.registerAccountExtended({
name: receiverName,
password: exchangeBankPassword,
username: exchangeBankUsername,
is_taler_exchange: true,
payto_uri: exchangePaytoUri,
});
const ageMaskSpec = opts.ageMaskSpec;
if (ageMaskSpec) {
exchange.enableAgeRestrictions(ageMaskSpec);
// Enable age restriction for all coins.
exchange.addCoinConfigList(
coinConfig.map((x) => ({
...x,
name: `${x.name}-age`,
ageRestricted: true,
})),
);
// For mixed age restrictions, we also offer coins without age restrictions
if (opts.mixedAgeRestriction) {
exchange.addCoinConfigList(
coinConfig.map((x) => ({ ...x, ageRestricted: false })),
);
}
} else {
exchange.addCoinConfigList(coinConfig);
}
await exchange.modifyConfig(async (config) => {
config.setString("exchange", "enable_kyc", "yes");
config.setString("KYC-RULE-R1", "operation_type", "deposit");
config.setString("KYC-RULE-R1", "enabled", "yes");
config.setString("KYC-RULE-R1", "exposed", "yes");
config.setString("KYC-RULE-R1", "is_and_combinator", "yes");
config.setString("KYC-RULE-R1", "threshold", "TESTKUDOS:0");
config.setString("KYC-RULE-R1", "timeframe", "1d");
config.setString("KYC-RULE-R1", "next_measures", "M1");
config.setString("KYC-MEASURE-M1", "check_name", "C1");
config.setString("KYC-MEASURE-M1", "context", "{}");
config.setString("KYC-MEASURE-M1", "program", "P1");
config.setString("AML-PROGRAM-P1", "command", "/bin/true");
config.setString("AML-PROGRAM-P1", "enabled", "true");
config.setString("AML-PROGRAM-P1", "description", "this does nothing");
config.setString("AML-PROGRAM-P1", "fallback", "M1");
config.setString("KYC-CHECK-C1", "type", "INFO");
config.setString("KYC-CHECK-C1", "description", "my check!");
config.setString("KYC-CHECK-C1", "fallback", "M1");
});
await exchange.start();
const cryptoApi = createSyncCryptoApi();
const amlKeypair = await cryptoApi.createEddsaKeypair({});
await exchange.enableAmlAccount(amlKeypair.pub, "Alice");
const walletService = new WalletService(t, {
name: "wallet",
useInMemoryDb: true,
});
await walletService.start();
await walletService.pingUntilAvailable();
const walletClient = new WalletClient({
name: "wallet",
unixPath: walletService.socketPath,
onNotification(n) {
console.log("got notification", n);
},
});
await walletClient.connect();
await walletClient.client.call(WalletApiOperation.InitWallet, {
config: {
testing: {
skipDefaults: true,
},
},
});
const merchant = await MerchantService.create(t, {
name: "testmerchant-1",
currency: "TESTKUDOS",
httpPort: 8083,
database: db.connStr,
});
merchant.addExchange(exchange);
if (opts.additionalMerchantConfig) {
opts.additionalMerchantConfig(merchant);
}
await merchant.start();
await merchant.pingUntilAvailable();
await merchant.addInstanceWithWireAccount({
id: "default",
name: "Default Instance",
paytoUris: [getTestHarnessPaytoForLabel("merchant-default")],
defaultWireTransferDelay: Duration.toTalerProtocolDuration(
Duration.fromSpec({ minutes: 1 }),
),
});
await merchant.addInstanceWithWireAccount({
id: "minst1",
name: "minst1",
paytoUris: [getTestHarnessPaytoForLabel("minst1")],
defaultWireTransferDelay: Duration.toTalerProtocolDuration(
Duration.fromSpec({ minutes: 1 }),
),
});
console.log("setup done!");
return {
commonDb: db,
exchange,
amlKeypair,
walletClient,
walletService,
bankClient,
merchant,
exchangeBankAccount: {
accountName: "",
accountPassword: "",
accountPaytoUri: "",
wireGatewayApiBaseUrl,
},
};
}
export async function runKycMerchantDepositTest(t: GlobalTestState) {
// Set up test environment
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.
const wres = await withdrawViaBankV3(t, {
bankClient,
amount: "TESTKUDOS:50",
exchange: exchange,
walletClient: walletClient,
});
await wres.withdrawalFinishedCond;
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);
if (resp.status === 200) {
kycRespOne = await readSuccessResponseJsonOrThrow(
resp,
codecForAccountKycRedirects(),
);
break;
}
// Wait 500ms
await new Promise((resolve) => {
setTimeout(() => resolve(), 500);
});
}
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,
codecForAccountKycRedirects(),
);
logger.info(`kyc resp 2: ${j2s(parsedResp)}`);
if (parsedResp.kyc_data[0].payto_kycauths == null) {
kycRespTwo = parsedResp;
break;
}
// Wait 500ms
await new Promise((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,
codecForAccountKycRedirects(),
);
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((resolve) => {
setTimeout(() => resolve(), 500);
});
}
}
runKycMerchantDepositTest.suites = ["wallet", "merchant", "kyc"];