summaryrefslogtreecommitdiff
path: root/packages/taler-harness/src/index.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-harness/src/index.ts')
-rw-r--r--packages/taler-harness/src/index.ts1288
1 files changed, 1288 insertions, 0 deletions
diff --git a/packages/taler-harness/src/index.ts b/packages/taler-harness/src/index.ts
new file mode 100644
index 000000000..0f282e123
--- /dev/null
+++ b/packages/taler-harness/src/index.ts
@@ -0,0 +1,1288 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 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 {
+ AccessToken,
+ AmountString,
+ Amounts,
+ BalancesResponse,
+ Configuration,
+ Duration,
+ HttpStatusCode,
+ Logger,
+ PaytoString,
+ TalerAuthenticationHttpClient,
+ TalerBankConversionHttpClient,
+ TalerCoreBankHttpClient,
+ TalerErrorCode,
+ TalerMerchantInstanceHttpClient,
+ TalerMerchantManagementHttpClient,
+ TransactionsResponse,
+ createAccessToken,
+ decodeCrock,
+ encodeCrock,
+ generateIban,
+ j2s,
+ randomBytes,
+ rsaBlind,
+ setGlobalLogLevelFromString,
+ setPrintHttpRequestAsCurl,
+ stringifyPayTemplateUri,
+} from "@gnu-taler/taler-util";
+import { clk } from "@gnu-taler/taler-util/clk";
+import {
+ HttpResponse,
+ createPlatformHttpLib,
+} from "@gnu-taler/taler-util/http";
+import {
+ CryptoDispatcher,
+ SynchronousCryptoWorkerFactoryPlain,
+ WalletApiOperation,
+} from "@gnu-taler/taler-wallet-core";
+import {
+ downloadExchangeInfo,
+ topupReserveWithBank,
+} from "@gnu-taler/taler-wallet-core/dbless";
+import { deepStrictEqual } from "assert";
+import fs from "fs";
+import os from "os";
+import path from "path";
+import { runBench1 } from "./bench1.js";
+import { runBench2 } from "./bench2.js";
+import { runBench3 } from "./bench3.js";
+import { runEnvFull } from "./env-full.js";
+import { runEnv1 } from "./env1.js";
+import {
+ GlobalTestState,
+ WalletClient,
+ delayMs,
+ runTestWithState,
+} from "./harness/harness.js";
+import {
+ createSimpleTestkudosEnvironmentV2,
+ createWalletDaemonWithClient,
+} from "./harness/helpers.js";
+import { getTestInfo, runTests } from "./integrationtests/testrunner.js";
+import { lintExchangeDeployment } from "./lint.js";
+import { randomUUID } from "crypto";
+
+const logger = new Logger("taler-harness:index.ts");
+
+process.on("unhandledRejection", (error: any) => {
+ logger.error("unhandledRejection", error.message);
+ logger.error("stack", error.stack);
+ process.exit(2);
+});
+
+declare const __VERSION__: string;
+declare const __GIT_HASH__: string;
+function printVersion(): void {
+ console.log(`${__VERSION__} ${__GIT_HASH__}`);
+ process.exit(0);
+}
+
+export const testingCli = clk
+ .program("testing", {
+ help: "Command line interface for the GNU Taler test/deployment harness.",
+ })
+ .maybeOption("log", ["-L", "--log"], clk.STRING, {
+ help: "configure log level (NONE, ..., TRACE)",
+ onPresentHandler: (x) => {
+ setGlobalLogLevelFromString(x);
+ },
+ })
+ .flag("version", ["-v", "--version"], {
+ onPresentHandler: printVersion,
+ })
+ .flag("verbose", ["-V", "--verbose"], {
+ help: "Enable verbose output.",
+ });
+
+const advancedCli = testingCli.subcommand("advancedArgs", "advanced", {
+ help: "Subcommands for advanced operations (only use if you know what you're doing!).",
+});
+
+advancedCli
+ .subcommand("decode", "decode", {
+ help: "Decode base32-crockford.",
+ })
+ .action((args) => {
+ const enc = fs.readFileSync(0, "utf8");
+ console.log(decodeCrock(enc.trim()));
+ });
+
+advancedCli
+ .subcommand("bench1", "bench1", {
+ help: "Run the 'bench1' benchmark",
+ })
+ .requiredOption("configJson", ["--config-json"], clk.STRING)
+ .action(async (args) => {
+ let config: any;
+ try {
+ config = JSON.parse(args.bench1.configJson);
+ } catch (e) {
+ console.log("Could not parse config JSON");
+ }
+ await runBench1(config);
+ });
+
+advancedCli
+ .subcommand("bench2", "bench2", {
+ help: "Run the 'bench2' benchmark",
+ })
+ .requiredOption("configJson", ["--config-json"], clk.STRING)
+ .action(async (args) => {
+ let config: any;
+ try {
+ config = JSON.parse(args.bench2.configJson);
+ } catch (e) {
+ console.log("Could not parse config JSON");
+ }
+ await runBench2(config);
+ });
+
+advancedCli
+ .subcommand("bench3", "bench3", {
+ help: "Run the 'bench3' benchmark",
+ })
+ .requiredOption("configJson", ["--config-json"], clk.STRING)
+ .action(async (args) => {
+ let config: any;
+ try {
+ config = JSON.parse(args.bench3.configJson);
+ } catch (e) {
+ console.log("Could not parse config JSON");
+ }
+ await runBench3(config);
+ });
+
+advancedCli
+ .subcommand("envFull", "env-full", {
+ help: "Run a test environment for bench1",
+ })
+ .action(async (args) => {
+ const testDir = fs.mkdtempSync(path.join(os.tmpdir(), "taler-env-full-"));
+ const testState = new GlobalTestState({
+ testDir,
+ });
+ await runTestWithState(testState, runEnvFull, "env-full", true);
+ });
+
+advancedCli
+ .subcommand("env1", "env1", {
+ help: "Run a test environment for bench1",
+ })
+ .action(async (args) => {
+ const testDir = fs.mkdtempSync(path.join(os.tmpdir(), "taler-env1-"));
+ const testState = new GlobalTestState({
+ testDir,
+ });
+ await runTestWithState(testState, runEnv1, "env1", true);
+ });
+
+async function doDbChecks(
+ t: GlobalTestState,
+ walletClient: WalletClient,
+ indir: string,
+): Promise<void> {
+ // Check that balance didn't break
+ const balPath = `${indir}/wallet-balances.json`;
+ const expectedBal: BalancesResponse = JSON.parse(
+ fs.readFileSync(balPath, { encoding: "utf8" }),
+ ) as BalancesResponse;
+ const actualBal = await walletClient.call(WalletApiOperation.GetBalances, {});
+ t.assertDeepEqual(actualBal.balances.length, expectedBal.balances.length);
+
+ // Check that transactions didn't break
+ const txnPath = `${indir}/wallet-transactions.json`;
+ const expectedTxn: TransactionsResponse = JSON.parse(
+ fs.readFileSync(txnPath, { encoding: "utf8" }),
+ ) as TransactionsResponse;
+ const actualTxn = await walletClient.call(
+ WalletApiOperation.GetTransactions,
+ { includeRefreshes: true },
+ );
+ t.assertDeepEqual(
+ actualTxn.transactions.length,
+ expectedTxn.transactions.length,
+ );
+}
+
+advancedCli
+ .subcommand("walletDbcheck", "wallet-dbcheck", {
+ help: "Check a wallet database (used for migration testing).",
+ })
+ .requiredArgument("indir", clk.STRING)
+ .action(async (args) => {
+ const indir = args.walletDbcheck.indir;
+ if (!fs.existsSync(indir)) {
+ throw Error("directory to be checked does not exist");
+ }
+
+ const testRootDir = fs.mkdtempSync(path.join(os.tmpdir(), "taler-dbchk-"));
+ const t: GlobalTestState = new GlobalTestState({
+ testDir: testRootDir,
+ });
+ const origWalletDbPath = `${indir}/wallet-db.sqlite3`;
+ const testWalletDbPath = `${testRootDir}/wallet-testdb.sqlite3`;
+ fs.cpSync(origWalletDbPath, testWalletDbPath);
+ if (!fs.existsSync(origWalletDbPath)) {
+ throw new Error("wallet db to be checked does not exist");
+ }
+ const { walletClient, walletService } = await createWalletDaemonWithClient(
+ t,
+ { name: "wallet-loaded", overrideDbPath: testWalletDbPath },
+ );
+
+ await walletService.pingUntilAvailable();
+
+ // Do DB checks with the DB we loaded.
+ await doDbChecks(t, walletClient, indir);
+
+ const {
+ walletClient: freshWalletClient,
+ walletService: freshWalletService,
+ } = await createWalletDaemonWithClient(t, {
+ name: "wallet-fresh",
+ persistent: false,
+ });
+
+ await freshWalletService.pingUntilAvailable();
+
+ // Check that we can still import the backup JSON.
+
+ const backupPath = `${indir}/wallet-backup.json`;
+ const backupData = JSON.parse(
+ fs.readFileSync(backupPath, { encoding: "utf8" }),
+ );
+ await freshWalletClient.call(WalletApiOperation.ImportDb, {
+ dump: backupData,
+ });
+
+ // Repeat same checks with wallet that we restored from backup
+ // instead of from the DB file.
+ await doDbChecks(t, freshWalletClient, indir);
+
+ await t.shutdown();
+ });
+
+advancedCli
+ .subcommand("walletDbgen", "wallet-dbgen", {
+ help: "Generate a wallet test database (to be used for migration testing).",
+ })
+ .requiredArgument("outdir", clk.STRING)
+ .action(async (args) => {
+ const outdir = args.walletDbgen.outdir;
+ if (fs.existsSync(outdir)) {
+ throw new Error("outdir already exists, please delete first");
+ }
+ fs.mkdirSync(outdir, {
+ recursive: true,
+ });
+
+ const testRootDir = fs.mkdtempSync(path.join(os.tmpdir(), "taler-dbgen-"));
+ console.log(`generating data in ${testRootDir}`);
+ const t = new GlobalTestState({
+ testDir: testRootDir,
+ });
+ const { walletClient, walletService, bank, exchange, merchant } =
+ await createSimpleTestkudosEnvironmentV2(t);
+ await walletClient.call(WalletApiOperation.RunIntegrationTestV2, {
+ amountToSpend: "TESTKUDOS:5" as AmountString,
+ amountToWithdraw: "TESTKUDOS:10" as AmountString,
+ corebankApiBaseUrl: bank.corebankApiBaseUrl,
+ exchangeBaseUrl: exchange.baseUrl,
+ merchantBaseUrl: merchant.makeInstanceBaseUrl(),
+ });
+ await walletClient.call(
+ WalletApiOperation.TestingWaitTransactionsFinal,
+ {},
+ );
+
+ const transactionsJson = await walletClient.call(
+ WalletApiOperation.GetTransactions,
+ {
+ includeRefreshes: true,
+ },
+ );
+
+ const balancesJson = await walletClient.call(
+ WalletApiOperation.GetBalances,
+ {},
+ );
+
+ const backupJson = await walletClient.call(WalletApiOperation.ExportDb, {});
+
+ const versionJson = await walletClient.call(
+ WalletApiOperation.GetVersion,
+ {},
+ );
+
+ await walletService.stop();
+
+ await t.shutdown();
+
+ console.log(`generated data in ${testRootDir}`);
+
+ fs.copyFileSync(walletService.dbPath, `${outdir}/wallet-db.sqlite3`);
+ fs.writeFileSync(
+ `${outdir}/wallet-transactions.json`,
+ j2s(transactionsJson),
+ );
+ fs.writeFileSync(`${outdir}/wallet-balances.json`, j2s(balancesJson));
+ fs.writeFileSync(`${outdir}/wallet-backup.json`, j2s(backupJson));
+ fs.writeFileSync(`${outdir}/wallet-version.json`, j2s(versionJson));
+ fs.writeFileSync(
+ `${outdir}/meta.json`,
+ j2s({
+ timestamp: new Date(),
+ }),
+ );
+ });
+
+const configCli = testingCli.subcommand("configArgs", "config", {
+ help: "Subcommands for handling the Taler configuration.",
+});
+
+configCli.subcommand("show", "show").action(async (args) => {
+ const config = Configuration.load();
+ const cfgStr = config.stringify({
+ diagnostics: true,
+ });
+ console.log(cfgStr);
+});
+
+configCli
+ .subcommand("get", "get")
+ .requiredArgument("section", clk.STRING)
+ .requiredArgument("option", clk.STRING)
+ .flag("file", ["-f"])
+ .action(async (args) => {
+ const config = Configuration.load();
+ let res;
+ if (args.get.file) {
+ res = config.getPath(args.get.section, args.get.option);
+ } else {
+ res = config.getString(args.get.section, args.get.option);
+ }
+ if (res.isDefined()) {
+ console.log(res.required());
+ } else {
+ console.warn("not found");
+ process.exit(1);
+ }
+ });
+
+const deploymentCli = testingCli.subcommand("deploymentArgs", "deployment", {
+ help: "Subcommands for handling GNU Taler deployments.",
+});
+
+deploymentCli
+ .subcommand("testTalerdotnetDemo", "test-demodottalerdotnet")
+ .action(async (args) => {
+ const http = createPlatformHttpLib();
+ const cryptiDisp = new CryptoDispatcher(
+ new SynchronousCryptoWorkerFactoryPlain(),
+ );
+ const cryptoApi = cryptiDisp.cryptoApi;
+ const reserveKeyPair = await cryptoApi.createEddsaKeypair({});
+ const exchangeBaseUrl = "https://exchange.demo.taler.net/";
+ const exchangeInfo = await downloadExchangeInfo(exchangeBaseUrl, http);
+ await topupReserveWithBank({
+ amount: "KUDOS:10" as AmountString,
+ corebankApiBaseUrl: "https://bank.demo.taler.net/",
+ exchangeInfo,
+ http,
+ reservePub: reserveKeyPair.pub,
+ });
+ let reserveUrl = new URL(`reserves/${reserveKeyPair.pub}`, exchangeBaseUrl);
+ reserveUrl.searchParams.set("timeout_ms", "30000");
+ console.log("requesting", reserveUrl.href);
+ const longpollReq = http.fetch(reserveUrl.href, {
+ method: "GET",
+ });
+ const reserveStatusResp = await longpollReq;
+ console.log("reserve status", reserveStatusResp.status);
+ });
+
+deploymentCli
+ .subcommand("testDemoTestdotdalerdotnet", "test-testdottalerdotnet")
+ .action(async (args) => {
+ const http = createPlatformHttpLib();
+ const cryptiDisp = new CryptoDispatcher(
+ new SynchronousCryptoWorkerFactoryPlain(),
+ );
+ const cryptoApi = cryptiDisp.cryptoApi;
+ const reserveKeyPair = await cryptoApi.createEddsaKeypair({});
+ const exchangeBaseUrl = "https://exchange.test.taler.net/";
+ const exchangeInfo = await downloadExchangeInfo(exchangeBaseUrl, http);
+ await topupReserveWithBank({
+ amount: "TESTKUDOS:10" as AmountString,
+ corebankApiBaseUrl: "https://bank.test.taler.net/",
+ exchangeInfo,
+ http,
+ reservePub: reserveKeyPair.pub,
+ });
+ let reserveUrl = new URL(`reserves/${reserveKeyPair.pub}`, exchangeBaseUrl);
+ reserveUrl.searchParams.set("timeout_ms", "30000");
+ console.log("requesting", reserveUrl.href);
+ const longpollReq = http.fetch(reserveUrl.href, {
+ method: "GET",
+ });
+ const reserveStatusResp = await longpollReq;
+ console.log("reserve status", reserveStatusResp.status);
+ });
+
+deploymentCli
+ .subcommand("testLocalhostDemo", "test-demo-localhost")
+ .action(async (args) => {
+ // Run checks against the "env-full" demo deployment on localhost
+ const http = createPlatformHttpLib();
+ const cryptiDisp = new CryptoDispatcher(
+ new SynchronousCryptoWorkerFactoryPlain(),
+ );
+ const cryptoApi = cryptiDisp.cryptoApi;
+ const reserveKeyPair = await cryptoApi.createEddsaKeypair({});
+ const exchangeBaseUrl = "http://localhost:8081/";
+ const exchangeInfo = await downloadExchangeInfo(exchangeBaseUrl, http);
+ await topupReserveWithBank({
+ amount: "TESTKUDOS:10" as AmountString,
+ corebankApiBaseUrl: "http://localhost:8082/taler-bank-access/",
+ exchangeInfo,
+ http,
+ reservePub: reserveKeyPair.pub,
+ });
+ let reserveUrl = new URL(`reserves/${reserveKeyPair.pub}`, exchangeBaseUrl);
+ reserveUrl.searchParams.set("timeout_ms", "30000");
+ console.log("requesting", reserveUrl.href);
+ const longpollReq = http.fetch(reserveUrl.href, {
+ method: "GET",
+ });
+ const reserveStatusResp = await longpollReq;
+ console.log("reserve status", reserveStatusResp.status);
+ });
+
+deploymentCli
+ .subcommand("lintExchange", "lint-exchange", {
+ help: "Run checks on the exchange deployment.",
+ })
+ .flag("cont", ["--continue"], {
+ help: "Continue after errors if possible",
+ })
+ .flag("debug", ["--debug"], {
+ help: "Output extra debug info",
+ })
+ .action(async (args) => {
+ await lintExchangeDeployment(
+ args.lintExchange.debug,
+ args.lintExchange.cont,
+ );
+ });
+
+deploymentCli
+ .subcommand("waitService", "wait-taler-service", {
+ help: "Wait for the config endpoint of a Taler-style service to be available",
+ })
+ .requiredArgument("serviceName", clk.STRING)
+ .requiredArgument("serviceConfigUrl", clk.STRING)
+ .action(async (args) => {
+ const serviceName = args.waitService.serviceName;
+ const serviceUrl = args.waitService.serviceConfigUrl;
+ console.log(
+ `Waiting for service ${serviceName} to be ready at ${serviceUrl}`,
+ );
+ const httpLib = createPlatformHttpLib();
+ while (1) {
+ console.log(`Fetching ${serviceUrl}`);
+ let resp: HttpResponse;
+ try {
+ resp = await httpLib.fetch(serviceUrl);
+ } catch (e) {
+ console.log(
+ `Got network error for service ${serviceName} at ${serviceUrl}`,
+ );
+ await delayMs(1000);
+ continue;
+ }
+ if (resp.status != 200) {
+ console.log(
+ `Got unexpected status ${resp.status} for service at ${serviceUrl}`,
+ );
+ await delayMs(1000);
+ continue;
+ }
+ let respJson: any;
+ try {
+ respJson = await resp.json();
+ } catch (e) {
+ console.log(
+ `Got json error for service ${serviceName} at ${serviceUrl}`,
+ );
+ await delayMs(1000);
+ continue;
+ }
+ const recServiceName = respJson.name;
+ console.log(`Got name ${recServiceName}`);
+ if (recServiceName != serviceName) {
+ console.log(`A different service is still running at ${serviceUrl}`);
+ await delayMs(1000);
+ continue;
+ }
+ console.log(`service ${serviceName} at ${serviceUrl} is now available`);
+ return;
+ }
+ });
+
+deploymentCli
+ .subcommand("waitEndpoint", "wait-endpoint", {
+ help: "Wait for an endpoint to return an HTTP 200 Ok status with JSON body",
+ })
+ .requiredArgument("serviceEndpoint", clk.STRING)
+ .action(async (args) => {
+ const serviceUrl = args.waitEndpoint.serviceEndpoint;
+ console.log(`Waiting for endpoint ${serviceUrl} to be ready`);
+ const httpLib = createPlatformHttpLib();
+ while (1) {
+ console.log(`Fetching ${serviceUrl}`);
+ let resp: HttpResponse;
+ try {
+ resp = await httpLib.fetch(serviceUrl);
+ } catch (e) {
+ console.log(`Got network error for service at ${serviceUrl}`);
+ await delayMs(1000);
+ continue;
+ }
+ if (resp.status != 200) {
+ console.log(
+ `Got unexpected status ${resp.status} for service at ${serviceUrl}`,
+ );
+ await delayMs(1000);
+ continue;
+ }
+ let respJson: any;
+ try {
+ respJson = await resp.json();
+ } catch (e) {
+ console.log(`Got json error for service at ${serviceUrl}`);
+ await delayMs(1000);
+ continue;
+ }
+ return;
+ }
+ });
+
+deploymentCli
+ .subcommand("genIban", "gen-iban", {
+ help: "Generate a random IBAN.",
+ })
+ .requiredArgument("countryCode", clk.STRING)
+ .requiredArgument("length", clk.INT)
+ .action(async (args) => {
+ console.log(generateIban(args.genIban.countryCode, args.genIban.length));
+ });
+
+deploymentCli
+ .subcommand("provisionBankMerchant", "provision-bank-and-merchant", {
+ help: "Provision a bank account, merchant instance and link them together.",
+ })
+ .requiredArgument("merchantApiBaseUrl", clk.STRING, {
+ help: "URL location of the merchant backend",
+ })
+ .requiredArgument("corebankApiBaseUrl", clk.STRING, {
+ help: "URL location of the libeufin bank backend",
+ })
+ .requiredOption(
+ "merchantToken",
+ ["--merchant-management-token"],
+ clk.STRING,
+ {
+ help: "access token of the default instance in the merchant backend",
+ },
+ )
+ .maybeOption("bankToken", ["--bank-admin-token"], clk.STRING, {
+ help: "libeufin bank admin's token if the account creation is restricted",
+ })
+ .maybeOption("bankPassword", ["--bank-admin-password"], clk.STRING, {
+ help: "libeufin bank admin's password if the account creation is restricted, it will override --bank-admin-token",
+ })
+ .requiredOption("name", ["--legal-name"], clk.STRING, {
+ help: "legal name of the merchant",
+ })
+ .maybeOption("email", ["--email"], clk.STRING, {
+ help: "email contact of the merchant",
+ })
+ .maybeOption("phone", ["--phone"], clk.STRING, {
+ help: "phone contact of the merchant",
+ })
+ .requiredOption("id", ["--id"], clk.STRING, {
+ help: "login id for the bank account and instance id of the merchant backend",
+ })
+ .flag("template", ["--create-template"], {
+ help: "use this flag to create a default template for the merchant with fixed summary",
+ })
+ .requiredOption("password", ["--password"], clk.STRING, {
+ help: "password of the accounts in libeufin bank and merchant backend",
+ })
+ .flag("randomPassword", ["--set-random-password"], {
+ help: "if everything worked ok, change the password of the accounts at the end",
+ })
+ .action(async (args) => {
+ const managementToken = createAccessToken(
+ args.provisionBankMerchant.merchantToken,
+ );
+ const bankAdminPassword = args.provisionBankMerchant.bankPassword;
+ const bankAdminTokenArg = args.provisionBankMerchant.bankToken
+ ? createAccessToken(args.provisionBankMerchant.bankToken)
+ : undefined;
+ const id = args.provisionBankMerchant.id;
+ const name = args.provisionBankMerchant.name;
+ const email = args.provisionBankMerchant.email;
+ const phone = args.provisionBankMerchant.phone;
+ const password = args.provisionBankMerchant.password;
+
+ const httpLib = createPlatformHttpLib({});
+ const merchantManager = new TalerMerchantManagementHttpClient(
+ args.provisionBankMerchant.merchantApiBaseUrl,
+ httpLib,
+ );
+ const bank = new TalerCoreBankHttpClient(
+ args.provisionBankMerchant.corebankApiBaseUrl,
+ httpLib,
+ );
+ const instanceURL = merchantManager.getSubInstanceAPI(id).href;
+ const merchantInstance = new TalerMerchantInstanceHttpClient(
+ instanceURL,
+ httpLib,
+ );
+ const conv = new TalerBankConversionHttpClient(
+ bank.getConversionInfoAPI().href,
+ httpLib,
+ );
+ const bankAuth = new TalerAuthenticationHttpClient(
+ bank.getAuthenticationAPI(id).href,
+ httpLib,
+ );
+
+ const bc = await bank.getConfig();
+ if (bc.type === "fail") {
+ logger.error(`couldn't get bank config. ${bc.detail.hint}`);
+ return;
+ }
+ if (!bank.isCompatible(bc.body.version)) {
+ logger.error(
+ `bank server version is not compatible: ${bc.body.version}, client version: ${bank.PROTOCOL_VERSION}`,
+ );
+ return;
+ }
+ const mc = await merchantManager.getConfig();
+ if (mc.type === "fail") {
+ logger.error(`couldn't get merchant config. ${mc.detail.hint}`);
+ return;
+ }
+ if (!merchantManager.isCompatible(mc.body.version)) {
+ logger.error(
+ `merchant server version is not compatible: ${mc.body.version}, client version: ${merchantManager.PROTOCOL_VERSION}`,
+ );
+ return;
+ }
+
+ let bankAdminToken: AccessToken | undefined;
+ if (bankAdminPassword) {
+ const adminAuth = new TalerAuthenticationHttpClient(
+ bank.getAuthenticationAPI("admin").href,
+ httpLib,
+ );
+
+ const resp = await adminAuth.createAccessTokenBasic(
+ "admin",
+ bankAdminPassword,
+ {
+ scope: "write",
+ duration: {
+ d_us: 1000 * 1000 * 10, //10 secs
+ },
+ refreshable: false,
+ },
+ );
+ if (resp.type === "fail") {
+ logger.error(`could not get bank admin token from password.`);
+ return;
+ }
+ bankAdminToken = resp.body.access_token;
+ } else {
+ bankAdminToken = bankAdminTokenArg;
+ }
+
+ /**
+ * create bank account
+ */
+ let accountPayto: PaytoString;
+ {
+ const resp = await bank.createAccount(bankAdminToken, {
+ name: name,
+ password: password,
+ username: id,
+ contact_data:
+ email || phone
+ ? {
+ email: email,
+ phone: phone,
+ }
+ : undefined,
+ });
+
+ if (resp.type === "fail") {
+ logger.error(
+ `unable to provision bank account, HTTP response status ${resp.case}`,
+ );
+ process.exit(2);
+ }
+ logger.info(`account ${id} successfully provisioned`);
+ accountPayto = resp.body.internal_payto_uri;
+ }
+
+ /**
+ * create merchant account
+ */
+ {
+ const resp = await merchantManager.createInstance(managementToken, {
+ address: {},
+ auth: {
+ method: "token",
+ token: createAccessToken(password),
+ },
+ default_pay_delay: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ hours: 1 }),
+ ),
+ default_wire_transfer_delay: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ hours: 1 }),
+ ),
+ id: id,
+ jurisdiction: {},
+ name: name,
+ use_stefan: true,
+ });
+
+ if (resp.type === "ok") {
+ logger.info(`instance ${id} created successfully`);
+ } else if (resp.case === HttpStatusCode.Conflict) {
+ logger.info(`instance ${id} already exists`);
+ } else {
+ logger.error(
+ `unable to create instance ${id}, HTTP status ${resp.case}`,
+ );
+ process.exit(2);
+ }
+ }
+
+ let wireAccount: string;
+ /**
+ * link bank account and merchant
+ */
+ {
+ const resp = await merchantInstance.addBankAccount(
+ createAccessToken(password),
+ {
+ payto_uri: accountPayto,
+ credit_facade_url: bank.getRevenueAPI(id).href,
+ credit_facade_credentials: {
+ type: "basic",
+ username: id,
+ password: password,
+ },
+ },
+ );
+ if (resp.type === "fail") {
+ console.error(
+ `unable to configure bank account for instance ${id}, status ${resp.case}`,
+ );
+ console.error(j2s(resp.detail));
+ process.exit(2);
+ }
+ wireAccount = resp.body.h_wire;
+ }
+
+ logger.info(`successfully configured bank account for ${id}`);
+
+ let templateURI;
+ /**
+ * create template
+ */
+ if (args.provisionBankMerchant.template) {
+ let currency = bc.body.currency;
+ if (bc.body.allow_conversion) {
+ const cc = await conv.getConfig();
+ if (cc.type === "ok") {
+ currency = cc.body.fiat_currency;
+ } else {
+ console.error(`could not get fiat currency status ${cc.case}`);
+ console.error(j2s(cc.detail));
+ }
+ } else {
+ console.log(`conversion is disabled, using bank currency`);
+ }
+
+ {
+ const resp = await merchantInstance.addTemplate(
+ createAccessToken(password),
+ {
+ template_id: "default",
+ template_description: "First template",
+ template_contract: {
+ pay_duration: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ hours: 1 }),
+ ),
+ minimum_age: 0,
+ currency,
+ summary: "Pay me!",
+ },
+ },
+ );
+ if (resp.type === "fail") {
+ console.error(
+ `unable to create template for insntaince ${id}, status ${resp.case}`,
+ );
+ console.error(j2s(resp.detail));
+ process.exit(2);
+ }
+ }
+
+ logger.info(`template default successfully created`);
+ templateURI = stringifyPayTemplateUri({
+ merchantBaseUrl: instanceURL,
+ templateId: "default",
+ templateParams: {
+ amount: currency,
+ },
+ });
+ }
+
+ let finalPassword = password;
+ if (args.provisionBankMerchant.randomPassword) {
+ const prevPassword = password;
+ const randomPassword = encodeCrock(randomBytes(16));
+ logger.info("random password: ", randomPassword);
+ let token: AccessToken;
+ {
+ const resp = await bankAuth.createAccessTokenBasic(id, prevPassword, {
+ scope: "readwrite",
+ duration: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ minutes: 1 }),
+ ),
+ refreshable: false,
+ });
+ if (resp.type === "fail") {
+ console.error(
+ `unable to login into bank accountfor user ${id}, status ${resp.case}`,
+ );
+ console.error(j2s(resp.detail));
+ process.exit(2);
+ }
+ token = resp.body.access_token;
+ }
+
+ {
+ const resp = await bank.updatePassword(
+ { username: id, token },
+ {
+ old_password: prevPassword,
+ new_password: randomPassword,
+ },
+ );
+ if (resp.type === "fail") {
+ console.error(
+ `unable to change bank password for user ${id}, status ${resp.case}`,
+ );
+ if (resp.case !== HttpStatusCode.Accepted) {
+ console.error(j2s(resp.detail));
+ } else {
+ console.error("2FA required");
+ }
+ process.exit(2);
+ }
+ }
+
+ {
+ const resp = await merchantInstance.updateCurrentInstanceAuthentication(
+ createAccessToken(prevPassword),
+ {
+ method: "token",
+ token: createAccessToken(randomPassword),
+ },
+ );
+ if (resp.type === "fail") {
+ console.error(
+ `unable to change merchant password for instance ${id}, status ${resp.case}`,
+ );
+ console.error(j2s(resp.detail));
+ process.exit(2);
+ }
+ }
+
+ {
+ const resp = await merchantInstance.updateBankAccount(
+ createAccessToken(randomPassword),
+ wireAccount,
+ {
+ credit_facade_url: bank.getRevenueAPI(id).href,
+ credit_facade_credentials: {
+ type: "basic",
+ username: id,
+ password: randomPassword,
+ },
+ },
+ );
+ if (resp.type != "ok") {
+ console.error(
+ `unable to update bank account for instance ${id}, status ${resp.case}`,
+ );
+ console.error(j2s(resp.detail));
+ process.exit(2);
+ }
+ }
+ finalPassword = randomPassword;
+ }
+ logger.info(`successfully configured bank account for ${id}`);
+
+ /**
+ * show result
+ */
+ console.log(
+ JSON.stringify(
+ {
+ bankUser: id,
+ bankURL: args.provisionBankMerchant.corebankApiBaseUrl,
+ merchantURL: instanceURL,
+ templateURI,
+ password: finalPassword,
+ },
+ undefined,
+ 2,
+ ),
+ );
+ });
+
+deploymentCli
+ .subcommand("provisionMerchantInstance", "provision-merchant-instance", {
+ help: "Provision a merchant backend instance.",
+ })
+ .requiredArgument("merchantApiBaseUrl", clk.STRING)
+ .requiredOption("managementToken", ["--management-token"], clk.STRING)
+ .requiredOption("instanceToken", ["--instance-token"], clk.STRING)
+ .requiredOption("name", ["--name"], clk.STRING)
+ .requiredOption("id", ["--id"], clk.STRING)
+ .requiredOption("payto", ["--payto"], clk.STRING)
+ .maybeOption("bankURL", ["--bankURL"], clk.STRING)
+ .maybeOption("bankUser", ["--bankUser"], clk.STRING)
+ .maybeOption("bankPassword", ["--bankPassword"], clk.STRING)
+ .action(async (args) => {
+ const httpLib = createPlatformHttpLib({});
+ const baseUrl = args.provisionMerchantInstance.merchantApiBaseUrl;
+ const api = new TalerMerchantManagementHttpClient(baseUrl, httpLib);
+ const managementToken = createAccessToken(
+ args.provisionMerchantInstance.managementToken,
+ );
+ const instanceToken = createAccessToken(
+ args.provisionMerchantInstance.instanceToken,
+ );
+ const instanceId = args.provisionMerchantInstance.id;
+ const instancceName = args.provisionMerchantInstance.name;
+ const bankURL = args.provisionMerchantInstance.bankURL;
+ const bankUser = args.provisionMerchantInstance.bankUser;
+ const bankPassword = args.provisionMerchantInstance.bankPassword;
+ const accountPayto = args.provisionMerchantInstance.payto as PaytoString;
+
+ const createResp = await api.createInstance(managementToken, {
+ address: {},
+ auth: {
+ method: "token",
+ token: instanceToken,
+ },
+ default_pay_delay: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ hours: 1 }),
+ ),
+ default_wire_transfer_delay: { d_us: 1 },
+ id: instanceId,
+ jurisdiction: {},
+ name: instancceName,
+ use_stefan: true,
+ });
+
+ if (createResp.type === "ok") {
+ logger.info(`instance ${instanceId} created successfully`);
+ } else if (createResp.case === HttpStatusCode.Conflict) {
+ logger.info(`instance ${instanceId} already exists`);
+ } else {
+ logger.error(
+ `unable to create instance ${instanceId}, HTTP status ${createResp.case}`,
+ );
+ process.exit(2);
+ }
+
+ const createAccountResp = await api.addBankAccount(instanceToken, {
+ payto_uri: accountPayto,
+ credit_facade_url: bankURL,
+ credit_facade_credentials:
+ bankUser && bankPassword
+ ? {
+ type: "basic",
+ username: bankUser,
+ password: bankPassword,
+ }
+ : undefined,
+ });
+ if (createAccountResp.type != "ok") {
+ console.error(
+ `unable to configure bank account for instance ${instanceId}, status ${createAccountResp.case}`,
+ );
+ console.error(j2s(createAccountResp.detail));
+ process.exit(2);
+ }
+ logger.info(`successfully configured bank account for ${instanceId}`);
+ });
+
+deploymentCli
+ .subcommand("provisionBankAccount", "provision-bank-account", {
+ help: "Provision a corebank account.",
+ })
+ .requiredArgument("corebankApiBaseUrl", clk.STRING)
+ .flag("exchange", ["--exchange"])
+ .flag("public", ["--public"])
+ .requiredOption("login", ["--login"], clk.STRING)
+ .requiredOption("name", ["--name"], clk.STRING)
+ .requiredOption("password", ["--password"], clk.STRING)
+ .maybeOption("internalPayto", ["--payto"], clk.STRING)
+ .action(async (args) => {
+ const httpLib = createPlatformHttpLib();
+ const baseUrl = args.provisionBankAccount.corebankApiBaseUrl;
+ const api = new TalerCoreBankHttpClient(baseUrl, httpLib);
+
+ const accountLogin = args.provisionBankAccount.login;
+ const resp = await api.createAccount(undefined, {
+ name: args.provisionBankAccount.name,
+ password: args.provisionBankAccount.password,
+ username: accountLogin,
+ is_public: !!args.provisionBankAccount.public,
+ is_taler_exchange: !!args.provisionBankAccount.exchange,
+ payto_uri: args.provisionBankAccount.internalPayto as PaytoString,
+ });
+
+ if (resp.type === "ok") {
+ logger.info(`account ${accountLogin} successfully provisioned`);
+ return;
+ }
+ logger.error(
+ `unable to provision bank account, HTTP response status ${resp.case}`,
+ );
+ process.exit(2);
+ });
+
+deploymentCli
+ .subcommand("coincfg", "gen-coin-config", {
+ help: "Generate a coin/denomination configuration for the exchange.",
+ })
+ .requiredOption("minAmount", ["--min-amount"], clk.STRING, {
+ help: "Smallest denomination",
+ })
+ .requiredOption("maxAmount", ["--max-amount"], clk.STRING, {
+ help: "Largest denomination",
+ })
+ .flag("noFees", ["--no-fees"])
+ .action(async (args) => {
+ let out = "";
+
+ const stamp = Math.floor(new Date().getTime() / 1000);
+
+ const min = Amounts.parseOrThrow(args.coincfg.minAmount);
+ const max = Amounts.parseOrThrow(args.coincfg.maxAmount);
+ if (min.currency != max.currency) {
+ console.error("currency mismatch");
+ process.exit(1);
+ }
+ const currency = min.currency;
+ let x = min;
+ let n = 1;
+
+ out += "# Coin configuration for the exchange.\n";
+ out += '# Should be placed in "/etc/taler/conf.d/exchange-coins.conf".\n';
+ out += "\n";
+
+ while (Amounts.cmp(x, max) < 0) {
+ out += `[COIN-${currency}-n${n}-t${stamp}]\n`;
+ out += `VALUE = ${Amounts.stringify(x)}\n`;
+ out += `DURATION_WITHDRAW = 7 days\n`;
+ out += `DURATION_SPEND = 2 years\n`;
+ out += `DURATION_LEGAL = 6 years\n`;
+ out += `FEE_WITHDRAW = ${currency}:0\n`;
+ if (args.coincfg.noFees) {
+ out += `FEE_DEPOSIT = ${currency}:0\n`;
+ } else {
+ out += `FEE_DEPOSIT = ${Amounts.stringify(min)}\n`;
+ }
+ out += `FEE_REFRESH = ${currency}:0\n`;
+ out += `FEE_REFUND = ${currency}:0\n`;
+ out += `RSA_KEYSIZE = 2048\n`;
+ out += `CIPHER = RSA\n`;
+ out += "\n";
+ x = Amounts.add(x, x).amount;
+ n++;
+ }
+
+ console.log(out);
+ });
+
+const deploymentConfigCli = deploymentCli.subcommand("configArgs", "config", {
+ help: "Subcommands the Taler configuration.",
+});
+
+deploymentConfigCli
+ .subcommand("show", "show")
+ .flag("diagnostics", ["-d", "--diagnostics"])
+ .maybeArgument("cfgfile", clk.STRING, {})
+ .action(async (args) => {
+ const cfg = Configuration.load(args.show.cfgfile);
+ console.log(
+ cfg.stringify({
+ diagnostics: args.show.diagnostics,
+ }),
+ );
+ });
+
+testingCli.subcommand("logtest", "logtest").action(async (args) => {
+ logger.trace("This is a trace message.");
+ logger.info("This is an info message.");
+ logger.warn("This is an warning message.");
+ logger.error("This is an error message.");
+});
+
+testingCli
+ .subcommand("listIntegrationtests", "list-integrationtests")
+ .action(async (args) => {
+ for (const t of getTestInfo()) {
+ let s = t.name;
+ if (t.suites.length > 0) {
+ s += ` (suites: ${t.suites.join(",")})`;
+ }
+ if (t.experimental) {
+ s += ` [experimental]`;
+ }
+ console.log(s);
+ }
+ });
+
+testingCli
+ .subcommand("runIntegrationtests", "run-integrationtests")
+ .maybeArgument("pattern", clk.STRING, {
+ help: "Glob pattern to select which tests to run",
+ })
+ .maybeOption("suites", ["--suites"], clk.STRING, {
+ help: "Only run selected suites (comma-separated list)",
+ })
+ .flag("dryRun", ["--dry"], {
+ help: "Only print tests that will be selected to run.",
+ })
+ .flag("experimental", ["--experimental"], {
+ help: "Include tests marked as experimental",
+ })
+ .flag("failFast", ["--fail-fast"], {
+ help: "Exit after the first error",
+ })
+ .flag("waitOnFail", ["--wait-on-fail"], {
+ help: "Exit after the first error",
+ })
+ .flag("quiet", ["--quiet"], {
+ help: "Produce less output.",
+ })
+ .flag("noTimeout", ["--no-timeout"], {
+ help: "Do not time out tests.",
+ })
+ .action(async (args) => {
+ await runTests({
+ includePattern: args.runIntegrationtests.pattern,
+ failFast: args.runIntegrationtests.failFast,
+ waitOnFail: args.runIntegrationtests.waitOnFail,
+ suiteSpec: args.runIntegrationtests.suites,
+ dryRun: args.runIntegrationtests.dryRun,
+ verbosity: args.runIntegrationtests.quiet ? 0 : 1,
+ includeExperimental: args.runIntegrationtests.experimental ?? false,
+ noTimeout: args.runIntegrationtests.noTimeout,
+ });
+ });
+
+async function read(stream: NodeJS.ReadStream) {
+ const chunks = [];
+ for await (const chunk of stream) chunks.push(chunk);
+ return Buffer.concat(chunks).toString("utf8");
+}
+
+testingCli.subcommand("tvgcheck", "tvgcheck").action(async (args) => {
+ const data = await read(process.stdin);
+
+ const lines = data.match(/[^\r\n]+/g);
+
+ if (!lines) {
+ throw Error("can't split lines");
+ }
+
+ const vals: Record<string, string> = {};
+
+ let inBlindSigningSection = false;
+
+ for (const line of lines) {
+ if (line === "blind signing:") {
+ inBlindSigningSection = true;
+ continue;
+ }
+ if (line[0] !== " ") {
+ inBlindSigningSection = false;
+ continue;
+ }
+ if (inBlindSigningSection) {
+ const m = line.match(/ (\w+) (\w+)/);
+ if (!m) {
+ console.log("bad format");
+ process.exit(2);
+ }
+ vals[m[1]] = m[2];
+ }
+ }
+
+ console.log(vals);
+
+ const req = (k: string) => {
+ if (!vals[k]) {
+ throw Error(`no value for ${k}`);
+ }
+ return decodeCrock(vals[k]);
+ };
+
+ const myBm = rsaBlind(
+ req("message_hash"),
+ req("blinding_key_secret"),
+ req("rsa_public_key"),
+ );
+
+ deepStrictEqual(req("blinded_message"), myBm);
+
+ console.log("check passed!");
+});
+
+export function main() {
+ testingCli.run();
+}