taler-typescript-core

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

commit 140979d025fbe333092816b1630ee00dcb44db75
parent 71181dfdcbd1a428df088203ab0aae472da49cd7
Author: Florian Dold <florian@dold.me>
Date:   Mon, 22 Sep 2025 14:27:12 +0200

donau: basic test and utils

Diffstat:
Apackages/taler-harness/src/harness/harness-donau.ts | 309+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-harness/src/harness/harness.ts | 6+++---
Apackages/taler-harness/src/integrationtests/test-donau.ts | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-harness/src/integrationtests/testrunner.ts | 2++
Apackages/taler-util/src/http-client/donau-client.ts | 160+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-util/src/index.ts | 1+
Mpackages/taler-util/src/talerconfig.ts | 7+++++++
Apackages/taler-util/src/types-donau.ts | 189+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-util/src/types-taler-common.ts | 2+-
Mpackages/taler-util/src/types-taler-exchange.ts | 54+++---------------------------------------------------
10 files changed, 734 insertions(+), 55 deletions(-)

diff --git a/packages/taler-harness/src/harness/harness-donau.ts b/packages/taler-harness/src/harness/harness-donau.ts @@ -0,0 +1,309 @@ +/* + This file is part of GNU Taler + (C) 2020-2025 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/> + */ + +import { + ConfigSources, + Configuration, + DonauHttpClient, +} from "@gnu-taler/taler-util"; +import * as fs from "node:fs"; +import { CoinConfig } from "./denomStructures.js"; +import { GlobalTestState, pingProc, ProcessWrapper, sh } from "./harness.js"; + +/** + * Test harness for various DONAU, the + * donation authority service. + * + * @author Florian Dold <dold@taler.net> + */ + +export interface DonauConfig { + name: string; + currency: string; + hostname?: string; + roundUnit?: string; + httpPort: number; + database: string; + overrideTestDir?: string; + + /** Financial domain that the donau operates under. */ + domain: string; + + /** + * Extra environment variables to pass to the donau processes. + */ + extraProcEnv?: Record<string, string>; +} + +/** + * Donau service handle. + */ +export class DonauService { + private currentTimetravelOffsetMs: number | undefined; + + setTimetravel(tMs: number | undefined): void { + if (this.isRunning()) { + throw Error("can't set time travel while the donau is running"); + } + this.currentTimetravelOffsetMs = tMs; + } + + private get timetravelArg(): string | undefined { + if (this.currentTimetravelOffsetMs != null) { + // Convert to microseconds + return `--timetravel=+${this.currentTimetravelOffsetMs * 1000}`; + } + return undefined; + } + + /** + * Return an empty array if no time travel is set, + * and an array with the time travel command line argument + * otherwise. + */ + private get timetravelArgArr(): string[] { + const tta = this.timetravelArg; + if (tta) { + return [tta]; + } + return []; + } + + get currency() { + return this.donauConfig.currency; + } + + changeConfig(f: (config: Configuration) => void) { + const config = Configuration.load( + this.configFilename, + ConfigSources["donau"], + ); + f(config); + config.writeTo(this.configFilename, { excludeDefaults: true }); + } + + static create(gc: GlobalTestState, e: DonauConfig) { + const testDir = e.overrideTestDir ?? gc.testDir; + const config = new Configuration(); + setDonauPaths(config, `${testDir}/talerhome-donau-${e.name}`, e.name); + config.setString("donau", "currency", e.currency); + config.setString( + "donau", + "currency_round_unit", + e.roundUnit ?? `${e.currency}:0.01`, + ); + const hostname = e.hostname ?? "localhost"; + config.setString("donau", "base_url", `http://${hostname}:${e.httpPort}/`); + config.setString("donau", "serve", "tcp"); + config.setString("donau", "port", `${e.httpPort}`); + config.setString("donau", "domain", e.domain); + config.setString("donau", "EXPIRE_LEGAL", "5"); + + config.setString("donaudb-postgres", "config", e.database); + + // Limit signing lookahead to make the test startup faster. + config.setString("donau-secmod-cs", "lookahead_sign", "24 days"); + config.setString("donau-secmod-eddsa", "lookahead_sign", "24 days"); + config.setString("donau-secmod-rsa", "lookahead_sign", "24 days"); + + + const cfgFilename = testDir + `/donau-${e.name}.conf`; + config.writeTo(cfgFilename, { excludeDefaults: true }); + return new DonauService(gc, e, cfgFilename); + } + + /** + * Run a function that modifies the existing donau configuration. + * The modified configuration will then be written to the + * file system. + */ + async modifyConfig( + f: (config: Configuration) => Promise<void>, + ): Promise<void> { + const config = Configuration.load( + this.configFilename, + ConfigSources["donau"], + ); + await f(config); + config.writeTo(this.configFilename, { excludeDefaults: true }); + } + + donauHttpProc: ProcessWrapper | undefined; + + helperCryptoRsaProc: ProcessWrapper | undefined; + helperCryptoEddsaProc: ProcessWrapper | undefined; + helperCryptoCsProc: ProcessWrapper | undefined; + + constructor( + private globalState: GlobalTestState, + private donauConfig: DonauConfig, + private configFilename: string, + ) {} + + get name() { + return this.donauConfig.name; + } + + get baseUrl() { + const host = this.donauConfig.hostname ?? "localhost"; + return `http://${host}:${this.donauConfig.httpPort}/`; + } + + isRunning(): boolean { + return !!this.donauHttpProc; + } + + async stop(): Promise<void> { + const httpd = this.donauHttpProc; + if (httpd) { + httpd.proc.kill("SIGTERM"); + await httpd.wait(); + this.donauHttpProc = undefined; + } + const cryptoRsa = this.helperCryptoRsaProc; + if (cryptoRsa) { + cryptoRsa.proc.kill("SIGTERM"); + await cryptoRsa.wait(); + this.helperCryptoRsaProc = undefined; + } + const cryptoEddsa = this.helperCryptoEddsaProc; + if (cryptoEddsa) { + cryptoEddsa.proc.kill("SIGTERM"); + await cryptoEddsa.wait(); + this.helperCryptoRsaProc = undefined; + } + const cryptoCs = this.helperCryptoCsProc; + if (cryptoCs) { + cryptoCs.proc.kill("SIGTERM"); + await cryptoCs.wait(); + this.helperCryptoCsProc = undefined; + } + } + + async dbinit() { + await sh( + this.globalState, + "donau-dbinit", + `donau-dbinit -c "${this.configFilename}"`, + ); + } + + addCoinConfigList(ccs: CoinConfig[]) { + const config = Configuration.load( + this.configFilename, + ConfigSources["donau"], + ); + ccs.forEach((cc) => setDonauCoin(config, cc)); + config.writeTo(this.configFilename, { excludeDefaults: true }); + } + + async start( + opts: { skipDbinit?: boolean; skipKeyup?: boolean } = {}, + ): Promise<void> { + if (this.isRunning()) { + throw Error("donau is already running"); + } + + const skipDbinit = opts.skipDbinit ?? false; + + if (!skipDbinit) { + await this.dbinit(); + } + + this.helperCryptoEddsaProc = this.globalState.spawnService( + "donau-secmod-eddsa", + ["-c", this.configFilename, "-LDEBUG", ...this.timetravelArgArr], + `donau-secmod-eddsa-${this.name}`, + ); + + this.helperCryptoCsProc = this.globalState.spawnService( + "donau-secmod-cs", + ["-c", this.configFilename, "-LDEBUG", ...this.timetravelArgArr], + `donau-secmod-cs-${this.name}`, + ); + + this.helperCryptoRsaProc = this.globalState.spawnService( + "donau-secmod-rsa", + ["-c", this.configFilename, "-LDEBUG", ...this.timetravelArgArr], + `donau-secmod-rsa-${this.name}`, + ); + + this.donauHttpProc = this.globalState.spawnService( + "donau-httpd", + ["-LDEBUG", "-c", this.configFilename, ...this.timetravelArgArr], + `donau-httpd-${this.name}`, + { ...process.env, ...(this.donauConfig.extraProcEnv ?? {}) }, + ); + + await this.pingUntilAvailable(); + + { + const donauClient = new DonauHttpClient(this.baseUrl, {}); + // Would throw on incompatible version. + const configResp = await donauClient.getConfig(); + this.globalState.assertTrue(configResp.type === "ok"); + } + } + + async pingUntilAvailable(): Promise<void> { + // We request /management/keys, since /keys can block + // when we didn't do the key setup yet. + const url = `http://localhost:${this.donauConfig.httpPort}/config`; + await pingProc(this.donauHttpProc, url, `donau (${this.name})`); + } +} + +/** + * @param name additional component name, needed when launching multiple instances of the same component + */ +function setDonauPaths(config: Configuration, home: string, name?: string) { + config.setString("paths", "taler_home", home); + // We need to make sure that the path of taler_runtime_dir isn't too long, + // as it contains unix domain sockets (108 character limit). + const extraName = name != null ? `${name}-` : ""; + const runDir = fs.mkdtempSync(`/tmp/donau-test-${extraName}`); + config.setString("paths", "donau_runtime_dir", runDir); + config.setString( + "paths", + "donau_data_home", + "$DONAU_HOME/.local/share/donau/", + ); + config.setString("paths", "donau_config_home", "$TALER_HOME/.config/donau/"); + config.setString("paths", "donau_cache_home", "$TALER_HOME/.config/donau/"); +} + +function setDonauCoin(config: Configuration, c: CoinConfig) { + const s = `doco_${c.name}`; + config.setString(s, "value", c.value); + config.setString(s, "duration_withdraw", c.durationWithdraw); + config.setString(s, "duration_spend", c.durationSpend); + config.setString(s, "duration_legal", c.durationLegal); + config.setString(s, "fee_deposit", c.feeDeposit); + config.setString(s, "fee_withdraw", c.feeWithdraw); + config.setString(s, "fee_refresh", c.feeRefresh); + config.setString(s, "fee_refund", c.feeRefund); + if (c.ageRestricted) { + config.setString(s, "age_restricted", "yes"); + } + if (c.cipher === "RSA") { + config.setString(s, "rsa_keysize", `${c.rsaKeySize}`); + config.setString(s, "cipher", "RSA"); + } else if (c.cipher === "CS") { + config.setString(s, "cipher", "CS"); + } else { + throw new Error(); + } +} diff --git a/packages/taler-harness/src/harness/harness.ts b/packages/taler-harness/src/harness/harness.ts @@ -620,7 +620,7 @@ function setTalerPaths(config: Configuration, home: string, name?: string) { config.setString("paths", "taler_cache_home", "$TALER_HOME/.config/taler/"); } -function setCoin(config: Configuration, c: CoinConfig) { +function setExchangeCoin(config: Configuration, c: CoinConfig) { const s = `coin_${c.name}`; config.setString(s, "value", c.value); config.setString(s, "duration_withdraw", c.durationWithdraw); @@ -1412,7 +1412,7 @@ export class ExchangeService implements ExchangeServiceInterface { ConfigSources["taler-exchange"], ); offeredCoins.forEach((cc) => - setCoin(config, cc(this.exchangeConfig.currency)), + setExchangeCoin(config, cc(this.exchangeConfig.currency)), ); config.writeTo(this.configFilename, { excludeDefaults: true }); } @@ -1422,7 +1422,7 @@ export class ExchangeService implements ExchangeServiceInterface { this.configFilename, ConfigSources["taler-exchange"], ); - ccs.forEach((cc) => setCoin(config, cc)); + ccs.forEach((cc) => setExchangeCoin(config, cc)); config.writeTo(this.configFilename, { excludeDefaults: true }); } diff --git a/packages/taler-harness/src/integrationtests/test-donau.ts b/packages/taler-harness/src/integrationtests/test-donau.ts @@ -0,0 +1,59 @@ +/* + This file is part of GNU Taler + (C) 2020-2025 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 { DonauHttpClient, j2s } from "@gnu-taler/taler-util"; +import { createSimpleTestkudosEnvironmentV3 } from "../harness/environments.js"; +import { DonauService } from "../harness/harness-donau.js"; +import { GlobalTestState } from "../harness/harness.js"; +import { defaultCoinConfig } from "../harness/denomStructures.js"; + +export async function runDonauTest(t: GlobalTestState) { + // Set up test environment + + const { + walletClient, + bankClient, + exchange, + merchant, + merchantAdminAccessToken, + commonDb, + } = await createSimpleTestkudosEnvironmentV3(t); + + const donau = DonauService.create(t, { + currency: "KUDOS", + database: commonDb.connStr, + httpPort: 8084, + name: "donau", + domain: "Bern", + }); + + donau.addCoinConfigList(defaultCoinConfig.map((x) => x("TESTKUDOS"))); + + await donau.start(); + + const donauClient = new DonauHttpClient(donau.baseUrl); + + const config = await donauClient.getConfig(); + console.log(`config: ${j2s(config)}`); + + const keys = await donauClient.getKeys(); + console.log(`keys: ${j2s(keys)}`); +} + +runDonauTest.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts @@ -196,6 +196,7 @@ import { runWithdrawalHugeTest } from "./test-withdrawal-huge.js"; import { runWithdrawalIdempotentTest } from "./test-withdrawal-idempotent.js"; import { runWithdrawalManualTest } from "./test-withdrawal-manual.js"; import { runWithdrawalPrepareTest } from "./test-withdrawal-prepare.js"; +import { runDonauTest } from "./test-donau.js"; /** * Test runner. @@ -377,6 +378,7 @@ const allTests: TestMainFunction[] = [ runMerchantAcctselTest, runLibeufinConversionTest, runDenomLostComplexTest, + runDonauTest, ]; export interface TestRunSpec { diff --git a/packages/taler-util/src/http-client/donau-client.ts b/packages/taler-util/src/http-client/donau-client.ts @@ -0,0 +1,160 @@ +/* + This file is part of GNU Taler + (C) 2022-2025 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/> + */ + +import { + HttpRequestLibrary, + HttpRequestOptions, + HttpResponse, + readSuccessResponseJsonOrThrow, +} from "../http-common.js"; +import { HttpStatusCode } from "../http-status-codes.js"; +import { createPlatformHttpLib } from "../http.js"; +import { LibtoolVersion } from "../libtool-version.js"; +import { + OperationFail, + OperationOk, + opFixedSuccess, + opKnownHttpFailure, + opSuccessFromHttp, + opUnknownHttpFailure, +} from "../operation.js"; + +import { TalerError } from "../errors.js"; +import { + CancellationToken, + codecForTalerCommonConfigResponse, + LongpollQueue, +} from "../index.js"; +import { TalerErrorCode } from "../taler-error-codes.js"; +import { + codecForDonauKeysResponse, + codecForDonauVersionResponse, + DonauKeysResponse, + DonauVersionResponse, +} from "../types-donau.js"; + +/** + * Client library for the GNU Taler donau service. + */ +export class DonauHttpClient { + public static readonly SUPPORTED_DONAU_PROTOCOL_VERSION = "0:0:0"; + private httpLib: HttpRequestLibrary; + private cancelationToken: CancellationToken; + private longPollQueue: LongpollQueue; + + constructor( + readonly baseUrl: string, + params: { + httpClient?: HttpRequestLibrary; + preventCompression?: boolean; + cancelationToken?: CancellationToken; + longPollQueue?: LongpollQueue; + } = {}, + ) { + this.httpLib = params.httpClient ?? createPlatformHttpLib(); + this.cancelationToken = + params.cancelationToken ?? CancellationToken.CONTINUE; + this.longPollQueue = params.longPollQueue ?? new LongpollQueue(); + } + + isCompatible(version: string): boolean { + const compare = LibtoolVersion.compare( + DonauHttpClient.SUPPORTED_DONAU_PROTOCOL_VERSION, + version, + ); + return compare?.compatible ?? false; + } + + private async fetch( + url_or_path: URL | string, + opts: HttpRequestOptions = {}, + longpoll: boolean = false, + ): Promise<HttpResponse> { + const url = + typeof url_or_path == "string" + ? new URL(url_or_path, this.baseUrl) + : url_or_path; + if (longpoll || url.searchParams.has("timeout_ms")) { + return this.longPollQueue.run( + url, + this.cancelationToken, + async (timeoutMs) => { + url.searchParams.set("timeout_ms", String(timeoutMs)); + return this.httpLib.fetch(url.href, { + cancellationToken: this.cancelationToken, + ...opts, + }); + }, + ); + } else { + return this.httpLib.fetch(url.href, { + cancellationToken: this.cancelationToken, + ...opts, + }); + } + } + + async getConfig(): Promise< + OperationFail<HttpStatusCode.NotFound> | OperationOk<DonauVersionResponse> + > { + const resp = await this.fetch("config"); + switch (resp.status) { + case HttpStatusCode.Ok: { + const minBody = await readSuccessResponseJsonOrThrow( + resp, + codecForTalerCommonConfigResponse(), + ); + const expectedName = "donau"; + if (minBody.name !== expectedName) { + throw TalerError.fromUncheckedDetail({ + code: TalerErrorCode.GENERIC_UNEXPECTED_REQUEST_ERROR, + requestUrl: resp.requestUrl, + httpStatusCode: resp.status, + detail: `Unexpected server component name (got ${minBody.name}, expected ${expectedName}})`, + }); + } + if (!this.isCompatible(minBody.version)) { + throw TalerError.fromUncheckedDetail({ + code: TalerErrorCode.GENERIC_CLIENT_UNSUPPORTED_PROTOCOL_VERSION, + requestUrl: resp.requestUrl, + httpStatusCode: resp.status, + detail: `Unsupported protocol version, client supports ${DonauHttpClient.SUPPORTED_DONAU_PROTOCOL_VERSION}, server supports ${minBody.version}`, + }); + } + // Now that we've checked the basic body, re-parse the full response. + const body = await readSuccessResponseJsonOrThrow( + resp, + codecForDonauVersionResponse(), + ); + return opFixedSuccess(body); + } + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownHttpFailure(resp); + } + } + + async getKeys(): Promise<OperationOk<DonauKeysResponse>> { + const resp = await this.fetch("keys"); + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForDonauKeysResponse()); + default: + return opUnknownHttpFailure(resp); + } + } +} diff --git a/packages/taler-util/src/index.ts b/packages/taler-util/src/index.ts @@ -16,6 +16,7 @@ export * from "./http-client/bank-integration.js"; export * from "./http-client/bank-revenue.js"; export * from "./http-client/bank-wire.js"; export * from "./http-client/challenger.js"; +export * from "./http-client/donau-client.js"; export * from "./http-client/exchange-client.js"; export * from "./http-client/merchant.js"; export * from "./http-client/officer-account.js"; diff --git a/packages/taler-util/src/talerconfig.ts b/packages/taler-util/src/talerconfig.ts @@ -126,6 +126,13 @@ export const ConfigSources = { baseConfigVarname: "GNUNET_BASE_CONFIG", prefixVarname: "GNUNET_PREFIX", } satisfies ConfigSource, + ["donau"]: { + projectName: "donau", + componentName: "donau", + installPathBinary: "donau-config", + baseConfigVarname: "DONAU_BASE_CONFIG", + prefixVarname: "DONAU_PREFIX", + } satisfies ConfigSource, } satisfies ConfigSourceDef; const defaultConfigSource: ConfigSource = ConfigSources["taler-exchange"]; diff --git a/packages/taler-util/src/types-donau.ts b/packages/taler-util/src/types-donau.ts @@ -0,0 +1,189 @@ +/* + This file is part of GNU Taler + (C) 2024-2025 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero 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 Affero General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + + SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { + buildCodecForObject, + Codec, + codecForConstString, + codecForString, +} from "./codec.js"; +import { + AmountString, + codecForAny, + codecForEddsaPublicKey, + codecForEddsaSignature, + codecForList, + codecForNumber, + codecForTimestamp, + Cs25519Point, + EddsaPublicKeyString, + EddsaSignatureString, + RsaPublicKeyString, + TalerProtocolTimestamp, +} from "./index.js"; + +export interface DonauVersionResponse { + // libtool-style representation of the Donau protocol version, see + // https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning + // The format is "current:revision:age". + version: string; + + // Name of the protocol. + name: "donau"; + + // Currency supported by this Donau. + currency: string; + + // Financial domain by this Donau. + //domain: string; +} + +export const codecForDonauVersionResponse = (): Codec<DonauVersionResponse> => + buildCodecForObject<DonauVersionResponse>() + .property("version", codecForString()) + .property("name", codecForConstString("donau")) + .property("currency", codecForString()) + //.property("domain", codecForString()) + .build("DonauApi.DonauVersionResponse"); + +/** + * Structure of one exchange signing key in the /keys response. + */ +export class DonauSignKeyJson { + stamp_start: TalerProtocolTimestamp; + stamp_expire: TalerProtocolTimestamp; + stamp_end: TalerProtocolTimestamp; + key: EddsaPublicKeyString; + master_sig: EddsaSignatureString; +} + +export const codecForDonauSignKeyJson = (): Codec<DonauSignKeyJson> => + buildCodecForObject<DonauSignKeyJson>() + .property("key", codecForEddsaPublicKey()) + .property("master_sig", codecForEddsaSignature()) + .property("stamp_end", codecForTimestamp) + .property("stamp_start", codecForTimestamp) + .property("stamp_expire", codecForTimestamp) + .build("DonauSignKeyJson"); + +export interface DonauKeysResponse { + // libtool-style representation of the Donau protocol version, see + // https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning + // The format is "current:revision:age". + version: string; + + // Financial domain this Donau operates for. + //domain: string; + + // The Donau's base URL. + base_url: string; + + // The Donau's currency. + currency: string; + + // How many digits should the amounts be rendered + // with by default. Small capitals should + // be used to render fractions beyond the number + // given here (like on gas stations). + //currency_fraction_digits: number; + + // Donation Units offered by this Donau + donation_units: DonationUnitKeyGroup[]; + + // The Donau's signing keys. + signkeys: DonauSignKeyJson[]; +} + +export type DonationUnitKeyGroup = + | DonationUnitKeyGroupRsa + | DonationUnitKeyGroupCs; + +export interface DonationUnitKeyGroupCommon { + // How much are coins of this denomination worth? + value: AmountString; + + // Fee charged by the exchange for withdrawing a coin of this denomination. + fee_withdraw: AmountString; + + // Fee charged by the exchange for depositing a coin of this denomination. + fee_deposit: AmountString; + + // Fee charged by the exchange for refreshing a coin of this denomination. + fee_refresh: AmountString; + + // Fee charged by the exchange for refunding a coin of this denomination. + fee_refund: AmountString; +} + +export interface DonationUnitKeyCommon { + // Signature of TALER_DenominationKeyValidityPS. + master_sig: EddsaSignatureString; + + // When does the denomination key become valid? + stamp_start: TalerProtocolTimestamp; + + // When is it no longer possible to deposit coins + // of this denomination? + stamp_expire_withdraw: TalerProtocolTimestamp; + + // Timestamp indicating by when legal disputes relating to these coins must + // be settled, as the exchange will afterwards destroy its evidence relating to + // transactions involving this coin. + stamp_expire_legal: TalerProtocolTimestamp; + + stamp_expire_deposit: TalerProtocolTimestamp; + + // Set to 'true' if the exchange somehow "lost" + // the private key. The denomination was not + // necessarily revoked, but still cannot be used + // to withdraw coins at this time (theoretically, + // the private key could be recovered in the + // future; coins signed with the private key + // remain valid). + lost?: boolean; +} + +export interface DonationUnitKeyGroupRsa extends DonationUnitKeyGroupCommon { + cipher: "RSA"; + + denoms: ({ + rsa_pub: RsaPublicKeyString; + } & DonationUnitKeyCommon)[]; +} + +export interface DonationUnitKeyGroupCs extends DonationUnitKeyGroupCommon { + cipher: "CS"; + denoms: ({ + cs_pub: Cs25519Point; + } & DonationUnitKeyCommon)[]; +} + +// FIXME: Validate properly! +export const codecForDonationUnitKeyGroup: Codec<DonationUnitKeyGroup> = + codecForAny(); + +export const codecForDonauKeysResponse = (): Codec<DonauKeysResponse> => + buildCodecForObject<DonauKeysResponse>() + .property("version", codecForString()) + .property("base_url", codecForString()) + .property("currency", codecForString()) + //.property("domain", codecForString()) + .property("signkeys", codecForAny()) + .property("donation_units", codecForList(codecForDonationUnitKeyGroup)) + //.property("currency_fraction_digits", codecForNumber()) + .build("DonauApi.DonauKeysResponse"); diff --git a/packages/taler-util/src/types-taler-common.ts b/packages/taler-util/src/types-taler-common.ts @@ -160,7 +160,7 @@ export interface InternationalizedString { [lang_tag: string]: string; } -export type RsaPublicKeySring = string; +export type RsaPublicKeyString = string; export type AgeMask = number; // The string must be a data URL according to RFC 2397 diff --git a/packages/taler-util/src/types-taler-exchange.ts b/packages/taler-util/src/types-taler-exchange.ts @@ -65,7 +65,7 @@ import { PaytoHash, RelativeTime, RsaPublicKey, - RsaPublicKeySring, + RsaPublicKeyString, Timestamp, WireSalt, codecForAccessToken, @@ -757,7 +757,7 @@ export interface DenomGroupRsa extends DenomGroupCommon { cipher: "RSA"; denoms: ({ - rsa_pub: RsaPublicKeySring; + rsa_pub: RsaPublicKeyString; } & DenomCommon)[]; } @@ -766,7 +766,7 @@ export interface DenomGroupRsaAgeRestricted extends DenomGroupCommon { age_mask: AgeMask; denoms: ({ - rsa_pub: RsaPublicKeySring; + rsa_pub: RsaPublicKeyString; } & DenomCommon)[]; } @@ -2410,54 +2410,6 @@ interface ExtensionManifest { config?: object; } -interface SignKey { - // The actual exchange's EdDSA signing public key. - key: EddsaPublicKeyString; - - // Initial validity date for the signing key. - stamp_start: Timestamp; - - // Date when the exchange will stop using the signing key, allowed to overlap - // slightly with the next signing key's validity to allow for clock skew. - stamp_expire: Timestamp; - - // Date when all signatures made by the signing key expire and should - // henceforth no longer be considered valid in legal disputes. - stamp_end: Timestamp; - - // Signature over key and stamp_expire by the exchange master key. - // Signature of TALER_ExchangeSigningKeyValidityPS. - // Must have purpose TALER_SIGNATURE_MASTER_SIGNING_KEY_VALIDITY. - master_sig: EddsaSignatureString; -} - -interface AuditorKeys { - // The auditor's EdDSA signing public key. - auditor_pub: EddsaPublicKeyString; - - // The auditor's URL. - auditor_url: string; - - // The auditor's name (for humans). - auditor_name: string; - - // An array of denomination keys the auditor affirms with its signature. - // Note that the message only includes the hash of the public key, while the - // signature is actually over the expanded information including expiration - // times and fees. The exact format is described below. - denomination_keys: AuditorDenominationKey[]; -} - -interface AuditorDenominationKey { - // Hash of the public RSA key used to sign coins of the respective - // denomination. Note that the auditor's signature covers more than just - // the hash, but this other information is already provided in denoms and - // thus not repeated here. - denom_pub_h: HashCodeString; - - // Signature of TALER_ExchangeKeyValidityPS. - auditor_sig: EddsaSignatureString; -} export interface GlobalFees { // What date (inclusive) does these fees go into effect?