/* 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 */ /** * Imports. */ import { AccessToken, AmountString, Amounts, BalancesResponse, Configuration, Duration, HttpStatusCode, Logger, PaytoString, TalerAuthenticationHttpClient, TalerBankConversionHttpClient, TalerCoreBankHttpClient, TalerMerchantInstanceHttpClient, TalerMerchantManagementHttpClient, TransactionsResponse, createRFC8959AccessTokenEncoded, createRFC8959AccessTokenPlain, decodeCrock, encodeCrock, generateIban, j2s, randomBytes, rsaBlind, setGlobalLogLevelFromString, 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"; 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 { // 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.", }) .maybeOption("configEntryFile", ["-c", "--config"], clk.STRING, { help: "Configuration file to use.", }) .maybeOption("project", ["--project"], clk.STRING, { help: `Selection of the project to inspect/change the config (default: taler).`, }); configCli .subcommand("show", "show", { help: "Show the current configuration.", }) .action(async (args) => { const config = Configuration.load( args.configArgs.configEntryFile, args.configArgs.project, ); const cfgStr = config.stringify({ diagnostics: true, }); console.log(cfgStr); }); configCli .subcommand("get", "get", { help: "Get a configuration option.", }) .requiredArgument("section", clk.STRING) .requiredArgument("option", clk.STRING) .flag("file", ["-f"], { help: "Treat the value as a filename, expanding placeholders.", }) .action(async (args) => { const config = Configuration.load( args.configArgs.configEntryFile, args.configArgs.project, ); 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); } }); configCli .subcommand("set", "set", { help: "Set a configuration option.", }) .requiredArgument("section", clk.STRING) .requiredArgument("option", clk.STRING) .requiredArgument("value", clk.STRING) .flag("dry", ["--dry"], { help: "Do not write the changed config to disk, only write it to stdout.", }) .action(async (args) => { const config = Configuration.load( args.configArgs.configEntryFile, args.configArgs.project, ); config.setString(args.set.section, args.set.option, args.set.value); if (args.set.dry) { console.log( config.stringify({ excludeDefaults: true, }), ); } else { config.write({ excludeDefaults: true, }); } }); 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 = createRFC8959AccessTokenPlain( args.provisionBankMerchant.merchantToken, ); const bankAdminPassword = args.provisionBankMerchant.bankPassword; const bankAdminTokenArg = args.provisionBankMerchant.bankToken ? createRFC8959AccessTokenPlain(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: "readwrite", 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; { logger.info(`token: ${j2s(bankAdminToken)}`); 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}`, ); logger.error(j2s(resp)); 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: createRFC8959AccessTokenPlain(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( createRFC8959AccessTokenEncoded(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( createRFC8959AccessTokenEncoded(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!", }, editable_defaults: { amount: currency, }, }, ); 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", }); } 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( createRFC8959AccessTokenEncoded(prevPassword), { method: "token", token: createRFC8959AccessTokenPlain(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( createRFC8959AccessTokenEncoded(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 managementApi = new TalerMerchantManagementHttpClient( baseUrl, httpLib, ); const managementToken = createRFC8959AccessTokenEncoded( args.provisionMerchantInstance.managementToken, ); const instanceTokenEnc = createRFC8959AccessTokenPlain( args.provisionMerchantInstance.instanceToken, ); const instanceTokenPlain = createRFC8959AccessTokenPlain( 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 managementApi.createInstance(managementToken, { address: {}, auth: { method: "token", token: instanceTokenPlain, }, 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 instanceUrl = managementApi.getSubInstanceAPI(instanceId).href; const instanceApi = new TalerMerchantInstanceHttpClient( instanceUrl, httpLib, ); const createAccountResp = await instanceApi.addBankAccount( instanceTokenEnc, { 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}`, ); logger.error(j2s(resp)); 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); }); 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 = {}; 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(); }