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:
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?