/* 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 { AmountString, Amounts, BalancesResponse, Configuration, Duration, HttpStatusCode, Logger, MerchantApiClient, MerchantInstanceConfig, RegisterAccountRequest, TalerCorebankApiClient, TransactionsResponse, addPaytoQueryParams, decodeCrock, generateIban, j2s, rsaBlind, setGlobalLogLevelFromString, } 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 { 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 { downloadExchangeInfo, topupReserveWithDemobank } from "@gnu-taler/taler-wallet-core/dbless"; 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 walletDbPath = `${indir}/wallet-db.sqlite3`; if (!fs.existsSync(walletDbPath)) { throw new Error("wallet db to be checked does not exist"); } const { walletClient, walletService } = await createWalletDaemonWithClient( t, { name: "wallet-loaded", overrideDbPath: walletDbPath }, ); 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 topupReserveWithDemobank({ 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 topupReserveWithDemobank({ 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 topupReserveWithDemobank({ 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("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) .action(async (args) => { const httpLib = createPlatformHttpLib(); const baseUrl = args.provisionMerchantInstance.merchantApiBaseUrl; const managementToken = args.provisionMerchantInstance.managementToken; const instanceToken = args.provisionMerchantInstance.instanceToken; const instanceId = args.provisionMerchantInstance.id; const body: MerchantInstanceConfig = { address: {}, auth: { method: "token", token: args.provisionMerchantInstance.instanceToken, }, default_pay_delay: Duration.toTalerProtocolDuration( Duration.fromSpec({ hours: 1 }), ), default_wire_transfer_delay: { d_us: 1 }, id: instanceId, jurisdiction: {}, name: args.provisionMerchantInstance.name, use_stefan: true, }; const url = new URL("management/instances", baseUrl); const createResp = await httpLib.fetch(url.href, { method: "POST", body, headers: { Authorization: `Bearer ${managementToken}`, }, }); if (createResp.status >= 200 && createResp.status <= 299) { logger.info(`instance ${instanceId} created successfully`); } else if (createResp.status === HttpStatusCode.Conflict) { logger.info(`instance ${instanceId} already exists`); } else { logger.error( `unable to create instance ${instanceId}, HTTP status ${createResp.status}`, ); process.exit(2); } const accountsUrl = new URL( `instances/${instanceId}/private/accounts`, baseUrl, ); const accountBody = { payto_uri: args.provisionMerchantInstance.payto, }; const createAccountResp = await httpLib.fetch(accountsUrl.href, { method: "POST", body: accountBody, headers: { Authorization: `Bearer ${instanceToken}`, }, }); if (createAccountResp.status != 200) { console.error( `unable to configure bank account for instance ${instanceId}, status ${createAccountResp.status}`, ); const resp = await createAccountResp.json(); console.error(j2s(resp)); 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 corebankApiBaseUrl = args.provisionBankAccount.corebankApiBaseUrl; const url = new URL("accounts", corebankApiBaseUrl); const accountLogin = args.provisionBankAccount.login; const body: RegisterAccountRequest = { 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, }; const resp = await httpLib.fetch(url.href, { method: "POST", body, }); if (resp.status >= 200 && resp.status <= 299) { logger.info(`account ${accountLogin} successfully provisioned`); return; } if (resp.status === HttpStatusCode.Conflict) { logger.info(`account ${accountLogin} already provisioned`); return; } logger.error( `unable to provision bank account, HTTP response status ${resp.status}`, ); 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 = {}; 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(); }