taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit 122d7370e81f0df3f906de37187ade7e4ddb91c1
parent 6b920a7986ec1f80f57b22569d2ab01b4a4b5040
Author: Florian Dold <florian@dold.me>
Date:   Mon, 27 Jan 2025 12:26:21 +0100

harness: implement simple exchange linter

Diffstat:
Mpackages/taler-harness/src/index.ts | 19++++++++++++++-----
Mpackages/taler-harness/src/integrationtests/test-exchange-timetravel.ts | 10+++++-----
Mpackages/taler-harness/src/lint.ts | 157+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
3 files changed, 169 insertions(+), 17 deletions(-)

diff --git a/packages/taler-harness/src/index.ts b/packages/taler-harness/src/index.ts @@ -88,7 +88,7 @@ import { } from "./harness/harness.js"; import { AML_PROGRAM_TEST_KYC_NEW_MEASURES_PROG } from "./integrationtests/test-kyc-new-measures-prog.js"; import { getTestInfo, runTests } from "./integrationtests/testrunner.js"; -import { lintExchangeDeployment } from "./lint.js"; +import { lintExchangeDeployment, lintExchangeUrl } from "./lint.js"; const logger = new Logger("taler-harness:index.ts"); @@ -604,8 +604,8 @@ deploymentCli }); deploymentCli - .subcommand("lintExchange", "lint-exchange", { - help: "Run checks on the exchange deployment.", + .subcommand("lintExchangeConfig", "lint-exchange-config", { + help: "Run checks on the exchange deployment running on the current machine.", }) .flag("cont", ["--continue"], { help: "Continue after errors if possible", @@ -615,12 +615,21 @@ deploymentCli }) .action(async (args) => { await lintExchangeDeployment( - args.lintExchange.debug, - args.lintExchange.cont, + args.lintExchangeConfig.debug, + args.lintExchangeConfig.cont, ); }); deploymentCli + .subcommand("lintExchangeUrl", "lint-exchange-url", { + help: "Run checks on a remote exchange deployment.", + }) + .requiredArgument("baseUrl", clk.STRING) + .action(async (args) => { + await lintExchangeUrl(args.lintExchangeUrl.baseUrl); + }); + +deploymentCli .subcommand("waitService", "wait-taler-service", { help: "Wait for the config endpoint of a Taler-style service to be available", }) diff --git a/packages/taler-harness/src/integrationtests/test-exchange-timetravel.ts b/packages/taler-harness/src/integrationtests/test-exchange-timetravel.ts @@ -19,11 +19,11 @@ */ import { AbsoluteTime, - codecForExchangeKeysJson, + codecForExchangeKeysResponse, DenominationPubKey, DenomKeyType, Duration, - ExchangeKeysJson, + ExchangeKeysResponse, Logger, TalerCorebankApiClient, } from "@gnu-taler/taler-util"; @@ -53,7 +53,7 @@ interface DenomInfo { expireDeposit: string; } -function getDenomInfoFromKeys(ek: ExchangeKeysJson): DenomInfo[] { +function getDenomInfoFromKeys(ek: ExchangeKeysResponse): DenomInfo[] { const denomInfos: DenomInfo[] = []; for (const denomGroup of ek.denominations) { switch (denomGroup.cipher) { @@ -201,7 +201,7 @@ export async function runExchangeTimetravelTest(t: GlobalTestState) { const keysResp1 = await http.fetch(exchange.baseUrl + "keys"); const keys1 = await readSuccessResponseJsonOrThrow( keysResp1, - codecForExchangeKeysJson(), + codecForExchangeKeysResponse(), ); console.log( "keys 1 (before time travel):", @@ -223,7 +223,7 @@ export async function runExchangeTimetravelTest(t: GlobalTestState) { const keysResp2 = await http.fetch(exchange.baseUrl + "keys"); const keys2 = await readSuccessResponseJsonOrThrow( keysResp2, - codecForExchangeKeysJson(), + codecForExchangeKeysResponse(), ); console.log( "keys 2 (after time travel):", diff --git a/packages/taler-harness/src/lint.ts b/packages/taler-harness/src/lint.ts @@ -32,18 +32,24 @@ * Imports. */ import { - codecForExchangeKeysJson, + AbsoluteTime, + canonicalizeBaseUrl, + codecForExchangeKeysResponse, codecForKeysManagementResponse, Configuration, decodeCrock, + narrowOpSuccessOrThrow, + parsePaytoUri, + TalerExchangeHttpClient, + TalerProtocolTimestamp, } from "@gnu-taler/taler-util"; -import { URL } from "node:url"; -import { spawn } from "child_process"; -import { delayMs } from "./harness/harness.js"; import { createPlatformHttpLib, readSuccessResponseJsonOrThrow, } from "@gnu-taler/taler-util/http"; +import { spawn } from "child_process"; +import { URL } from "node:url"; +import { delayMs } from "./harness/harness.js"; interface BasicConf { mainCurrency: string; @@ -430,7 +436,10 @@ export async function checkExchangeHttpd( { const keysUrl = new URL("keys", baseUrl); - const resp = await Promise.race([httpLib.fetch(keysUrl.href), delayMs(2000)]); + const resp = await Promise.race([ + httpLib.fetch(keysUrl.href), + delayMs(2000), + ]); if (!resp) { context.numErr++; @@ -446,7 +455,7 @@ export async function checkExchangeHttpd( } else { const keys = await readSuccessResponseJsonOrThrow( resp, - codecForExchangeKeysJson(), + codecForExchangeKeysResponse(), ); if (keys.master_public_key !== pubConf.masterPublicKey) { @@ -466,7 +475,10 @@ export async function checkExchangeHttpd( { const keysUrl = new URL("wire", baseUrl); - const resp = await Promise.race([httpLib.fetch(keysUrl.href), delayMs(2000)]); + const resp = await Promise.race([ + httpLib.fetch(keysUrl.href), + delayMs(2000), + ]); if (!resp) { context.numErr++; @@ -534,3 +546,134 @@ export async function lintExchangeDeployment( process.exit(1); } } + +function isCurrentlyValid( + start: TalerProtocolTimestamp, + end: TalerProtocolTimestamp, +): boolean { + if (!AbsoluteTime.isExpired(AbsoluteTime.fromProtocolTimestamp(start))) { + return false; + } + if (AbsoluteTime.isExpired(AbsoluteTime.fromProtocolTimestamp(end))) { + return false; + } + return true; +} + +class LintReporter { + numErrors = 0; + reportError(s: string): void { + this.numErrors++; + console.log(`ERROR: ${s}`); + } + reportWarning(s: string): void { + console.log(`WARNING: ${s}`); + } + reportInfo(s: string): void { + console.log(`INFO: ${s}`); + } +} + +export async function lintExchangeUrl(exchangeBaseUrl: string): Promise<void> { + const canonUrl = canonicalizeBaseUrl(exchangeBaseUrl); + if (exchangeBaseUrl != canonUrl) { + console.error(`invalid or non-canonical base url: ${exchangeBaseUrl}`); + process.exit(1); + } + const exchangeClient = new TalerExchangeHttpClient(canonUrl); + const configResp = await exchangeClient.getConfig(); + narrowOpSuccessOrThrow("", configResp); + const currency = configResp.body.currency; + const keysResp = await exchangeClient.getKeys(); + narrowOpSuccessOrThrow("", configResp); + + const reporter = new LintReporter(); + + if (keysResp.body.currency != currency) { + reporter.reportError("currency mismatch between /config and /keys"); + } + + if (keysResp.body.signkeys.length == 0) { + reporter.reportError("exchange has no signing keys"); + } + // Check for valid signing key + { + let haveValidSk = false; + for (const sk of keysResp.body.signkeys) { + if (isCurrentlyValid(sk.stamp_start, sk.stamp_end)) { + haveValidSk = true; + break; + } + } + if (!haveValidSk) { + reporter.reportError("no signing key is valid"); + } + } + let numDenomWithdraw = 0; + let numDenomDeposit = 0; + let numDenomLost = 0; + // Check denominations + { + for (const denomGroup of keysResp.body.denominations) { + for (const denom of denomGroup.denoms) { + if (denom.lost) { + numDenomLost++; + } + if ( + !denom.lost && + isCurrentlyValid(denom.stamp_start, denom.stamp_expire_withdraw) + ) { + numDenomWithdraw++; + } + if (isCurrentlyValid(denom.stamp_start, denom.stamp_expire_deposit)) { + numDenomDeposit++; + } + } + } + if (numDenomWithdraw == 0) { + reporter.reportWarning("no valid denomination available for withdrawal"); + } + if (numDenomDeposit == 0) { + reporter.reportWarning("no valid denomination available for deposit"); + } + if (numDenomLost > 0) { + reporter.reportWarning(`have lost denominations (${numDenomLost})`); + } + } + { + let haveGlobalFee = false; + for (const gf of keysResp.body.global_fees) { + if (isCurrentlyValid(gf.start_date, gf.end_date)) { + haveGlobalFee = true; + break; + } + } + if (!haveGlobalFee) { + reporter.reportError("no valid global fees for current time"); + } + } + if (keysResp.body.accounts.length === 0) { + reporter.reportError("no wire accounts configured"); + } + let wireTypesSet = new Set<string>(); + for (const acc of keysResp.body.accounts) { + const payto = parsePaytoUri(acc.payto_uri); + if (!payto) { + reporter.reportError(`invalid account payto URI: ${acc.payto_uri}`); + continue; + } + wireTypesSet.add(payto?.targetType); + } + for (const wireType of wireTypesSet) { + let haveAccountFees = false; + for (const wf of keysResp.body.wire_fees[wireType]) { + if (isCurrentlyValid(wf.start_date, wf.end_date)) { + haveAccountFees = true; + break; + } + } + if (!haveAccountFees) { + reporter.reportError(`missing wire account fees for ${wireType}`); + } + } +}