summaryrefslogtreecommitdiff
path: root/packages/taler-util/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-util/src')
-rw-r--r--packages/taler-util/src/MerchantApiClient.ts371
-rw-r--r--packages/taler-util/src/ReserveStatus.ts14
-rw-r--r--packages/taler-util/src/ReserveTransaction.ts13
-rw-r--r--packages/taler-util/src/TaskThrottler.ts160
-rw-r--r--packages/taler-util/src/amounts.test.ts75
-rw-r--r--packages/taler-util/src/amounts.ts229
-rw-r--r--packages/taler-util/src/argon2-impl.missing.ts9
-rw-r--r--packages/taler-util/src/argon2-impl.wasm.ts19
-rw-r--r--packages/taler-util/src/argon2.ts17
-rw-r--r--packages/taler-util/src/backup-types.ts1259
-rw-r--r--packages/taler-util/src/bank-api-client.ts458
-rw-r--r--packages/taler-util/src/bitcoin.ts8
-rw-r--r--packages/taler-util/src/clk.ts61
-rw-r--r--packages/taler-util/src/codec.ts104
-rw-r--r--packages/taler-util/src/compat.d.ts23
-rw-r--r--packages/taler-util/src/compat.node.ts64
-rw-r--r--packages/taler-util/src/compat.qtart.ts57
-rw-r--r--packages/taler-util/src/contract-terms.ts2
-rw-r--r--packages/taler-util/src/errors.ts329
-rw-r--r--packages/taler-util/src/globbing/minimatch.ts14
-rw-r--r--packages/taler-util/src/helpers.ts16
-rw-r--r--packages/taler-util/src/http-client/README.md19
-rw-r--r--packages/taler-util/src/http-client/authentication.ts137
-rw-r--r--packages/taler-util/src/http-client/bank-conversion.ts223
-rw-r--r--packages/taler-util/src/http-client/bank-core.ts1038
-rw-r--r--packages/taler-util/src/http-client/bank-integration.ts179
-rw-r--r--packages/taler-util/src/http-client/bank-revenue.ts130
-rw-r--r--packages/taler-util/src/http-client/bank-wire.ts226
-rw-r--r--packages/taler-util/src/http-client/challenger.ts291
-rw-r--r--packages/taler-util/src/http-client/exchange.ts271
-rw-r--r--packages/taler-util/src/http-client/merchant.ts2370
-rw-r--r--packages/taler-util/src/http-client/officer-account.ts105
-rw-r--r--packages/taler-util/src/http-client/types.ts5461
-rw-r--r--packages/taler-util/src/http-client/utils.ts116
-rw-r--r--packages/taler-util/src/http-common.ts526
-rw-r--r--packages/taler-util/src/http-impl.missing.ts38
-rw-r--r--packages/taler-util/src/http-impl.node.d.ts25
-rw-r--r--packages/taler-util/src/http-impl.node.ts324
-rw-r--r--packages/taler-util/src/http-impl.qtart.ts211
-rw-r--r--packages/taler-util/src/http.ts37
-rw-r--r--packages/taler-util/src/i18n.ts21
-rw-r--r--packages/taler-util/src/iban.test.ts30
-rw-r--r--packages/taler-util/src/iban.ts296
-rw-r--r--packages/taler-util/src/index.browser.ts4
-rw-r--r--packages/taler-util/src/index.node.ts2
-rw-r--r--packages/taler-util/src/index.qtart.ts27
-rw-r--r--packages/taler-util/src/index.ts67
-rw-r--r--packages/taler-util/src/invariants.ts59
-rw-r--r--packages/taler-util/src/iso-4217.ts1717
-rw-r--r--packages/taler-util/src/kdf.ts35
-rw-r--r--packages/taler-util/src/libeufin-api-types.ts31
-rw-r--r--packages/taler-util/src/libtool-version.test.ts2
-rw-r--r--packages/taler-util/src/logging.ts118
-rw-r--r--packages/taler-util/src/notifications.ts459
-rw-r--r--packages/taler-util/src/observability.ts98
-rw-r--r--packages/taler-util/src/operation.ts198
-rw-r--r--packages/taler-util/src/payto.ts133
-rw-r--r--packages/taler-util/src/promises.ts112
-rw-r--r--packages/taler-util/src/qtart.ts35
-rw-r--r--packages/taler-util/src/rfc3548.ts60
-rw-r--r--packages/taler-util/src/sha256.ts2
-rw-r--r--packages/taler-util/src/taler-crypto.test.ts21
-rw-r--r--packages/taler-util/src/taler-crypto.ts351
-rw-r--r--packages/taler-util/src/taler-error-codes.ts1685
-rw-r--r--packages/taler-util/src/taler-types.ts1040
-rw-r--r--packages/taler-util/src/talerconfig.ts350
-rw-r--r--packages/taler-util/src/taleruri.test.ts443
-rw-r--r--packages/taler-util/src/taleruri.ts652
-rw-r--r--packages/taler-util/src/time.test.ts39
-rw-r--r--packages/taler-util/src/time.ts343
-rw-r--r--packages/taler-util/src/timer.ts213
-rw-r--r--packages/taler-util/src/transaction-test-data.ts113
-rw-r--r--packages/taler-util/src/transactions-types.ts385
-rw-r--r--packages/taler-util/src/twrpc-impl.missing.ts26
-rw-r--r--packages/taler-util/src/twrpc-impl.node.ts216
-rw-r--r--packages/taler-util/src/twrpc-impl.qtart.ts26
-rw-r--r--packages/taler-util/src/twrpc.ts63
-rw-r--r--packages/taler-util/src/wallet-types.ts2102
-rw-r--r--packages/taler-util/src/whatwg-url.ts9
79 files changed, 23603 insertions, 3009 deletions
diff --git a/packages/taler-util/src/MerchantApiClient.ts b/packages/taler-util/src/MerchantApiClient.ts
new file mode 100644
index 000000000..f58757fb5
--- /dev/null
+++ b/packages/taler-util/src/MerchantApiClient.ts
@@ -0,0 +1,371 @@
+/*
+ This file is part of GNU Taler
+ (C) 2023 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 { codecForAny } from "./codec.js";
+import {
+ TalerMerchantApi,
+ codecForMerchantConfig,
+ codecForMerchantOrderPrivateStatusResponse,
+ codecForPostOrderResponse,
+} from "./http-client/types.js";
+import { HttpStatusCode } from "./http-status-codes.js";
+import {
+ createPlatformHttpLib,
+ expectSuccessResponseOrThrow,
+ readSuccessResponseJsonOrThrow,
+ readTalerErrorResponse,
+} from "./http.js";
+import { FacadeCredentials } from "./libeufin-api-types.js";
+import { LibtoolVersion } from "./libtool-version.js";
+import { Logger } from "./logging.js";
+import {
+ FailCasesByMethod,
+ OperationFail,
+ OperationOk,
+ ResultByMethod,
+ opEmptySuccess,
+ opKnownHttpFailure,
+ opSuccessFromHttp,
+ opUnknownFailure,
+} from "./operation.js";
+import { AmountString } from "./taler-types.js";
+import { TalerProtocolDuration } from "./time.js";
+
+const logger = new Logger("MerchantApiClient.ts");
+
+// FIXME: Explain!
+export type TalerMerchantResultByMethod<prop extends keyof MerchantApiClient> =
+ ResultByMethod<MerchantApiClient, prop>;
+
+// FIXME: Explain!
+export type TalerMerchantErrorsByMethod<prop extends keyof MerchantApiClient> =
+ FailCasesByMethod<MerchantApiClient, prop>;
+
+export interface MerchantAuthConfiguration {
+ method: "external" | "token";
+ token?: string;
+}
+
+// FIXME: Why do we need this? Describe / fix!
+export interface PartialMerchantInstanceConfig {
+ auth?: MerchantAuthConfiguration;
+ id: string;
+ name: string;
+ paytoUris: string[];
+ address?: unknown;
+ jurisdiction?: unknown;
+ defaultWireTransferDelay?: TalerProtocolDuration;
+ defaultPayDelay?: TalerProtocolDuration;
+}
+
+export interface CreateMerchantTippingReserveRequest {
+ // Amount that the merchant promises to put into the reserve
+ initial_balance: AmountString;
+
+ // Exchange the merchant intends to use for tipping
+ exchange_url: string;
+
+ // Desired wire method, for example "iban" or "x-taler-bank"
+ wire_method: string;
+}
+
+export interface DeleteTippingReserveArgs {
+ reservePub: string;
+ purge?: boolean;
+}
+
+interface MerchantBankAccount {
+ // The payto:// URI where the wallet will send coins.
+ payto_uri: string;
+
+ // Optional base URL for a facade where the
+ // merchant backend can see incoming wire
+ // transfers to reconcile its accounting
+ // with that of the exchange. Used by
+ // taler-merchant-wirewatch.
+ credit_facade_url?: string;
+
+ // Credentials for accessing the credit facade.
+ credit_facade_credentials?: FacadeCredentials;
+}
+
+export interface MerchantInstanceConfig {
+ auth: MerchantAuthConfiguration;
+ id: string;
+ name: string;
+ address: unknown;
+ jurisdiction: unknown;
+ use_stefan: boolean;
+ default_wire_transfer_delay: TalerProtocolDuration;
+ default_pay_delay: TalerProtocolDuration;
+}
+
+export interface PrivateOrderStatusQuery {
+ instance?: string;
+ orderId: string;
+ sessionId?: string;
+}
+
+export interface OtpDeviceAddDetails {
+ // Device ID to use.
+ otp_device_id: string;
+
+ // Human-readable description for the device.
+ otp_device_description: string;
+
+ // A base64-encoded key
+ otp_key: string;
+
+ // Algorithm for computing the POS confirmation.
+ otp_algorithm: number;
+
+ // Counter for counter-based OTP devices.
+ otp_ctr?: number;
+}
+
+/**
+ * Client for the GNU Taler merchant backend.
+ */
+export class MerchantApiClient {
+ /**
+ * Base URL for the particular instance that this merchant API client
+ * is for.
+ */
+ private baseUrl: string;
+
+ readonly auth: MerchantAuthConfiguration;
+
+ public readonly PROTOCOL_VERSION = "6:0:2";
+
+ constructor(
+ baseUrl: string,
+ options: { auth?: MerchantAuthConfiguration } = {},
+ ) {
+ this.baseUrl = baseUrl;
+
+ this.auth = options?.auth ?? {
+ method: "external",
+ };
+ }
+
+ httpClient = createPlatformHttpLib();
+
+ async changeAuth(auth: MerchantAuthConfiguration): Promise<void> {
+ const url = new URL("private/auth", this.baseUrl);
+ const res = await this.httpClient.fetch(url.href, {
+ method: "POST",
+ body: auth,
+ headers: this.makeAuthHeader(),
+ });
+ await expectSuccessResponseOrThrow(res);
+ }
+
+ async getPrivateInstanceInfo(): Promise<any> {
+ const url = new URL("private", this.baseUrl);
+ const resp = await this.httpClient.fetch(url.href, {
+ method: "GET",
+ headers: this.makeAuthHeader(),
+ });
+ return await resp.json();
+ }
+
+ async deleteInstance(instanceId: string) {
+ const url = new URL(`management/instances/${instanceId}`, this.baseUrl);
+ const resp = await this.httpClient.fetch(url.href, {
+ method: "DELETE",
+ headers: this.makeAuthHeader(),
+ });
+ await expectSuccessResponseOrThrow(resp);
+ }
+
+ async createInstance(req: MerchantInstanceConfig): Promise<void> {
+ const url = new URL("management/instances", this.baseUrl);
+ await this.httpClient.fetch(url.href, {
+ method: "POST",
+ body: req,
+ headers: this.makeAuthHeader(),
+ });
+ }
+
+ async getInstances(): Promise<TalerMerchantApi.InstancesResponse> {
+ const url = new URL("management/instances", this.baseUrl);
+ const resp = await this.httpClient.fetch(url.href, {
+ headers: this.makeAuthHeader(),
+ });
+ return readSuccessResponseJsonOrThrow(resp, codecForAny());
+ }
+
+ async getInstanceFullDetails(instanceId: string): Promise<any> {
+ const url = new URL(`management/instances/${instanceId}`, this.baseUrl);
+ try {
+ const resp = await this.httpClient.fetch(url.href, {
+ headers: this.makeAuthHeader(),
+ });
+ return resp.json();
+ } catch (e) {
+ throw e;
+ }
+ }
+
+ async createOrder(
+ req: TalerMerchantApi.PostOrderRequest,
+ ): Promise<TalerMerchantApi.PostOrderResponse> {
+ let url = new URL("private/orders", this.baseUrl);
+ const resp = await this.httpClient.fetch(url.href, {
+ method: "POST",
+ body: req,
+ headers: this.makeAuthHeader(),
+ });
+ return readSuccessResponseJsonOrThrow(resp, codecForPostOrderResponse());
+ }
+
+ async deleteOrder(req: { orderId: string; force?: boolean }): Promise<void> {
+ let url = new URL(`private/orders/${req.orderId}`, this.baseUrl);
+ if (req.force) {
+ url.searchParams.set("force", "yes");
+ }
+ const resp = await this.httpClient.fetch(url.href, {
+ method: "DELETE",
+ body: req,
+ headers: this.makeAuthHeader(),
+ });
+ if (resp.status !== 204) {
+ throw Error(`failed to delete order (status ${resp.status})`);
+ }
+ }
+
+ async queryPrivateOrderStatus(
+ query: PrivateOrderStatusQuery,
+ ): Promise<TalerMerchantApi.MerchantOrderStatusResponse> {
+ const reqUrl = new URL(`private/orders/${query.orderId}`, this.baseUrl);
+ if (query.sessionId) {
+ reqUrl.searchParams.set("session_id", query.sessionId);
+ }
+ const resp = await this.httpClient.fetch(reqUrl.href, {
+ headers: this.makeAuthHeader(),
+ });
+ return readSuccessResponseJsonOrThrow(
+ resp,
+ codecForMerchantOrderPrivateStatusResponse(),
+ );
+ }
+
+ async giveRefund(r: {
+ instance: string;
+ orderId: string;
+ amount: string;
+ justification: string;
+ }): Promise<{ talerRefundUri: string }> {
+ const reqUrl = new URL(`private/orders/${r.orderId}/refund`, this.baseUrl);
+ const resp = await this.httpClient.fetch(reqUrl.href, {
+ method: "POST",
+ body: {
+ refund: r.amount,
+ reason: r.justification,
+ },
+ });
+ const respBody = await resp.json();
+ return {
+ talerRefundUri: respBody.taler_refund_uri,
+ };
+ }
+
+ async createTemplate(req: TalerMerchantApi.MerchantTemplateAddDetails) {
+ let url = new URL("private/templates", this.baseUrl);
+ const resp = await this.httpClient.fetch(url.href, {
+ method: "POST",
+ body: req,
+ headers: this.makeAuthHeader(),
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ case HttpStatusCode.NoContent:
+ return opEmptySuccess(resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ async getTemplate(templateId: string) {
+ let url = new URL(`private/templates/${templateId}`, this.baseUrl);
+ const resp = await this.httpClient.fetch(url.href, {
+ method: "GET",
+ headers: this.makeAuthHeader(),
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForAny());
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ isCompatible(version: string): boolean {
+ const compare = LibtoolVersion.compare(this.PROTOCOL_VERSION, version);
+ return compare?.compatible ?? false;
+ }
+ /**
+ * https://docs.taler.net/core/api-merchant.html#get--config
+ *
+ */
+ async getConfig(): Promise<OperationOk<TalerMerchantApi.VersionResponse>> {
+ const url = new URL(`config`, this.baseUrl);
+ const resp = await this.httpClient.fetch(url.href, {
+ method: "GET",
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForMerchantConfig());
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ async createOtpDevice(
+ req: OtpDeviceAddDetails,
+ ): Promise<OperationOk<void> | OperationFail<HttpStatusCode.NotFound>> {
+ let url = new URL("private/otp-devices", this.baseUrl);
+ const resp = await this.httpClient.fetch(url.href, {
+ method: "POST",
+ body: req,
+ headers: this.makeAuthHeader(),
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ case HttpStatusCode.NoContent:
+ return opEmptySuccess(resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ private makeAuthHeader(): Record<string, string> {
+ switch (this.auth.method) {
+ case "external":
+ return {};
+ case "token":
+ return {
+ Authorization: `Bearer ${this.auth.token}`,
+ };
+ }
+ }
+}
diff --git a/packages/taler-util/src/ReserveStatus.ts b/packages/taler-util/src/ReserveStatus.ts
index be9fa9e8e..3a30755ce 100644
--- a/packages/taler-util/src/ReserveStatus.ts
+++ b/packages/taler-util/src/ReserveStatus.ts
@@ -21,17 +21,9 @@
/**
* Imports.
*/
-import {
- codecForString,
- buildCodecForObject,
- codecForList,
- Codec,
-} from "./codec.js";
+import { codecForAmountString } from "./amounts.js";
+import { Codec, buildCodecForObject } from "./codec.js";
import { AmountString } from "./taler-types.js";
-import {
- ReserveTransaction,
- codecForReserveTransaction,
-} from "./ReserveTransaction.js";
/**
* Status of a reserve.
@@ -47,5 +39,5 @@ export interface ReserveStatus {
export const codecForReserveStatus = (): Codec<ReserveStatus> =>
buildCodecForObject<ReserveStatus>()
- .property("balance", codecForString())
+ .property("balance", codecForAmountString())
.build("ReserveStatus");
diff --git a/packages/taler-util/src/ReserveTransaction.ts b/packages/taler-util/src/ReserveTransaction.ts
index 5d3f86b1a..7a3c69d07 100644
--- a/packages/taler-util/src/ReserveTransaction.ts
+++ b/packages/taler-util/src/ReserveTransaction.ts
@@ -23,6 +23,7 @@
/**
* Imports.
*/
+import { codecForAmountString } from "./amounts.js";
import {
codecForString,
buildCodecForObject,
@@ -189,18 +190,18 @@ export type ReserveTransaction =
export const codecForReserveWithdrawTransaction =
(): Codec<ReserveWithdrawTransaction> =>
buildCodecForObject<ReserveWithdrawTransaction>()
- .property("amount", codecForString())
+ .property("amount", codecForAmountString())
.property("h_coin_envelope", codecForString())
.property("h_denom_pub", codecForString())
.property("reserve_sig", codecForString())
.property("type", codecForConstString(ReserveTransactionType.Withdraw))
- .property("withdraw_fee", codecForString())
+ .property("withdraw_fee", codecForAmountString())
.build("ReserveWithdrawTransaction");
export const codecForReserveCreditTransaction =
(): Codec<ReserveCreditTransaction> =>
buildCodecForObject<ReserveCreditTransaction>()
- .property("amount", codecForString())
+ .property("amount", codecForAmountString())
.property("sender_account_url", codecForString())
.property("timestamp", codecForTimestamp)
.property("wire_reference", codecForNumber())
@@ -210,8 +211,8 @@ export const codecForReserveCreditTransaction =
export const codecForReserveClosingTransaction =
(): Codec<ReserveClosingTransaction> =>
buildCodecForObject<ReserveClosingTransaction>()
- .property("amount", codecForString())
- .property("closing_fee", codecForString())
+ .property("amount", codecForAmountString())
+ .property("closing_fee", codecForAmountString())
.property("exchange_pub", codecForString())
.property("exchange_sig", codecForString())
.property("h_wire", codecForString())
@@ -223,7 +224,7 @@ export const codecForReserveClosingTransaction =
export const codecForReserveRecoupTransaction =
(): Codec<ReserveRecoupTransaction> =>
buildCodecForObject<ReserveRecoupTransaction>()
- .property("amount", codecForString())
+ .property("amount", codecForAmountString())
.property("coin_pub", codecForString())
.property("exchange_pub", codecForString())
.property("exchange_sig", codecForString())
diff --git a/packages/taler-util/src/TaskThrottler.ts b/packages/taler-util/src/TaskThrottler.ts
new file mode 100644
index 000000000..e4fb82171
--- /dev/null
+++ b/packages/taler-util/src/TaskThrottler.ts
@@ -0,0 +1,160 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 GNUnet e.V.
+
+ 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.
+
+ 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 { Logger } from "./logging.js";
+import { AbsoluteTime, Duration } from "./time.js";
+
+/**
+ * Implementation of token bucket throttling.
+ */
+
+/**
+ * Logger.
+ */
+const logger = new Logger("OperationThrottler.ts");
+
+/**
+ * Maximum request per second, per origin.
+ */
+const MAX_PER_SECOND = 100;
+
+/**
+ * Maximum request per minute, per origin.
+ */
+const MAX_PER_MINUTE = 500;
+
+/**
+ * Maximum request per hour, per origin.
+ */
+const MAX_PER_HOUR = 2000;
+
+/**
+ * Throttling state for one task.
+ */
+class TaskState {
+ tokensSecond: number = MAX_PER_SECOND;
+ tokensMinute: number = MAX_PER_MINUTE;
+ tokensHour: number = MAX_PER_HOUR;
+ lastUpdate = AbsoluteTime.now();
+
+ private refill(): void {
+ const now = AbsoluteTime.now();
+ if (AbsoluteTime.cmp(now, this.lastUpdate) < 0) {
+ // Did the system time change?
+ this.lastUpdate = now;
+ return;
+ }
+ const d = AbsoluteTime.difference(now, this.lastUpdate);
+ if (d.d_ms === "forever") {
+ throw Error("assertion failed");
+ }
+ this.tokensSecond = Math.min(
+ MAX_PER_SECOND,
+ this.tokensSecond + d.d_ms / 1000,
+ );
+ this.tokensMinute = Math.min(
+ MAX_PER_MINUTE,
+ this.tokensMinute + d.d_ms / 1000 / 60,
+ );
+ this.tokensHour = Math.min(
+ MAX_PER_HOUR,
+ this.tokensHour + d.d_ms / 1000 / 60 / 60,
+ );
+ this.lastUpdate = now;
+ }
+
+ /**
+ * Return true if the request for this origin should be throttled.
+ * Otherwise, take a token out of the respective buckets.
+ */
+ applyThrottle(): boolean {
+ this.refill();
+ if (this.tokensSecond < 1) {
+ logger.warn("request throttled (per second limit exceeded)");
+ return true;
+ }
+ if (this.tokensMinute < 1) {
+ logger.warn("request throttled (per minute limit exceeded)");
+ return true;
+ }
+ if (this.tokensHour < 1) {
+ logger.warn("request throttled (per hour limit exceeded)");
+ return true;
+ }
+ this.tokensSecond--;
+ this.tokensMinute--;
+ this.tokensHour--;
+ return false;
+ }
+}
+
+/**
+ * Request throttler, used as a "last layer of defense" when some
+ * other part of the re-try logic is broken and we're sending too
+ * many requests to the same exchange/bank/merchant.
+ */
+export class TaskThrottler {
+ private perTaskInfo: { [taskId: string]: TaskState } = {};
+
+ /**
+ * Get the throttling state for an origin, or
+ * initialize if no state is associated with the
+ * origin yet.
+ */
+ private getState(origin: string): TaskState {
+ const s = this.perTaskInfo[origin];
+ if (s) {
+ return s;
+ }
+ const ns = (this.perTaskInfo[origin] = new TaskState());
+ return ns;
+ }
+
+ /**
+ * Apply throttling to a request.
+ *
+ * @returns whether the request should be throttled.
+ */
+ applyThrottle(taskId: string): boolean {
+ for (let [k, v] of Object.entries(this.perTaskInfo)) {
+ // Remove throttled tasks that haven't seen an update in more than one hour.
+ if (
+ Duration.cmp(
+ AbsoluteTime.difference(v.lastUpdate, AbsoluteTime.now()),
+ Duration.fromSpec({ hours: 1 }),
+ ) > 1
+ ) {
+ delete this.perTaskInfo[k];
+ }
+ }
+ return this.getState(taskId).applyThrottle();
+ }
+
+ /**
+ * Get the throttle statistics for a particular URL.
+ */
+ getThrottleStats(taskId: string): Record<string, unknown> {
+ const state = this.getState(taskId);
+ return {
+ tokensHour: state.tokensHour,
+ tokensMinute: state.tokensMinute,
+ tokensSecond: state.tokensSecond,
+ maxTokensHour: MAX_PER_HOUR,
+ maxTokensMinute: MAX_PER_MINUTE,
+ maxTokensSecond: MAX_PER_SECOND,
+ };
+ }
+}
diff --git a/packages/taler-util/src/amounts.test.ts b/packages/taler-util/src/amounts.test.ts
index 064023e2d..449a6319a 100644
--- a/packages/taler-util/src/amounts.test.ts
+++ b/packages/taler-util/src/amounts.test.ts
@@ -17,6 +17,7 @@
import test from "ava";
import { Amounts, AmountJson, amountMaxValue } from "./amounts.js";
+import { AmountString } from "./taler-types.js";
const jAmt = (
value: number,
@@ -120,21 +121,71 @@ test("amount parsing", (t) => {
});
test("amount stringification", (t) => {
- t.is(Amounts.stringify(jAmt(0, 0, "TESTKUDOS")), "TESTKUDOS:0");
- t.is(Amounts.stringify(jAmt(4, 94000000, "TESTKUDOS")), "TESTKUDOS:4.94");
- t.is(Amounts.stringify(jAmt(0, 10000000, "TESTKUDOS")), "TESTKUDOS:0.1");
- t.is(Amounts.stringify(jAmt(0, 1, "TESTKUDOS")), "TESTKUDOS:0.00000001");
- t.is(Amounts.stringify(jAmt(5, 0, "TESTKUDOS")), "TESTKUDOS:5");
+ t.is(
+ Amounts.stringify(jAmt(0, 0, "TESTKUDOS")),
+ "TESTKUDOS:0" as AmountString,
+ );
+ t.is(
+ Amounts.stringify(jAmt(4, 94000000, "TESTKUDOS")),
+ "TESTKUDOS:4.94" as AmountString,
+ );
+ t.is(
+ Amounts.stringify(jAmt(0, 10000000, "TESTKUDOS")),
+ "TESTKUDOS:0.1" as AmountString,
+ );
+ t.is(
+ Amounts.stringify(jAmt(0, 1, "TESTKUDOS")),
+ "TESTKUDOS:0.00000001" as AmountString,
+ );
+ t.is(
+ Amounts.stringify(jAmt(5, 0, "TESTKUDOS")),
+ "TESTKUDOS:5" as AmountString,
+ );
// denormalized
- t.is(Amounts.stringify(jAmt(1, 100000000, "TESTKUDOS")), "TESTKUDOS:2");
+ t.is(
+ Amounts.stringify(jAmt(1, 100000000, "TESTKUDOS")),
+ "TESTKUDOS:2" as AmountString,
+ );
t.pass();
});
test("amount multiplication", (t) => {
- t.is(Amounts.stringify(Amounts.mult(sAmt("EUR:1.11"), 0).amount), "EUR:0");
- t.is(Amounts.stringify(Amounts.mult(sAmt("EUR:1.11"), 1).amount), "EUR:1.11");
- t.is(Amounts.stringify(Amounts.mult(sAmt("EUR:1.11"), 2).amount), "EUR:2.22");
- t.is(Amounts.stringify(Amounts.mult(sAmt("EUR:1.11"), 3).amount), "EUR:3.33");
- t.is(Amounts.stringify(Amounts.mult(sAmt("EUR:1.11"), 4).amount), "EUR:4.44");
- t.is(Amounts.stringify(Amounts.mult(sAmt("EUR:1.11"), 5).amount), "EUR:5.55");
+ t.is(
+ Amounts.stringify(Amounts.mult(sAmt("EUR:1.11"), 0).amount),
+ "EUR:0" as AmountString,
+ );
+ t.is(
+ Amounts.stringify(Amounts.mult(sAmt("EUR:1.11"), 1).amount),
+ "EUR:1.11" as AmountString,
+ );
+ t.is(
+ Amounts.stringify(Amounts.mult(sAmt("EUR:1.11"), 2).amount),
+ "EUR:2.22" as AmountString,
+ );
+ t.is(
+ Amounts.stringify(Amounts.mult(sAmt("EUR:1.11"), 3).amount),
+ "EUR:3.33" as AmountString,
+ );
+ t.is(
+ Amounts.stringify(Amounts.mult(sAmt("EUR:1.11"), 4).amount),
+ "EUR:4.44" as AmountString,
+ );
+ t.is(
+ Amounts.stringify(Amounts.mult(sAmt("EUR:1.11"), 5).amount),
+ "EUR:5.55" as AmountString,
+ );
+});
+
+test("amount division", (t) => {
+ t.is(Amounts.divmod("EUR:5", "EUR:1").quotient, 5);
+ t.is(
+ Amounts.stringify(Amounts.divmod("EUR:5", "EUR:1").remainder),
+ "EUR:0" as AmountString,
+ );
+
+ t.is(Amounts.divmod("EUR:5", "EUR:2").quotient, 2);
+ t.is(
+ Amounts.stringify(Amounts.divmod("EUR:5", "EUR:2").remainder),
+ "EUR:1" as AmountString,
+ );
});
diff --git a/packages/taler-util/src/amounts.ts b/packages/taler-util/src/amounts.ts
index 991b13912..82a3d3b68 100644
--- a/packages/taler-util/src/amounts.ts
+++ b/packages/taler-util/src/amounts.ts
@@ -22,11 +22,15 @@
* Imports.
*/
import {
+ Codec,
+ Context,
+ DecodingError,
buildCodecForObject,
- codecForString,
codecForNumber,
- Codec,
+ codecForString,
+ renderContext,
} from "./codec.js";
+import { CurrencySpecification } from "./index.js";
import { AmountString } from "./taler-types.js";
/**
@@ -47,6 +51,11 @@ export const amountFractionalLength = 8;
export const amountMaxValue = 2 ** 52;
/**
+ * Separator character between integer and fractional
+ */
+export const FRAC_SEPARATOR = ".";
+
+/**
* Non-negative financial amount. Fractional values are expressed as multiples
* of 1e-8.
*/
@@ -67,6 +76,48 @@ export interface AmountJson {
readonly currency: string;
}
+/**
+ * Immutable amount.
+ */
+export class Amount {
+ static from(a: AmountLike): Amount {
+ return new Amount(Amounts.parseOrThrow(a), 0);
+ }
+
+ static zeroOfCurrency(currency: string): Amount {
+ return new Amount(Amounts.zeroOfCurrency(currency), 0);
+ }
+
+ add(...a: AmountLike[]): Amount {
+ if (this.saturated) {
+ return this;
+ }
+ const r = Amounts.add(this.val, ...a);
+ return new Amount(r.amount, r.saturated ? 1 : 0);
+ }
+
+ mult(n: number): Amount {
+ if (this.saturated) {
+ return this;
+ }
+ const r = Amounts.mult(this, n);
+ return new Amount(r.amount, r.saturated ? 1 : 0);
+ }
+
+ toJson(): AmountJson {
+ return { ...this.val };
+ }
+
+ toString(): AmountString {
+ return Amounts.stringify(this.val);
+ }
+
+ private constructor(
+ private val: AmountJson,
+ private saturated: number,
+ ) {}
+}
+
export const codecForAmountJson = (): Codec<AmountJson> =>
buildCodecForObject<AmountJson>()
.property("currency", codecForString())
@@ -74,7 +125,23 @@ export const codecForAmountJson = (): Codec<AmountJson> =>
.property("fraction", codecForNumber())
.build("AmountJson");
-export const codecForAmountString = (): Codec<AmountString> => codecForString();
+export function codecForAmountString(): Codec<AmountString> {
+ return {
+ decode(x: any, c?: Context): AmountString {
+ if (typeof x !== "string") {
+ throw new DecodingError(
+ `expected string at ${renderContext(c)} but got ${typeof x}`,
+ );
+ }
+ if (Amounts.parse(x) === undefined) {
+ throw new DecodingError(
+ `invalid amount at ${renderContext(c)} got "${x}"`,
+ );
+ }
+ return x as AmountString;
+ },
+ };
+}
/**
* Result of a possibly overflowing operation.
@@ -93,7 +160,12 @@ export interface Result {
/**
* Type for things that are treated like amounts.
*/
-export type AmountLike = AmountString | AmountJson;
+export type AmountLike = string | AmountString | AmountJson | Amount;
+
+export interface DivmodResult {
+ quotient: number;
+ remainder: AmountJson;
+}
/**
* Helper class for dealing with amounts.
@@ -132,9 +204,37 @@ export class Amounts {
if (typeof amt === "string") {
return Amounts.parseOrThrow(amt);
}
+ if (amt instanceof Amount) {
+ return amt.toJson();
+ }
return amt;
}
+ static divmod(a1: AmountLike, a2: AmountLike): DivmodResult {
+ const am1 = Amounts.jsonifyAmount(a1);
+ const am2 = Amounts.jsonifyAmount(a2);
+ if (am1.currency != am2.currency) {
+ throw Error(`incompatible currency (${am1.currency} vs${am2.currency})`);
+ }
+
+ const x1 =
+ BigInt(am1.value) * BigInt(amountFractionalBase) + BigInt(am1.fraction);
+ const x2 =
+ BigInt(am2.value) * BigInt(amountFractionalBase) + BigInt(am2.fraction);
+
+ const quotient = x1 / x2;
+ const remainderScaled = x1 % x2;
+
+ return {
+ quotient: Number(quotient),
+ remainder: {
+ currency: am1.currency,
+ value: Number(remainderScaled / BigInt(amountFractionalBase)),
+ fraction: Number(remainderScaled % BigInt(amountFractionalBase)),
+ },
+ };
+ }
+
static sum(amounts: AmountLike[]): Result {
if (amounts.length <= 0) {
throw Error("can't sum zero amounts");
@@ -303,7 +403,8 @@ export class Amounts {
/**
* Check if an amount is non-zero.
*/
- static isNonZero(a: AmountJson): boolean {
+ static isNonZero(a: AmountLike): boolean {
+ a = Amounts.jsonifyAmount(a);
return a.value > 0 || a.fraction > 0;
}
@@ -313,14 +414,24 @@ export class Amounts {
}
/**
+ * Check whether a string is a valid currency for a Taler amount.
+ */
+ static isCurrency(s: string): boolean {
+ return /^[a-zA-Z]{1,11}$/.test(s);
+ }
+
+ /**
* Parse an amount like 'EUR:20.5' for 20 Euros and 50 ct.
+ *
+ * Currency name size limit is 11 of ASCII letters
+ * Fraction size limit is 8
*/
static parse(s: string): AmountJson | undefined {
- const res = s.match(/^([a-zA-Z0-9_*-]+):([0-9]+)([.][0-9]+)?$/);
+ const res = s.match(/^([a-zA-Z]{1,11}):([0-9]+)([.][0-9]{1,8})?$/);
if (!res) {
return undefined;
}
- const tail = res[3] || ".0";
+ const tail = res[3] || FRAC_SEPARATOR + "0";
if (tail.length > amountFractionalLength + 1) {
return undefined;
}
@@ -340,6 +451,9 @@ export class Amounts {
* throw if the input is not a valid amount.
*/
static parseOrThrow(s: AmountLike): AmountJson {
+ if (s instanceof Amount) {
+ return s.toJson();
+ }
if (typeof s === "object") {
if (typeof s.currency !== "string") {
throw Error("invalid amount object");
@@ -362,20 +476,6 @@ export class Amounts {
}
}
- /**
- * Convert a float to a Taler amount.
- * Loss of precision possible.
- */
- static fromFloat(floatVal: number, currency: string): AmountJson {
- return {
- currency,
- fraction: Math.floor(
- (floatVal - Math.floor(floatVal)) * amountFractionalBase,
- ),
- value: Math.floor(floatVal),
- };
- }
-
static min(a: AmountLike, b: AmountLike): AmountJson {
const cr = Amounts.cmp(a, b);
if (cr >= 0) {
@@ -397,7 +497,7 @@ export class Amounts {
static mult(a: AmountLike, n: number): Result {
a = this.jsonifyAmount(a);
if (!Number.isInteger(n)) {
- throw Error("amount can only be multipied by an integer");
+ throw Error("amount can only be multiplied by an integer");
}
if (n < 0) {
throw Error("amount can only be multiplied by a positive integer");
@@ -449,19 +549,23 @@ export class Amounts {
* Convert to standard human-readable string representation that's
* also used in JSON formats.
*/
- static stringify(a: AmountLike): string {
+ static stringify(a: AmountLike): AmountString {
a = Amounts.jsonifyAmount(a);
const s = this.stringifyValue(a);
- return `${a.currency}:${s}`;
+ return `${a.currency}:${s}` as AmountString;
}
- static isSameCurrency(a1: AmountLike, a2: AmountLike): boolean {
+ static amountHasSameCurrency(a1: AmountLike, a2: AmountLike): boolean {
const x1 = this.jsonifyAmount(a1);
const x2 = this.jsonifyAmount(a2);
return x1.currency.toUpperCase() === x2.currency.toUpperCase();
}
+ static isSameCurrency(curr1: string, curr2: string): boolean {
+ return curr1.toLowerCase() === curr2.toLowerCase();
+ }
+
static stringifyValue(a: AmountLike, minFractional = 0): string {
const aJ = Amounts.jsonifyAmount(a);
const av = aJ.value + Math.floor(aJ.fraction / amountFractionalBase);
@@ -469,7 +573,7 @@ export class Amounts {
let s = av.toString();
if (af || minFractional) {
- s = s + ".";
+ s = s + FRAC_SEPARATOR;
let n = af;
for (let i = 0; i < amountFractionalLength; i++) {
if (!n && i >= minFractional) {
@@ -504,4 +608,77 @@ export class Amounts {
}
return amountFractionalLength - i + 1;
}
+
+ static stringifyValueWithSpec(
+ value: AmountJson,
+ spec: CurrencySpecification,
+ ): { currency: string; normal: string; small?: string } {
+ const strValue = Amounts.stringifyValue(value);
+ const pos = strValue.indexOf(FRAC_SEPARATOR);
+ const originalPosition = pos < 0 ? strValue.length : pos;
+
+ let currency = value.currency;
+ const names = Object.keys(spec.alt_unit_names);
+ let FRAC_POS_NEW_POSITION = originalPosition;
+ //find symbol
+ //FIXME: this should be based on a cache to speed up
+ if (names.length > 0) {
+ let unitIndex: string = "0"; //default entry by DD51
+ names.forEach((index) => {
+ const i = Number.parseInt(index, 10);
+ if (Number.isNaN(i)) return; //skip
+ if (originalPosition - i <= 0) return; //too big
+ if (originalPosition - i < FRAC_POS_NEW_POSITION) {
+ FRAC_POS_NEW_POSITION = originalPosition - i;
+ unitIndex = index;
+ }
+ });
+ currency = spec.alt_unit_names[unitIndex];
+ }
+
+ if (originalPosition === FRAC_POS_NEW_POSITION) {
+ const { normal, small } = splitNormalAndSmall(
+ strValue,
+ originalPosition,
+ spec,
+ );
+ return { currency, normal, small };
+ }
+
+ const intPart = strValue.substring(0, originalPosition);
+ const fracPArt = strValue.substring(originalPosition + 1);
+ //indexSize is always smaller than originalPosition
+ const newValue =
+ intPart.substring(0, FRAC_POS_NEW_POSITION) +
+ FRAC_SEPARATOR +
+ intPart.substring(FRAC_POS_NEW_POSITION) +
+ fracPArt;
+ const { normal, small } = splitNormalAndSmall(
+ newValue,
+ FRAC_POS_NEW_POSITION,
+ spec,
+ );
+ return { currency, normal, small };
+ }
+}
+
+function splitNormalAndSmall(
+ decimal: string,
+ fracSeparatorIndex: number,
+ spec: CurrencySpecification,
+): { normal: string; small?: string } {
+ let normal: string;
+ let small: string | undefined;
+ if (
+ decimal.length - fracSeparatorIndex - 1 >
+ spec.num_fractional_normal_digits
+ ) {
+ const limit = fracSeparatorIndex + spec.num_fractional_normal_digits + 1;
+ normal = decimal.substring(0, limit);
+ small = decimal.substring(limit);
+ } else {
+ normal = decimal;
+ small = undefined;
+ }
+ return { normal, small };
}
diff --git a/packages/taler-util/src/argon2-impl.missing.ts b/packages/taler-util/src/argon2-impl.missing.ts
new file mode 100644
index 000000000..2e175bc75
--- /dev/null
+++ b/packages/taler-util/src/argon2-impl.missing.ts
@@ -0,0 +1,9 @@
+export async function HashArgon2idImpl(
+ password: Uint8Array,
+ salt: Uint8Array,
+ iterations: number,
+ memorySize: number,
+ hashLength: number,
+): Promise<Uint8Array> {
+ throw new Error("Method not implemented.");
+}
diff --git a/packages/taler-util/src/argon2-impl.wasm.ts b/packages/taler-util/src/argon2-impl.wasm.ts
new file mode 100644
index 000000000..d1a36c4fe
--- /dev/null
+++ b/packages/taler-util/src/argon2-impl.wasm.ts
@@ -0,0 +1,19 @@
+import { argon2id } from "hash-wasm";
+
+export async function HashArgon2idImpl(
+ password: Uint8Array,
+ salt: Uint8Array,
+ iterations: number,
+ memorySize: number,
+ hashLength: number,
+): Promise<Uint8Array> {
+ return await argon2id({
+ password: password,
+ salt: salt,
+ iterations: iterations,
+ memorySize: memorySize,
+ hashLength: hashLength,
+ parallelism: 1,
+ outputType: "binary",
+ });
+}
diff --git a/packages/taler-util/src/argon2.ts b/packages/taler-util/src/argon2.ts
new file mode 100644
index 000000000..aebfb6962
--- /dev/null
+++ b/packages/taler-util/src/argon2.ts
@@ -0,0 +1,17 @@
+import * as impl from "#argon2-impl";
+
+export async function hashArgon2id(
+ password: Uint8Array,
+ salt: Uint8Array,
+ iterations: number,
+ memorySize: number,
+ hashLength: number,
+): Promise<Uint8Array> {
+ return await impl.HashArgon2idImpl(
+ password,
+ salt,
+ iterations,
+ memorySize,
+ hashLength,
+ );
+}
diff --git a/packages/taler-util/src/backup-types.ts b/packages/taler-util/src/backup-types.ts
index b3c6b5515..8c38b70a6 100644
--- a/packages/taler-util/src/backup-types.ts
+++ b/packages/taler-util/src/backup-types.ts
@@ -14,405 +14,14 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-/**
- * Type declarations for the backup content format.
- *
- * Contains some redundancy with the other type declarations,
- * as the backup schema must remain very stable and should be self-contained.
- *
- * Future:
- * 1. Ghost spends (coin unexpectedly spent by a wallet with shared data)
- * 2. Ghost withdrawals (reserve unexpectedly emptied by another wallet with shared data)
- * 3. Track losses through re-denomination of payments/refreshes
- * 4. (Feature:) Payments to own bank account and P2P-payments need to be backed up
- * 5. Track last/next update time, so on restore we need to do less work
- * 6. Currency render preferences?
- *
- * Questions:
- * 1. What happens when two backups are merged that have
- * the same coin in different refresh groups?
- * => Both are added, one will eventually fail
- * 2. Should we make more information forgettable? I.e. is
- * the coin selection still relevant for a purchase after the coins
- * are legally expired?
- * => Yes, still needs to be implemented
- * 3. What about re-denominations / re-selection of payment coins?
- * Is it enough to store a clock value for the selection?
- * => Coin derivation should also consider denom pub hash
- *
- * General considerations / decisions:
- * 1. Information about previously occurring errors and
- * retries is never backed up.
- * 2. The ToS text of an exchange is never backed up.
- * 3. Derived information is never backed up (hashed values, public keys
- * when we know the private key).
- *
- * Problems:
- *
- * Withdrawal group fork/merging loses money:
- * - Before the withdrawal happens, wallet forks into two backups.
- * - Both wallets need to re-denominate the withdrawal (unlikely but possible).
- * - Because the backup doesn't store planchets where a withdrawal was attempted,
- * after merging some money will be list.
- * - Fix: backup withdrawal objects also store planchets where withdrawal has been attempted
- *
- * @author Florian Dold <dold@taler.net>
- */
-
-/**
- * Imports.
- */
-import { DenominationPubKey, UnblindedSignature } from "./taler-types.js";
-import { TalerProtocolDuration, TalerProtocolTimestamp } from "./time.js";
-
-export const BACKUP_TAG = "gnu-taler-wallet-backup-content" as const;
-/**
- * Major version. Each increment means a backwards-incompatible change.
- * Typically this means that a custom converter needs to be written.
- */
-export const BACKUP_VERSION_MAJOR = 1 as const;
-
-/**
- * Minor version. Each increment means that information is added to the backup
- * in a backwards-compatible way.
- *
- * Wallets can always import a smaller minor version than their own backup code version.
- * When importing a bigger version, data loss is possible and the user should be urged to
- * upgrade their wallet first.
- */
-export const BACKUP_VERSION_MINOR = 1 as const;
-
-/**
- * Type alias for strings that are to be treated like amounts.
- */
-type BackupAmountString = string;
-
-/**
- * A human-recognizable identifier here that is
- * reasonable unique and assigned the first time the wallet is
- * started/installed, such as:
- *
- * `${wallet-implementation} ${os} ${hostname} (${short-uid})`
- * => e.g. "GNU Taler Android iceking ABC123"
- */
-type DeviceIdString = string;
-
-/**
- * Contract terms JSON.
- */
-type RawContractTerms = any;
-
-/**
- * Unique identifier for an operation, used to either (a) reference
- * the operation in a tombstone (b) disambiguate conflicting writes.
- */
-type OperationUid = string;
-
-/**
- * Content of the backup.
- *
- * The contents of the wallet must be serialized in a deterministic
- * way across implementations, so that the normalized backup content
- * JSON is identical when the wallet's content is identical.
- */
-export interface WalletBackupContentV1 {
- /**
- * Magic constant to identify that this is a backup content JSON.
- */
- schema_id: typeof BACKUP_TAG;
-
- /**
- * Version of the schema.
- */
- schema_version: typeof BACKUP_VERSION_MAJOR;
-
- minor_version: number;
-
- /**
- * Root public key of the wallet. This field is present as
- * a sanity check if the backup content JSON is loaded from file.
- */
- wallet_root_pub: string;
-
- /**
- * Current device identifier that "owns" the backup.
- *
- * This identifier allows one wallet to notice when another
- * wallet is "alive" and connected to the same sync provider.
- */
- current_device_id: DeviceIdString;
-
- /**
- * Timestamp of the backup.
- *
- * This timestamp should only be advanced if the content
- * of the backup changes.
- */
- timestamp: TalerProtocolTimestamp;
-
- /**
- * Per-exchange data sorted by exchange master public key.
- *
- * Sorted by the exchange public key.
- */
- exchanges: BackupExchange[];
-
- exchange_details: BackupExchangeDetails[];
-
- /**
- * Withdrawal groups.
- *
- * Sorted by the withdrawal group ID.
- */
- withdrawal_groups: BackupWithdrawalGroup[];
-
- /**
- * Grouped refresh sessions.
- *
- * Sorted by the refresh group ID.
- */
- refresh_groups: BackupRefreshGroup[];
-
- /**
- * Tips.
- *
- * Sorted by the wallet tip ID.
- */
- tips: BackupTip[];
-
- /**
- * Accepted purchases.
- *
- * Sorted by the proposal ID.
- */
- purchases: BackupPurchase[];
-
- /**
- * All backup providers. Backup providers
- * in this list should be considered "active".
- *
- * Sorted by the provider base URL.
- */
- backup_providers: BackupBackupProvider[];
+import { AmountString } from "./taler-types.js";
- /**
- * Recoup groups.
- */
- recoup_groups: BackupRecoupGroup[];
-
- /**
- * Trusted auditors, either for official (3 letter) or local (4-12 letter)
- * currencies.
- *
- * Auditors are sorted by their canonicalized base URL.
- */
- trusted_auditors: { [currency: string]: BackupTrustAuditor[] };
-
- /**
- * Trusted exchange. Only applicable for local currencies (4-12 letter currency code).
- *
- * Exchanges are sorted by their canonicalized base URL.
- */
- trusted_exchanges: { [currency: string]: BackupTrustExchange[] };
-
- /**
- * Interning table for forgettable values of contract terms.
- *
- * Used to reduce storage space, as many forgettable items (product image,
- * addresses, etc.) might be shared among many contract terms.
- */
- intern_table: { [hash: string]: any };
-
- /**
- * Permanent error reports.
- */
- error_reports: BackupErrorReport[];
-
- /**
- * Deletion tombstones. Lexically sorted.
- */
- tombstones: Tombstone[];
-}
-
-export enum BackupOperationStatus {
- Cancelled = "cancelled",
- Finished = "finished",
- Pending = "pending",
-}
-
-export enum BackupWgType {
- BankManual = "bank-manual",
- BankIntegrated = "bank-integrated",
- PeerPullCredit = "peer-pull-credit",
- PeerPushCredit = "peer-push-credit",
- Recoup = "recoup",
-}
-
-export type BackupWgInfo =
- | {
- type: BackupWgType.BankManual;
- }
- | {
- type: BackupWgType.BankIntegrated;
- taler_withdraw_uri: string;
-
- /**
- * URL that the user can be redirected to, and allows
- * them to confirm (or abort) the bank-integrated withdrawal.
- */
- confirm_url?: string;
-
- /**
- * Exchange payto URI that the bank will use to fund the reserve.
- */
- exchange_payto_uri: string;
-
- /**
- * Time when the information about this reserve was posted to the bank.
- *
- * Only applies if bankWithdrawStatusUrl is defined.
- *
- * Set to undefined if that hasn't happened yet.
- */
- timestamp_reserve_info_posted?: TalerProtocolTimestamp;
-
- /**
- * Time when the reserve was confirmed by the bank.
- *
- * Set to undefined if not confirmed yet.
- */
- timestamp_bank_confirmed?: TalerProtocolTimestamp;
- }
- | {
- type: BackupWgType.PeerPullCredit;
- contract_terms: any;
- contract_priv: string;
- }
- | {
- type: BackupWgType.PeerPushCredit;
- contract_terms: any;
- }
- | {
- type: BackupWgType.Recoup;
- };
-
-/**
- * FIXME: Open questions:
- * - Do we have to store the denomination selection? Why?
- * (If deterministic, amount shouldn't change. Not storing it is simpler.)
- */
-export interface BackupWithdrawalGroup {
- withdrawal_group_id: string;
-
- /**
- * Detailed info based on the type of withdrawal group.
- */
- info: BackupWgInfo;
-
- secret_seed: string;
-
- reserve_priv: string;
-
- exchange_base_url: string;
-
- timestamp_created: TalerProtocolTimestamp;
-
- timestamp_finish?: TalerProtocolTimestamp;
-
- operation_status: BackupOperationStatus;
-
- instructed_amount: BackupAmountString;
-
- /**
- * Amount including fees (i.e. the amount subtracted from the
- * reserve to withdraw all coins in this withdrawal session).
- *
- * Note that this *includes* the amount remaining in the reserve
- * that is too small to be withdrawn, and thus can't be derived
- * from selectedDenoms.
- */
- raw_withdrawal_amount: BackupAmountString;
-
- effective_withdrawal_amount: BackupAmountString;
-
- /**
- * Restrict withdrawals from this reserve to this age.
- */
- restrict_age?: number;
-
- /**
- * Multiset of denominations selected for withdrawal.
- */
- selected_denoms: BackupDenomSel;
-
- selected_denoms_uid: OperationUid;
-}
-
-/**
- * Tombstone in the format "<type>:<key>"
- */
-export type Tombstone = string;
-
-/**
- * Detailed error report.
- *
- * For auditor-relevant reports with attached cryptographic proof,
- * the error report also should contain the submission status to
- * the auditor(s).
- */
-interface BackupErrorReport {
- // FIXME: specify!
-}
-
-/**
- * Trust declaration for an auditor.
- *
- * The trust applies based on the public key of
- * the auditor, irrespective of what base URL the exchange
- * is referencing.
- */
-export interface BackupTrustAuditor {
- /**
- * Base URL of the auditor.
- */
- auditor_base_url: string;
-
- /**
- * Public key of the auditor.
- */
- auditor_pub: string;
-
- /**
- * UIDs for the operation of adding this auditor
- * as a trusted auditor.
- */
- uids: OperationUid;
-}
-
-/**
- * Trust declaration for an exchange.
- *
- * The trust only applies for the combination of base URL
- * and public key. If the master public key changes while the base
- * URL stays the same, the exchange has to be re-added by a wallet update
- * or by the user.
- */
-export interface BackupTrustExchange {
- /**
- * Canonicalized exchange base URL.
- */
- exchange_base_url: string;
-
- /**
- * Master public key of the exchange.
- */
- exchange_master_pub: string;
-
- /**
- * UIDs for the operation of adding this exchange
- * as trusted.
- */
- uids: OperationUid;
+export interface BackupRecovery {
+ walletRootPriv: string;
+ providers: {
+ name: string;
+ url: string;
+ }[];
}
export class BackupBackupProviderTerms {
@@ -424,862 +33,10 @@ export class BackupBackupProviderTerms {
/**
* Last known annual fee.
*/
- annual_fee: BackupAmountString;
+ annual_fee: AmountString;
/**
* Last known storage limit.
*/
storage_limit_in_megabytes: number;
}
-
-/**
- * Backup information about one backup storage provider.
- */
-export class BackupBackupProvider {
- /**
- * Canonicalized base URL of the provider.
- */
- base_url: string;
-
- /**
- * Last known terms. Might be unavailable in some situations, such
- * as directly after restoring form a backup recovery document.
- */
- terms?: BackupBackupProviderTerms;
-
- /**
- * Proposal IDs for payments to this provider.
- */
- pay_proposal_ids: string[];
-
- /**
- * UIDs for adding this backup provider.
- */
- uids: OperationUid[];
-}
-
-/**
- * Status of recoup operations that were grouped together.
- *
- * The remaining amount of the corresponding coins must be set to
- * zero when the recoup group is created/imported.
- */
-export interface BackupRecoupGroup {
- /**
- * Unique identifier for the recoup group record.
- */
- recoup_group_id: string;
-
- /**
- * Timestamp when the recoup was started.
- */
- timestamp_created: TalerProtocolTimestamp;
-
- timestamp_finish?: TalerProtocolTimestamp;
- finish_clock?: TalerProtocolTimestamp;
- // FIXME: Use some enum here!
- finish_is_failure?: boolean;
-
- /**
- * Information about each coin being recouped.
- */
- coins: {
- coin_pub: string;
- recoup_finished: boolean;
- }[];
-}
-
-/**
- * Types of coin sources.
- */
-export enum BackupCoinSourceType {
- Withdraw = "withdraw",
- Refresh = "refresh",
- Tip = "tip",
-}
-
-/**
- * Metadata about a coin obtained via withdrawing.
- */
-export interface BackupWithdrawCoinSource {
- type: BackupCoinSourceType.Withdraw;
-
- /**
- * Can be the empty string for orphaned coins.
- */
- withdrawal_group_id: string;
-
- /**
- * Index of the coin in the withdrawal session.
- */
- coin_index: number;
-
- /**
- * Reserve public key for the reserve we got this coin from.
- */
- reserve_pub: string;
-}
-
-/**
- * Metadata about a coin obtained from refreshing.
- *
- * FIXME: Currently does not link to the refreshGroupId because
- * the wallet DB doesn't do this. Not really necessary,
- * but would be more consistent.
- */
-export interface BackupRefreshCoinSource {
- type: BackupCoinSourceType.Refresh;
-
- /**
- * Public key of the coin that was refreshed into this coin.
- */
- old_coin_pub: string;
-
- refresh_group_id: string;
-}
-
-/**
- * Metadata about a coin obtained from a tip.
- */
-export interface BackupTipCoinSource {
- type: BackupCoinSourceType.Tip;
-
- /**
- * Wallet's identifier for the tip that this coin
- * originates from.
- */
- wallet_tip_id: string;
-
- /**
- * Index in the tip planchets of the tip.
- */
- coin_index: number;
-}
-
-/**
- * Metadata about a coin depending on the origin.
- */
-export type BackupCoinSource =
- | BackupWithdrawCoinSource
- | BackupRefreshCoinSource
- | BackupTipCoinSource;
-
-/**
- * Backup information about a coin.
- *
- * (Always part of a BackupExchange/BackupDenom)
- */
-export interface BackupCoin {
- /**
- * Where did the coin come from? Used for recouping coins.
- */
- coin_source: BackupCoinSource;
-
- /**
- * Private key to authorize operations on the coin.
- */
- coin_priv: string;
-
- /**
- * Unblinded signature by the exchange.
- */
- denom_sig: UnblindedSignature;
-
- /**
- * Information about where and how the coin was spent.
- */
- spend_allocation:
- | {
- id: string;
- amount: BackupAmountString;
- }
- | undefined;
-
- /**
- * Blinding key used when withdrawing the coin.
- * Potentionally used again during payback.
- */
- blinding_key: string;
-
- /**
- * Does the wallet think that the coin is still fresh?
- *
- * Note that even if a fresh coin is imported, it should still
- * be refreshed in most situations.
- */
- fresh: boolean;
-}
-
-/**
- * Status of a tip we got from a merchant.
- */
-export interface BackupTip {
- /**
- * Tip ID chosen by the wallet.
- */
- wallet_tip_id: string;
-
- /**
- * The merchant's identifier for this tip.
- */
- merchant_tip_id: string;
-
- /**
- * Secret seed used for the tipping planchets.
- */
- secret_seed: string;
-
- /**
- * Has the user accepted the tip? Only after the tip has been accepted coins
- * withdrawn from the tip may be used.
- */
- timestamp_accepted: TalerProtocolTimestamp | undefined;
-
- /**
- * When was the tip first scanned by the wallet?
- */
- timestamp_created: TalerProtocolTimestamp;
-
- timestamp_finished?: TalerProtocolTimestamp;
- finish_is_failure?: boolean;
-
- /**
- * The tipped amount.
- */
- tip_amount_raw: BackupAmountString;
-
- /**
- * Timestamp, the tip can't be picked up anymore after this deadline.
- */
- timestamp_expiration: TalerProtocolTimestamp;
-
- /**
- * The exchange that will sign our coins, chosen by the merchant.
- */
- exchange_base_url: string;
-
- /**
- * Base URL of the merchant that is giving us the tip.
- */
- merchant_base_url: string;
-
- /**
- * Selected denominations. Determines the effective tip amount.
- */
- selected_denoms: BackupDenomSel;
-
- /**
- * UID for the denomination selection.
- * Used to disambiguate when merging.
- */
- selected_denoms_uid: OperationUid;
-}
-
-/**
- * Reasons for why a coin is being refreshed.
- */
-export enum BackupRefreshReason {
- Manual = "manual",
- Pay = "pay",
- Refund = "refund",
- AbortPay = "abort-pay",
- Recoup = "recoup",
- BackupRestored = "backup-restored",
- Scheduled = "scheduled",
-}
-
-/**
- * Information about one refresh session, always part
- * of a refresh group.
- *
- * (Public key of the old coin is stored in the refresh group.)
- */
-export interface BackupRefreshSession {
- /**
- * Hashed denominations of the newly requested coins.
- */
- new_denoms: BackupDenomSel;
-
- /**
- * Seed used to derive the planchets and
- * transfer private keys for this refresh session.
- */
- session_secret_seed: string;
-
- /**
- * The no-reveal-index after we've done the melting.
- */
- noreveal_index?: number;
-}
-
-/**
- * Refresh session for one coin inside a refresh group.
- */
-export interface BackupRefreshOldCoin {
- /**
- * Public key of the old coin,
- */
- coin_pub: string;
-
- /**
- * Requested amount to refresh. Must be subtracted from the coin's remaining
- * amount as soon as the coin is added to the refresh group.
- */
- input_amount: BackupAmountString;
-
- /**
- * Estimated output (may change if it takes a long time to create the
- * actual session).
- */
- estimated_output_amount: BackupAmountString;
-
- /**
- * Did the refresh session finish (or was it unnecessary/impossible to create
- * one)
- */
- finished: boolean;
-
- /**
- * Refresh session (if created) or undefined it not created yet.
- */
- refresh_session: BackupRefreshSession | undefined;
-}
-
-/**
- * Information about one refresh group.
- *
- * May span more than one exchange, but typically doesn't
- */
-export interface BackupRefreshGroup {
- refresh_group_id: string;
-
- reason: BackupRefreshReason;
-
- /**
- * Details per old coin.
- */
- old_coins: BackupRefreshOldCoin[];
-
- timestamp_created: TalerProtocolTimestamp;
-
- timestamp_finish?: TalerProtocolTimestamp;
- finish_is_failure?: boolean;
-}
-
-export enum BackupRefundState {
- Failed = "failed",
- Applied = "applied",
- Pending = "pending",
-}
-
-/**
- * Common information about a refund.
- */
-export interface BackupRefundItemCommon {
- /**
- * Execution time as claimed by the merchant
- */
- execution_time: TalerProtocolTimestamp;
-
- /**
- * Time when the wallet became aware of the refund.
- */
- obtained_time: TalerProtocolTimestamp;
-
- /**
- * Amount refunded for the coin.
- */
- refund_amount: BackupAmountString;
-
- /**
- * Coin being refunded.
- */
- coin_pub: string;
-
- /**
- * The refund transaction ID for the refund.
- */
- rtransaction_id: number;
-
- /**
- * Upper bound on the refresh cost incurred by
- * applying this refund.
- *
- * Might be lower in practice when two refunds on the same
- * coin are refreshed in the same refresh operation.
- *
- * Used to display fees, and stored since it's expensive to recompute
- * accurately.
- */
- total_refresh_cost_bound: BackupAmountString;
-}
-
-/**
- * Failed refund, either because the merchant did
- * something wrong or it expired.
- */
-export interface BackupRefundFailedItem extends BackupRefundItemCommon {
- type: BackupRefundState.Failed;
-}
-
-export interface BackupRefundPendingItem extends BackupRefundItemCommon {
- type: BackupRefundState.Pending;
-}
-
-export interface BackupRefundAppliedItem extends BackupRefundItemCommon {
- type: BackupRefundState.Applied;
-}
-
-/**
- * State of one refund from the merchant, maintained by the wallet.
- */
-export type BackupRefundItem =
- | BackupRefundFailedItem
- | BackupRefundPendingItem
- | BackupRefundAppliedItem;
-
-/**
- * Data we store when the payment was accepted.
- */
-export interface BackupPayInfo {
- pay_coins: {
- /**
- * Public keys of the coins that were selected.
- */
- coin_pub: string;
-
- /**
- * Amount that each coin contributes.
- */
- contribution: BackupAmountString;
- }[];
-
- /**
- * Unique ID to disambiguate pay coin selection on merge.
- */
- pay_coins_uid: OperationUid;
-
- /**
- * Total cost initially shown to the user.
- *
- * This includes the amount taken by the merchant, fees (wire/deposit) contributed
- * by the customer, refreshing fees, fees for withdraw-after-refresh and "trimmings"
- * of coins that are too small to spend.
- *
- * Note that in rare situations, this cost might not be accurate (e.g.
- * when the payment or refresh gets re-denominated).
- * We might show adjustments to this later, but currently we don't do so.
- */
- total_pay_cost: BackupAmountString;
-}
-
-export interface BackupPurchase {
- /**
- * Proposal ID for this purchase. Uniquely identifies the
- * purchase and the proposal.
- */
- proposal_id: string;
-
- /**
- * Status of the proposal.
- */
- proposal_status: BackupProposalStatus;
-
- /**
- * Proposal that this one got "redirected" to as part of
- * the repurchase detection.
- */
- repurchase_proposal_id: string | undefined;
-
- /**
- * Session ID we got when downloading the contract.
- */
- download_session_id?: string;
-
- /**
- * Merchant-assigned order ID of the proposal.
- */
- order_id: string;
-
- /**
- * Base URL of the merchant that proposed the purchase.
- */
- merchant_base_url: string;
-
- /**
- * Claim token initially given by the merchant.
- */
- claim_token: string | undefined;
-
- /**
- * Contract terms we got from the merchant.
- */
- contract_terms_raw?: RawContractTerms;
-
- /**
- * Signature on the contract terms.
- *
- * FIXME: Better name needed.
- */
- merchant_sig?: string;
-
- /**
- * Private key for the nonce. Might eventually be used
- * to prove ownership of the contract.
- */
- nonce_priv: string;
-
- pay_info: BackupPayInfo | undefined;
-
- /**
- * Timestamp of the first time that sending a payment to the merchant
- * for this purchase was successful.
- */
- timestamp_first_successful_pay: TalerProtocolTimestamp | undefined;
-
- /**
- * Signature by the merchant confirming the payment.
- */
- merchant_pay_sig: string | undefined;
-
- timestamp_proposed: TalerProtocolTimestamp;
-
- /**
- * When was the purchase made?
- * Refers to the time that the user accepted.
- */
- timestamp_accepted: TalerProtocolTimestamp | undefined;
-
- /**
- * Pending refunds for the purchase. A refund is pending
- * when the merchant reports a transient error from the exchange.
- */
- refunds: BackupRefundItem[];
-
- /**
- * Continue querying the refund status until this deadline has expired.
- */
- auto_refund_deadline: TalerProtocolTimestamp | undefined;
-}
-
-/**
- * Info about one denomination in the backup.
- *
- * Note that the wallet only backs up validated denominations.
- */
-export interface BackupDenomination {
- /**
- * Value of one coin of the denomination.
- */
- value: BackupAmountString;
-
- /**
- * The denomination public key.
- */
- denom_pub: DenominationPubKey;
-
- /**
- * Fee for withdrawing.
- */
- fee_withdraw: BackupAmountString;
-
- /**
- * Fee for depositing.
- */
- fee_deposit: BackupAmountString;
-
- /**
- * Fee for refreshing.
- */
- fee_refresh: BackupAmountString;
-
- /**
- * Fee for refunding.
- */
- fee_refund: BackupAmountString;
-
- /**
- * Validity start date of the denomination.
- */
- stamp_start: TalerProtocolTimestamp;
-
- /**
- * Date after which the currency can't be withdrawn anymore.
- */
- stamp_expire_withdraw: TalerProtocolTimestamp;
-
- /**
- * Date after the denomination officially doesn't exist anymore.
- */
- stamp_expire_legal: TalerProtocolTimestamp;
-
- /**
- * Data after which coins of this denomination can't be deposited anymore.
- */
- stamp_expire_deposit: TalerProtocolTimestamp;
-
- /**
- * Signature by the exchange's master key over the denomination
- * information.
- */
- master_sig: string;
-
- /**
- * Was this denomination still offered by the exchange the last time
- * we checked?
- * Only false when the exchange redacts a previously published denomination.
- */
- is_offered: boolean;
-
- /**
- * Did the exchange revoke the denomination?
- * When this field is set to true in the database, the same transaction
- * should also mark all affected coins as revoked.
- */
- is_revoked: boolean;
-
- /**
- * Coins of this denomination.
- */
- coins: BackupCoin[];
-
- /**
- * The list issue date of the exchange "/keys" response
- * that this denomination was last seen in.
- */
- list_issue_date: TalerProtocolTimestamp;
-}
-
-/**
- * Denomination selection.
- */
-export type BackupDenomSel = {
- denom_pub_hash: string;
- count: number;
-}[];
-
-/**
- * Wire fee for one wire payment target type as stored in the
- * wallet's database.
- *
- * (Flattened to a list to make the declaration simpler).
- */
-export interface BackupExchangeWireFee {
- wire_type: string;
-
- /**
- * Fee for wire transfers.
- */
- wire_fee: string;
-
- /**
- * Fees to close and refund a reserve.
- */
- closing_fee: string;
-
- /**
- * Start date of the fee.
- */
- start_stamp: TalerProtocolTimestamp;
-
- /**
- * End date of the fee.
- */
- end_stamp: TalerProtocolTimestamp;
-
- /**
- * Signature made by the exchange master key.
- */
- sig: string;
-}
-
-/**
- * Global fee as stored in the wallet's database.
- *
- */
-export interface BackupExchangeGlobalFees {
- startDate: TalerProtocolTimestamp;
- endDate: TalerProtocolTimestamp;
-
- historyFee: BackupAmountString;
- accountFee: BackupAmountString;
- purseFee: BackupAmountString;
-
- historyTimeout: TalerProtocolDuration;
- purseTimeout: TalerProtocolDuration;
-
- purseLimit: number;
-
- signature: string;
-}
-/**
- * Structure of one exchange signing key in the /keys response.
- */
-export class BackupExchangeSignKey {
- stamp_start: TalerProtocolTimestamp;
- stamp_expire: TalerProtocolTimestamp;
- stamp_end: TalerProtocolTimestamp;
- key: string;
- master_sig: string;
-}
-
-/**
- * Signature by the auditor that a particular denomination key is audited.
- */
-export class BackupAuditorDenomSig {
- /**
- * Denomination public key's hash.
- */
- denom_pub_h: string;
-
- /**
- * The signature.
- */
- auditor_sig: string;
-}
-
-/**
- * Auditor information as given by the exchange in /keys.
- */
-export class BackupExchangeAuditor {
- /**
- * Auditor's public key.
- */
- auditor_pub: string;
-
- /**
- * Base URL of the auditor.
- */
- auditor_url: string;
-
- /**
- * List of signatures for denominations by the auditor.
- */
- denomination_keys: BackupAuditorDenomSig[];
-}
-
-/**
- * Backup information for an exchange. Serves effectively
- * as a pointer to the exchange details identified by
- * the base URL, master public key and currency.
- */
-export interface BackupExchange {
- base_url: string;
-
- master_public_key: string;
-
- currency: string;
-
- /**
- * Time when the pointer to the exchange details
- * was last updated.
- *
- * Used to facilitate automatic merging.
- */
- update_clock: TalerProtocolTimestamp;
-}
-
-/**
- * Backup information about an exchange's details.
- *
- * Note that one base URL can have multiple exchange
- * details. The BackupExchange stores a pointer
- * to the current exchange details.
- */
-export interface BackupExchangeDetails {
- /**
- * Canonicalized base url of the exchange.
- */
- base_url: string;
-
- /**
- * Master public key of the exchange.
- */
- master_public_key: string;
-
- /**
- * Auditors (partially) auditing the exchange.
- */
- auditors: BackupExchangeAuditor[];
-
- /**
- * Currency that the exchange offers.
- */
- currency: string;
-
- /**
- * Denominations offered by the exchange.
- */
- denominations: BackupDenomination[];
-
- /**
- * Last observed protocol version.
- */
- protocol_version: string;
-
- /**
- * Closing delay of reserves.
- */
- reserve_closing_delay: TalerProtocolDuration;
-
- /**
- * Signing keys we got from the exchange, can also contain
- * older signing keys that are not returned by /keys anymore.
- */
- signing_keys: BackupExchangeSignKey[];
-
- wire_fees: BackupExchangeWireFee[];
-
- global_fees: BackupExchangeGlobalFees[];
-
- /**
- * Bank accounts offered by the exchange;
- */
- accounts: {
- payto_uri: string;
- master_sig: string;
- }[];
-
- /**
- * ETag for last terms of service download.
- */
- tos_accepted_etag: string | undefined;
-
- /**
- * Timestamp when the ToS has been accepted.
- */
- tos_accepted_timestamp: TalerProtocolTimestamp | undefined;
-}
-
-export enum BackupProposalStatus {
- /**
- * Proposed (and either downloaded or not,
- * depending on whether contract terms are present),
- * but the user needs to accept/reject it.
- */
- Proposed = "proposed",
- /**
- * The user has rejected the proposal.
- */
- Refused = "refused",
- /**
- * Downloading or processing the proposal has failed permanently.
- *
- * FIXME: Should this be modeled as a "misbehavior report" instead?
- */
- PermanentlyFailed = "permanently-failed",
- /**
- * Downloaded proposal was detected as a re-purchase.
- */
- Repurchase = "repurchase",
-
- Paid = "paid",
-}
-
-export interface BackupRecovery {
- walletRootPriv: string;
- providers: {
- name: string;
- url: string;
- }[];
-}
diff --git a/packages/taler-util/src/bank-api-client.ts b/packages/taler-util/src/bank-api-client.ts
new file mode 100644
index 000000000..e9f442af6
--- /dev/null
+++ b/packages/taler-util/src/bank-api-client.ts
@@ -0,0 +1,458 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 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/>
+ */
+
+/**
+ * Client for the Taler (demo-)bank.
+ */
+
+/**
+ * Imports.
+ */
+import {
+ AmountString,
+ base64FromArrayBuffer,
+ buildCodecForObject,
+ Codec,
+ codecForAny,
+ codecForString,
+ encodeCrock,
+ getRandomBytes,
+ HttpStatusCode,
+ j2s,
+ Logger,
+ opEmptySuccess,
+ opKnownHttpFailure,
+ opUnknownFailure,
+ stringToBytes,
+ TalerError,
+ TalerErrorCode,
+} from "@gnu-taler/taler-util";
+import {
+ checkSuccessResponseOrThrow,
+ createPlatformHttpLib,
+ expectSuccessResponseOrThrow,
+ HttpRequestLibrary,
+ readSuccessResponseJsonOrThrow,
+ readSuccessResponseTextOrThrow,
+ readTalerErrorResponse,
+} from "@gnu-taler/taler-util/http";
+
+const logger = new Logger("bank-api-client.ts");
+
+export enum CreditDebitIndicator {
+ Credit = "credit",
+ Debit = "debit",
+}
+
+export interface BankAccountBalanceResponse {
+ balance: {
+ amount: AmountString;
+ credit_debit_indicator: CreditDebitIndicator;
+ };
+}
+
+export interface BankUser {
+ username: string;
+ password: string;
+ accountPaytoUri: string;
+}
+
+export interface WithdrawalOperationInfo {
+ withdrawal_id: string;
+ taler_withdraw_uri: string;
+}
+
+/**
+ * Helper function to generate the "Authorization" HTTP header.
+ */
+function makeBasicAuthHeader(username: string, password: string): string {
+ const auth = `${username}:${password}`;
+ const authEncoded: string = base64FromArrayBuffer(stringToBytes(auth));
+ return `Basic ${authEncoded}`;
+}
+
+const codecForWithdrawalOperationInfo = (): Codec<WithdrawalOperationInfo> =>
+ buildCodecForObject<WithdrawalOperationInfo>()
+ .property("withdrawal_id", codecForString())
+ .property("taler_withdraw_uri", codecForString())
+ .build("WithdrawalOperationInfo");
+
+export interface BankAccessApiClientArgs {
+ auth?: { username: string; password: string };
+ httpClient?: HttpRequestLibrary;
+}
+
+export interface BankAccessApiCreateTransactionRequest {
+ amount: AmountString;
+ paytoUri: string;
+}
+
+export class WireGatewayApiClientArgs {
+ auth?: {
+ username: string;
+ password: string;
+ };
+ httpClient?: HttpRequestLibrary;
+}
+
+/**
+ * This API look like it belongs to harness
+ * but it will be nice to have in utils to be used by others
+ */
+export class WireGatewayApiClient {
+ httpLib;
+
+ constructor(
+ private baseUrl: string,
+ private args: WireGatewayApiClientArgs = {},
+ ) {
+ this.httpLib = args.httpClient ?? createPlatformHttpLib();
+ }
+
+ private makeAuthHeader(): Record<string, string> {
+ const auth = this.args.auth;
+ if (auth) {
+ return {
+ Authorization: makeBasicAuthHeader(auth.username, auth.password),
+ };
+ }
+ return {};
+ }
+
+ async adminAddIncoming(params: {
+ amount: string;
+ reservePub: string;
+ debitAccountPayto: string;
+ }): Promise<void> {
+ let url = new URL(`admin/add-incoming`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body: {
+ amount: params.amount,
+ reserve_pub: params.reservePub,
+ debit_account: params.debitAccountPayto,
+ },
+ headers: this.makeAuthHeader(),
+ });
+ logger.info(`add-incoming response status: ${resp.status}`);
+ await checkSuccessResponseOrThrow(resp);
+ }
+}
+
+export interface ChallengeContactData {
+ // E-Mail address
+ email?: string;
+
+ // Phone number.
+ phone?: string;
+}
+
+export interface AccountBalance {
+ amount: AmountString;
+ credit_debit_indicator: "credit" | "debit";
+}
+
+export interface RegisterAccountRequest {
+ // Username
+ username: string;
+
+ // Password.
+ password: string;
+
+ // Legal name of the account owner
+ name: string;
+
+ // Defaults to false.
+ is_public?: boolean;
+
+ // Is this a taler exchange account?
+ // If true:
+ // - incoming transactions to the account that do not
+ // have a valid reserve public key are automatically
+ // - the account provides the taler-wire-gateway-api endpoints
+ // Defaults to false.
+ is_taler_exchange?: boolean;
+
+ // Addresses where to send the TAN for transactions.
+ // Currently only used for cashouts.
+ // If missing, cashouts will fail.
+ // In the future, might be used for other transactions
+ // as well.
+ challenge_contact_data?: ChallengeContactData;
+
+ // 'payto' address pointing a bank account
+ // external to the libeufin-bank.
+ // Payments will be sent to this bank account
+ // when the user wants to convert the local currency
+ // back to fiat currency outside libeufin-bank.
+ cashout_payto_uri?: string;
+
+ // Internal payto URI of this bank account.
+ // Used mostly for testing.
+ payto_uri?: string;
+}
+
+export interface AccountData {
+ // Legal name of the account owner.
+ name: string;
+
+ // Available balance on the account.
+ balance: AccountBalance;
+
+ // payto://-URI of the account.
+ payto_uri: string;
+
+ // Number indicating the max debit allowed for the requesting user.
+ debit_threshold: AmountString;
+
+ contact_data?: ChallengeContactData;
+
+ // 'payto' address pointing the bank account
+ // where to send cashouts. This field is optional
+ // because not all the accounts are required to participate
+ // in the merchants' circuit. One example is the exchange:
+ // that never cashouts. Registering these accounts can
+ // be done via the access API.
+ cashout_payto_uri?: string;
+}
+
+export interface ConfirmWithdrawalArgs {
+ withdrawalOperationId: string;
+}
+
+/**
+ * Client for the Taler corebank API.
+ */
+export class TalerCorebankApiClient {
+ httpLib: HttpRequestLibrary;
+
+ constructor(
+ public baseUrl: string,
+ private args: BankAccessApiClientArgs = {},
+ ) {
+ this.httpLib = args.httpClient ?? createPlatformHttpLib();
+ }
+
+ setAuth(auth: { username: string; password: string }) {
+ this.args.auth = auth;
+ }
+
+ private makeAuthHeader(): Record<string, string> {
+ if (!this.args.auth) {
+ return {};
+ }
+ const authHeaderValue = makeBasicAuthHeader(
+ this.args.auth.username,
+ this.args.auth.password,
+ );
+ return {
+ Authorization: authHeaderValue,
+ };
+ }
+
+ async getAccountBalance(
+ username: string,
+ ): Promise<BankAccountBalanceResponse> {
+ const url = new URL(`accounts/${username}`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ headers: this.makeAuthHeader(),
+ });
+ return readSuccessResponseJsonOrThrow(resp, codecForAny());
+ }
+
+ async getTransactions(username: string): Promise<void> {
+ const reqUrl = new URL(`accounts/${username}/transactions`, this.baseUrl);
+ const resp = await this.httpLib.fetch(reqUrl.href, {
+ method: "GET",
+ headers: {
+ ...this.makeAuthHeader(),
+ },
+ });
+
+ const res = await readSuccessResponseJsonOrThrow(resp, codecForAny());
+ logger.info(`result: ${j2s(res)}`);
+ }
+
+ async createTransaction(
+ username: string,
+ req: BankAccessApiCreateTransactionRequest,
+ ): Promise<any> {
+ const reqUrl = new URL(`accounts/${username}/transactions`, this.baseUrl);
+
+ const resp = await this.httpLib.fetch(reqUrl.href, {
+ method: "POST",
+ body: req,
+ headers: this.makeAuthHeader(),
+ });
+
+ return await readSuccessResponseJsonOrThrow(resp, codecForAny());
+ }
+
+ async registerAccountExtended(req: RegisterAccountRequest): Promise<void> {
+ const url = new URL("accounts", this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body: req,
+ headers: this.makeAuthHeader(),
+ });
+
+ if (
+ resp.status !== 200 &&
+ resp.status !== 201 &&
+ resp.status !== 202 &&
+ resp.status !== 204
+ ) {
+ logger.error(`unexpected status ${resp.status} from POST ${url.href}`);
+ logger.error(`${j2s(await resp.json())}`);
+ throw TalerError.fromDetail(
+ TalerErrorCode.GENERIC_UNEXPECTED_REQUEST_ERROR,
+ {
+ httpStatusCode: resp.status,
+ },
+ );
+ }
+ }
+
+ /**
+ * Register a new account and return information about it.
+ *
+ * This is a helper, as it does both the registration and the
+ * account info query.
+ */
+ async registerAccount(username: string, password: string): Promise<BankUser> {
+ const url = new URL("accounts", this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body: {
+ username,
+ password,
+ name: username,
+ },
+ headers: this.makeAuthHeader(),
+ });
+ if (
+ resp.status !== 200 &&
+ resp.status !== 201 &&
+ resp.status !== 202 &&
+ resp.status !== 204
+ ) {
+ logger.error(`unexpected status ${resp.status} from POST ${url.href}`);
+ logger.error(`${j2s(await resp.json())}`);
+ throw TalerError.fromDetail(
+ TalerErrorCode.GENERIC_UNEXPECTED_REQUEST_ERROR,
+ {
+ httpStatusCode: resp.status,
+ },
+ );
+ }
+ // FIXME: Corebank should directly return this info!
+ const infoUrl = new URL(`accounts/${username}`, this.baseUrl);
+ const infoResp = await this.httpLib.fetch(infoUrl.href, {
+ headers: {
+ Authorization: makeBasicAuthHeader(username, password),
+ },
+ });
+ // FIXME: Validate!
+ const acctInfo: AccountData = await readSuccessResponseJsonOrThrow(
+ infoResp,
+ codecForAny(),
+ );
+ return {
+ password,
+ username,
+ accountPaytoUri: acctInfo.payto_uri,
+ };
+ }
+
+ async createRandomBankUser(): Promise<BankUser> {
+ const username = "user-" + encodeCrock(getRandomBytes(10)).toLowerCase();
+ const password = "pw-" + encodeCrock(getRandomBytes(10)).toLowerCase();
+ return await this.registerAccount(username, password);
+ }
+
+ async createWithdrawalOperation(
+ user: string,
+ amount: string,
+ ): Promise<WithdrawalOperationInfo> {
+ const url = new URL(`accounts/${user}/withdrawals`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body: {
+ amount,
+ },
+ headers: this.makeAuthHeader(),
+ });
+ return readSuccessResponseJsonOrThrow(
+ resp,
+ codecForWithdrawalOperationInfo(),
+ );
+ }
+
+ async confirmWithdrawalOperation(
+ username: string,
+ wopi: ConfirmWithdrawalArgs,
+ ) {
+ const url = new URL(
+ `accounts/${username}/withdrawals/${wopi.withdrawalOperationId}/confirm`,
+ this.baseUrl,
+ );
+ logger.info(`confirming withdrawal operation via ${url.href}`);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body: {},
+ headers: this.makeAuthHeader(),
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ case HttpStatusCode.NoContent:
+ return opEmptySuccess(resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ async abortWithdrawalOperation(wopi: WithdrawalOperationInfo): Promise<void> {
+ const url = new URL(
+ `withdrawals/${wopi.withdrawal_id}/abort`,
+ this.baseUrl,
+ );
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body: {},
+ headers: this.makeAuthHeader(),
+ });
+ await readSuccessResponseJsonOrThrow(resp, codecForAny());
+ }
+
+ async abortWithdrawalOperationV2(
+ username: string,
+ wopi: WithdrawalOperationInfo,
+ ): Promise<void> {
+ const url = new URL(
+ `accounts/${username}/withdrawals/${wopi.withdrawal_id}/abort`,
+ this.baseUrl,
+ );
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body: {},
+ headers: this.makeAuthHeader(),
+ });
+ await expectSuccessResponseOrThrow(resp);
+ }
+}
diff --git a/packages/taler-util/src/bitcoin.ts b/packages/taler-util/src/bitcoin.ts
index 8c22ba522..37b7ae6b9 100644
--- a/packages/taler-util/src/bitcoin.ts
+++ b/packages/taler-util/src/bitcoin.ts
@@ -69,10 +69,10 @@ export function generateFakeSegwitAddress(
addr[0] === "t" && addr[1] == "b"
? "tb"
: addr[0] === "b" && addr[1] == "c" && addr[2] === "r" && addr[3] == "t"
- ? "bcrt"
- : addr[0] === "b" && addr[1] == "c"
- ? "bc"
- : undefined;
+ ? "bcrt"
+ : addr[0] === "b" && addr[1] == "c"
+ ? "bc"
+ : undefined;
if (prefix === undefined) throw new Error("unknown bitcoin net");
const addr1 = segwit.default.encode(prefix, 0, first_part);
diff --git a/packages/taler-util/src/clk.ts b/packages/taler-util/src/clk.ts
index e99ebf733..60969af69 100644
--- a/packages/taler-util/src/clk.ts
+++ b/packages/taler-util/src/clk.ts
@@ -17,16 +17,20 @@
/**
* Imports.
*/
-import process from "process";
-import path from "path";
-import readline from "readline";
-import { devNull } from "os";
+import {
+ processExit,
+ processArgv,
+ readlinePrompt,
+ pathBasename,
+} from "#compat-impl";
+import { AmountString } from "./taler-types.js";
export namespace clk {
class Converter<T> {}
export const INT = new Converter<number>();
export const STRING: Converter<string> = new Converter<string>();
+ export const AMOUNT: Converter<AmountString> = new Converter<AmountString>();
export interface OptionArgs<T> {
help?: string;
@@ -336,6 +340,8 @@ export namespace clk {
myArgs[def.name] = Number.parseInt(value);
} else if (def.conv == null || def.conv === STRING) {
myArgs[def.name] = value;
+ } else if (def.conv == null || def.conv === AMOUNT) {
+ myArgs[def.name] = value;
} else {
throw Error("unknown converter");
}
@@ -359,13 +365,13 @@ export namespace clk {
console.error(
`error: unknown option '--${r.key}' for ${currentName}`,
);
- process.exit(-1);
+ processExit(-1);
throw Error("not reached");
}
if (d.isFlag) {
if (r.value !== undefined) {
console.error(`error: flag '--${r.key}' does not take a value`);
- process.exit(-1);
+ processExit(-1);
throw Error("not reached");
}
storeFlag(d, true);
@@ -373,7 +379,7 @@ export namespace clk {
if (r.value === undefined) {
if (i === unparsedArgs.length - 1) {
console.error(`error: option '--${r.key}' needs an argument`);
- process.exit(-1);
+ processExit(-1);
throw Error("not reached");
}
storeOption(d, unparsedArgs[i + 1]);
@@ -391,7 +397,7 @@ export namespace clk {
const opt = this.shortOptions[chr];
if (!opt) {
console.error(`error: option '-${chr}' not known`);
- process.exit(-1);
+ processExit(-1);
}
if (opt.isFlag) {
storeFlag(opt, true);
@@ -399,7 +405,7 @@ export namespace clk {
if (si == optShort.length - 1) {
if (i === unparsedArgs.length - 1) {
console.error(`error: option '-${chr}' needs an argument`);
- process.exit(-1);
+ processExit(-1);
throw Error("not reached");
} else {
storeOption(opt, unparsedArgs[i + 1]);
@@ -418,7 +424,7 @@ export namespace clk {
const subcmd = this.subcommandMap[argVal];
if (!subcmd) {
console.error(`error: unknown command '${argVal}'`);
- process.exit(-1);
+ processExit(-1);
throw Error("not reached");
}
foundSubcommand = subcmd.commandGroup;
@@ -427,7 +433,7 @@ export namespace clk {
const d = this.arguments[posArgIndex];
if (!d) {
console.error(`error: too many arguments for ${currentName}`);
- process.exit(-1);
+ processExit(-1);
throw Error("not reached");
}
myArgs[d.name] = unparsedArgs[i];
@@ -437,7 +443,7 @@ export namespace clk {
if (parsedArgs[this.argKey].help) {
this.printHelp(progname, parents);
- process.exit(0);
+ processExit(0);
throw Error("not reached");
}
@@ -450,7 +456,7 @@ export namespace clk {
console.error(
`error: missing positional argument '${d.name}' for ${currentName}`,
);
- process.exit(-1);
+ processExit(-1);
throw Error("not reached");
}
}
@@ -464,7 +470,7 @@ export namespace clk {
} else {
const name = option.flagspec.join(",");
console.error(`error: missing option '${name}'`);
- process.exit(-1);
+ processExit(-1);
throw Error("not reached");
}
}
@@ -492,16 +498,16 @@ export namespace clk {
} catch (e) {
console.error(`An error occurred while running ${currentName}`);
console.error(e);
- process.exit(1);
+ processExit(1);
}
Promise.resolve(r).catch((e) => {
console.error(`An error occurred while running ${currentName}`);
console.error(e);
- process.exit(1);
+ processExit(1);
});
} else {
this.printHelp(progname, parents);
- process.exit(-1);
+ processExit(-1);
throw Error("not reached");
}
}
@@ -524,15 +530,15 @@ export namespace clk {
if (cmdlineArgs) {
args = cmdlineArgs;
} else {
- args = process.argv.slice(1);
+ args = processArgv().slice(1);
}
if (args.length < 1) {
console.error(
"Error while parsing command line arguments: not enough arguments",
);
- process.exit(-1);
+ processExit(-1);
}
- const progname = path.basename(args[0]);
+ const progname = pathBasename(args[0]);
const rest = args.slice(1);
this.mainCommand.run(progname, [], rest, {});
@@ -611,8 +617,8 @@ export namespace clk {
export type GetArgType<T> = T extends Program<any, infer AT>
? AT
: T extends CommandGroup<any, infer AT>
- ? AT
- : any;
+ ? AT
+ : any;
export function program<PN extends keyof any>(
argKey: PN,
@@ -622,15 +628,6 @@ export namespace clk {
}
export function prompt(question: string): Promise<string> {
- const stdinReadline = readline.createInterface({
- input: process.stdin,
- output: process.stdout,
- });
- return new Promise<string>((resolve, reject) => {
- stdinReadline.question(question, (res) => {
- resolve(res);
- stdinReadline.close();
- });
- });
+ return readlinePrompt(question);
}
}
diff --git a/packages/taler-util/src/codec.ts b/packages/taler-util/src/codec.ts
index 695f3c147..b04ce0612 100644
--- a/packages/taler-util/src/codec.ts
+++ b/packages/taler-util/src/codec.ts
@@ -14,12 +14,17 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
+import { j2s } from "./helpers.js";
+import { Logger } from "./logging.js";
+
/**
* Type-safe codecs for converting from/to JSON.
*/
/* eslint-disable @typescript-eslint/ban-types */
+const logger = new Logger("codec.ts");
+
/**
* Error thrown when decoding fails.
*/
@@ -141,7 +146,7 @@ class UnionCodecBuilder<
constructor(
private discriminator: TagPropertyLabel,
private baseCodec?: Codec<CommonBaseType>,
- ) {}
+ ) { }
/**
* Define a property for the object.
@@ -322,6 +327,74 @@ export function codecForString(): Codec<string> {
}
/**
+ * Return a codec for a value that must be a string.
+ */
+export function codecForStringURL(shouldEndWithSlash?: boolean): Codec<string> {
+ return {
+ decode(x: any, c?: Context): string {
+ if (typeof x !== "string") {
+ throw new DecodingError(
+ `expected string at ${renderContext(c)} but got ${typeof x}`,
+ );
+ }
+ if (shouldEndWithSlash && !x.endsWith("/")) {
+ throw new DecodingError(
+ `expected URL string that ends with slash at ${renderContext(
+ c,
+ )} but got ${x}`,
+ );
+ }
+ try {
+ const url = new URL(x);
+ return x;
+ } catch (e) {
+ if (e instanceof Error) {
+ throw new DecodingError(e.message);
+ } else {
+ throw new DecodingError(
+ `expected an URL string at ${renderContext(c)} but got "${x}"`,
+ );
+ }
+ }
+ },
+ };
+}
+
+/**
+ * Return a codec for a value that must be a string.
+ */
+export function codecForURL(shouldEndWithSlash?: boolean): Codec<URL> {
+ return {
+ decode(x: any, c?: Context): URL {
+ if (typeof x !== "string") {
+ throw new DecodingError(
+ `expected string at ${renderContext(c)} but got ${typeof x}`,
+ );
+ }
+ if (shouldEndWithSlash && !x.endsWith("/")) {
+ throw new DecodingError(
+ `expected URL string that ends with slash at ${renderContext(
+ c,
+ )} but got ${x}`,
+ );
+ }
+ try {
+ const url = new URL(x);
+ return url;
+ } catch (e) {
+ if (e instanceof Error) {
+ throw new DecodingError(e.message);
+ } else {
+ throw new DecodingError(
+ `expected an URL string at ${renderContext(c)} but got "${x}"`,
+ );
+ }
+ }
+ },
+ };
+}
+
+/**
* Codec that allows any value.
*/
export function codecForAny(): Codec<any> {
@@ -418,6 +491,30 @@ export function codecOptional<V>(innerCodec: Codec<V>): Codec<V | undefined> {
};
}
+export function codecOptionalDefault<V>(innerCodec: Codec<V>, def: V): Codec<V> {
+ return {
+ decode(x: any, c?: Context): V {
+ if (x === undefined || x === null) {
+ return def;
+ }
+ return innerCodec.decode(x, c);
+ },
+ };
+}
+
+export function codecForLazy<V>(innerCodec: () => Codec<V>): Codec<V> {
+ let instance: Codec<V> | undefined = undefined
+ return {
+ decode(x: any, c?: Context): V {
+ if (instance === undefined) {
+ instance = innerCodec()
+ }
+ return instance.decode(x, c);
+ },
+ };
+}
+
+
export type CodecType<T> = T extends Codec<infer X> ? X : any;
export function codecForEither<T extends Array<Codec<unknown>>>(
@@ -432,11 +529,12 @@ export function codecForEither<T extends Array<Codec<unknown>>>(
continue;
}
}
+ if (logger.shouldLogTrace()) {
+ logger.trace(`offending value: ${j2s(x)}`);
+ }
throw new DecodingError(
`No alternative matched at at ${renderContext(c)}`,
);
},
};
}
-
-const x = codecForEither(codecForString(), codecForNumber());
diff --git a/packages/taler-util/src/compat.d.ts b/packages/taler-util/src/compat.d.ts
new file mode 100644
index 000000000..d7ccf19f0
--- /dev/null
+++ b/packages/taler-util/src/compat.d.ts
@@ -0,0 +1,23 @@
+/*
+ This file is part of GNU Taler
+ (C) 2023 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/>
+ */
+
+export function processExit(status: number): never;
+export function processArgv(): string[];
+export function readlinePrompt(prompt: string): Promise<string>;
+export function pathBasename(s: string): string;
+export function setUnhandledRejectionHandler(h: (e: any) => void): void;
+export function getenv(name: string): string | undefined;
+export function readFile(fileName: string): string;
diff --git a/packages/taler-util/src/compat.node.ts b/packages/taler-util/src/compat.node.ts
new file mode 100644
index 000000000..2f78c9346
--- /dev/null
+++ b/packages/taler-util/src/compat.node.ts
@@ -0,0 +1,64 @@
+/*
+ This file is part of GNU Taler
+ (C) 2023 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 process from "node:process";
+import readline from "node:readline";
+import path from "node:path";
+import os from "node:os";
+import fs from "node:fs";
+
+export function processExit(status: number): never {
+ process.exit(status);
+}
+
+export function processArgv(): string[] {
+ return [...process.argv];
+}
+
+export function readlinePrompt(prompt: string): Promise<string> {
+ const stdinReadline = readline.createInterface({
+ input: process.stdin,
+ output: process.stdout,
+ });
+ return new Promise<string>((resolve, reject) => {
+ stdinReadline.question(prompt, (res) => {
+ resolve(res);
+ stdinReadline.close();
+ });
+ });
+}
+
+export function pathBasename(p: string): string {
+ return path.basename(p);
+}
+
+export function pathHomedir(): string {
+ return os.homedir();
+}
+
+export function setUnhandledRejectionHandler(h: (e: any) => void): void {
+ process.on("unhandledRejection", (e) => {
+ h(e);
+ });
+}
+
+export function getenv(name: string): string | undefined {
+ return process.env[name];
+}
+
+export function readFile(fileName: string): string {
+ return fs.readFileSync(fileName, "utf-8");
+}
diff --git a/packages/taler-util/src/compat.qtart.ts b/packages/taler-util/src/compat.qtart.ts
new file mode 100644
index 000000000..7d11cf375
--- /dev/null
+++ b/packages/taler-util/src/compat.qtart.ts
@@ -0,0 +1,57 @@
+/*
+ This file is part of GNU Taler
+ (C) 2023 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/>
+ */
+
+// qtart "std" library
+// @ts-ignore
+import * as std from "std";
+
+export function processExit(status: number): never {
+ std.exit(status);
+ throw Error("not reached");
+}
+
+export function processArgv(): string[] {
+ // @ts-ignore
+ return ["qtart", ...globalThis.scriptArgs];
+}
+
+export function readlinePrompt(prompt: string): Promise<string> {
+ throw new Error("realinePrompt not yet supported in qtart");
+}
+
+export function pathBasename(p: string): string {
+ const slashIndex = p.lastIndexOf("/");
+ if (slashIndex < 0) {
+ return p;
+ }
+ return p.substring(0, slashIndex);
+}
+
+export function pathHomedir(): string {
+ return std.getenv("HOME");
+}
+
+export function setUnhandledRejectionHandler(h: (e: any) => void): void {
+ // not supported
+}
+
+export function getenv(name: string): string | undefined {
+ return std.getenv(name);
+}
+
+export function readFile(fileName: string): string {
+ throw new Error("readFile not yet supported in qtart");
+}
diff --git a/packages/taler-util/src/contract-terms.ts b/packages/taler-util/src/contract-terms.ts
index 3567b50d8..b906a1d7f 100644
--- a/packages/taler-util/src/contract-terms.ts
+++ b/packages/taler-util/src/contract-terms.ts
@@ -16,12 +16,12 @@
import { canonicalJson } from "./helpers.js";
import { Logger } from "./logging.js";
-import { kdf } from "./kdf.js";
import {
decodeCrock,
encodeCrock,
getRandomBytes,
hash,
+ kdf,
stringToBytes,
} from "./taler-crypto.js";
diff --git a/packages/taler-util/src/errors.ts b/packages/taler-util/src/errors.ts
new file mode 100644
index 000000000..9378d25e8
--- /dev/null
+++ b/packages/taler-util/src/errors.ts
@@ -0,0 +1,329 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019-2020 Taler Systems SA
+
+ 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/>
+ */
+
+/**
+ * Classes and helpers for error handling specific to wallet operations.
+ *
+ * @author Florian Dold <dold@taler.net>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ AbsoluteTime,
+ CancellationToken,
+ PaymentInsufficientBalanceDetails,
+ TalerErrorCode,
+ TalerErrorDetail,
+ TransactionType,
+} from "@gnu-taler/taler-util";
+
+type empty = Record<string, never>;
+
+export interface DetailsMap {
+ [TalerErrorCode.WALLET_PENDING_OPERATION_FAILED]: {
+ innerError: TalerErrorDetail;
+ transactionId?: string;
+ };
+ [TalerErrorCode.WALLET_EXCHANGE_DENOMINATIONS_INSUFFICIENT]: {
+ exchangeBaseUrl: string;
+ };
+ [TalerErrorCode.WALLET_EXCHANGE_PROTOCOL_VERSION_INCOMPATIBLE]: {
+ exchangeProtocolVersion: string;
+ walletProtocolVersion: string;
+ };
+ [TalerErrorCode.WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK]: empty;
+ [TalerErrorCode.WALLET_REWARD_COIN_SIGNATURE_INVALID]: empty;
+ [TalerErrorCode.WALLET_ORDER_ALREADY_CLAIMED]: {
+ orderId: string;
+ claimUrl: string;
+ };
+ [TalerErrorCode.WALLET_ORDER_ALREADY_PAID]: {
+ orderId: string;
+ fulfillmentUrl: string;
+ };
+ [TalerErrorCode.WALLET_CONTRACT_TERMS_MALFORMED]: empty;
+ [TalerErrorCode.WALLET_CONTRACT_TERMS_SIGNATURE_INVALID]: {
+ merchantPub: string;
+ orderId: string;
+ };
+ [TalerErrorCode.WALLET_CONTRACT_TERMS_BASE_URL_MISMATCH]: {
+ baseUrlForDownload: string;
+ baseUrlFromContractTerms: string;
+ };
+ [TalerErrorCode.WALLET_INVALID_TALER_PAY_URI]: {
+ talerPayUri: string;
+ };
+ [TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR]: {
+ requestUrl: string;
+ requestMethod: string;
+ httpStatusCode: number;
+ errorResponse?: any;
+ };
+ [TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION]: {
+ stack?: string;
+ };
+ [TalerErrorCode.WALLET_BANK_INTEGRATION_PROTOCOL_VERSION_INCOMPATIBLE]: {
+ bankProtocolVersion: string;
+ walletProtocolVersion: string;
+ };
+ [TalerErrorCode.WALLET_CORE_API_OPERATION_UNKNOWN]: {
+ operation: string;
+ };
+ [TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED]: {
+ requestUrl: string;
+ requestMethod: string;
+ throttleStats: Record<string, unknown>;
+ };
+ [TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT]: {
+ requestUrl: string;
+ requestMethod: string;
+ timeoutMs: number;
+ };
+ [TalerErrorCode.GENERIC_TIMEOUT]: {
+ requestUrl: string;
+ requestMethod: string;
+ timeoutMs: number;
+ };
+ [TalerErrorCode.WALLET_NETWORK_ERROR]: {
+ requestUrl: string;
+ requestMethod: string;
+ };
+ [TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE]: {
+ requestUrl: string;
+ requestMethod: string;
+ httpStatusCode: number;
+ validationError?: string;
+ /**
+ * Content type of the response, usually only specified if not the
+ * expected content type.
+ */
+ contentType?: string;
+ };
+ [TalerErrorCode.GENERIC_CLIENT_INTERNAL_ERROR]: {
+ operation: string;
+ error: string;
+ detail: TalerErrorDetail | undefined;
+ };
+ [TalerErrorCode.WALLET_EXCHANGE_COIN_SIGNATURE_INVALID]: empty;
+ [TalerErrorCode.WALLET_WITHDRAWAL_GROUP_INCOMPLETE]: {
+ numErrors: number;
+ errorsPerCoin: Record<number, TalerErrorDetail>;
+ };
+ [TalerErrorCode.WALLET_CORE_NOT_AVAILABLE]: {
+ lastError?: TalerErrorDetail;
+ };
+ [TalerErrorCode.GENERIC_UNEXPECTED_REQUEST_ERROR]: {
+ httpStatusCode: number;
+ };
+ [TalerErrorCode.WALLET_PAY_MERCHANT_SERVER_ERROR]: {
+ requestError: TalerErrorDetail;
+ };
+ [TalerErrorCode.WALLET_CRYPTO_WORKER_ERROR]: {
+ innerError: TalerErrorDetail;
+ };
+ [TalerErrorCode.WALLET_CRYPTO_WORKER_BAD_REQUEST]: {
+ detail: string;
+ };
+ [TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED]: {
+ kycUrl: string;
+ };
+ [TalerErrorCode.WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE]: {
+ insufficientBalanceDetails: PaymentInsufficientBalanceDetails;
+ };
+ [TalerErrorCode.WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE]: {
+ insufficientBalanceDetails: PaymentInsufficientBalanceDetails;
+ };
+ [TalerErrorCode.WALLET_REFRESH_GROUP_INCOMPLETE]: {
+ numErrors: number;
+ /**
+ * Errors, can be truncated.
+ */
+ errors: TalerErrorDetail[];
+ };
+ [TalerErrorCode.WALLET_EXCHANGE_BASE_URL_MISMATCH]: {
+ urlWallet: string;
+ urlExchange: string;
+ };
+ [TalerErrorCode.WALLET_EXCHANGE_UNAVAILABLE]: {
+ exchangeBaseUrl: string;
+ innerError: TalerErrorDetail | undefined;
+ };
+ [TalerErrorCode.WALLET_DB_UNAVAILABLE]: {
+ innerError: TalerErrorDetail | undefined;
+ };
+}
+
+type ErrBody<Y> = Y extends keyof DetailsMap ? DetailsMap[Y] : empty;
+
+export function makeErrorDetail<C extends TalerErrorCode>(
+ code: C,
+ detail: ErrBody<C>,
+ hint?: string,
+): TalerErrorDetail {
+ if (!hint && !(detail as any).hint) {
+ hint = getDefaultHint(code);
+ }
+ const when = AbsoluteTime.now();
+ return { code, when, hint, ...detail };
+}
+
+export function makePendingOperationFailedError(
+ innerError: TalerErrorDetail,
+ tag: TransactionType,
+ uid: string,
+): TalerError {
+ return TalerError.fromDetail(TalerErrorCode.WALLET_PENDING_OPERATION_FAILED, {
+ innerError,
+ transactionId: `${tag}:${uid}`,
+ });
+}
+
+export function summarizeTalerErrorDetail(ed: TalerErrorDetail): string {
+ const errName = TalerErrorCode[ed.code] ?? "<unknown>";
+ return `Error (${ed.code}/${errName})`;
+}
+
+function getDefaultHint(code: number): string {
+ const errName = TalerErrorCode[code];
+ if (errName) {
+ return `Error (${errName})`;
+ } else {
+ return `Error (<unknown>)`;
+ }
+}
+
+export class TalerProtocolViolationError extends Error {
+ constructor(hint?: string) {
+ let msg: string;
+ if (hint) {
+ msg = `Taler protocol violation error (${hint})`;
+ } else {
+ msg = `Taler protocol violation error`;
+ }
+ super(msg);
+ Object.setPrototypeOf(this, TalerProtocolViolationError.prototype);
+ }
+}
+
+// compute a subset of TalerError, just for http request
+type HttpErrors =
+ | TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT
+ | TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED
+ | TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE
+ | TalerErrorCode.WALLET_NETWORK_ERROR
+ | TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR;
+
+type TalerHttpErrorsDetails = {
+ [code in HttpErrors]: TalerError<DetailsMap[code]>;
+};
+
+export type TalerHttpError =
+ TalerHttpErrorsDetails[keyof TalerHttpErrorsDetails];
+
+export class TalerError<T = any> extends Error {
+ errorDetail: TalerErrorDetail & T;
+ cause: Error | undefined;
+ private constructor(d: TalerErrorDetail & T, cause?: Error) {
+ super(d.hint ?? `Error (code ${d.code})`);
+ this.errorDetail = d;
+ this.cause = cause;
+ Object.setPrototypeOf(this, TalerError.prototype);
+ }
+
+ static fromDetail<C extends TalerErrorCode>(
+ code: C,
+ detail: ErrBody<C>,
+ hint?: string,
+ cause?: Error,
+ ): TalerError {
+ if (!hint) {
+ hint = getDefaultHint(code);
+ }
+ const when = AbsoluteTime.now();
+ return new TalerError<unknown>({ code, when, hint, ...detail }, cause);
+ }
+
+ static fromUncheckedDetail(d: TalerErrorDetail, c?: Error): TalerError {
+ return new TalerError<unknown>({ ...d }, c);
+ }
+
+ static fromException(e: any): TalerError {
+ const errDetail = getErrorDetailFromException(e);
+ return new TalerError(errDetail, e);
+ }
+
+ hasErrorCode<C extends keyof DetailsMap>(
+ code: C,
+ ): this is TalerError<DetailsMap[C]> {
+ return this.errorDetail.code === code;
+ }
+
+ toString(): string {
+ return `TalerError: ${JSON.stringify(this.errorDetail)}`;
+ }
+}
+
+export function safeStringifyException(e: any): string {
+ return JSON.stringify(getErrorDetailFromException(e), undefined, 2);
+}
+
+/**
+ * Convert an exception (or anything that was thrown) into
+ * a TalerErrorDetail object.
+ */
+export function getErrorDetailFromException(e: any): TalerErrorDetail {
+ if (e instanceof TalerError) {
+ return e.errorDetail;
+ }
+ if (e instanceof CancellationToken.CancellationError) {
+ const err = makeErrorDetail(
+ TalerErrorCode.WALLET_CORE_REQUEST_CANCELLED,
+ {},
+ );
+ return err;
+ }
+ if (e instanceof Error) {
+ const err = makeErrorDetail(
+ TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
+ {
+ stack: e.stack,
+ },
+ `unexpected exception (message: ${e.message})`,
+ );
+ return err;
+ }
+ // Something was thrown that is not even an exception!
+ // Try to stringify it.
+ let excString: string;
+ try {
+ excString = e.toString();
+ } catch (e) {
+ // Something went horribly wrong.
+ excString = "can't stringify exception";
+ }
+ const err = makeErrorDetail(
+ TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
+ {},
+ `unexpected exception (not an exception, ${excString})`,
+ );
+ return err;
+}
+
+export function assertUnreachable(x: never): never {
+ throw new Error("Didn't expect to get here");
+}
diff --git a/packages/taler-util/src/globbing/minimatch.ts b/packages/taler-util/src/globbing/minimatch.ts
index b23de4e92..30391ba26 100644
--- a/packages/taler-util/src/globbing/minimatch.ts
+++ b/packages/taler-util/src/globbing/minimatch.ts
@@ -341,9 +341,9 @@ export class Minimatch {
pattern.charAt(0) === "."
? "" // anything
: // not (start or / followed by . or .. followed by / or end)
- options.dot
- ? "(?!(?:^|\\/)\\.{1,2}(?:$|\\/))"
- : "(?!\\.)";
+ options.dot
+ ? "(?!(?:^|\\/)\\.{1,2}(?:$|\\/))"
+ : "(?!\\.)";
var self = this;
function clearStateChar() {
@@ -874,8 +874,8 @@ export class Minimatch {
var twoStar = options.noglobstar
? star
: options.dot
- ? twoStarDot
- : twoStarNoDot;
+ ? twoStarDot
+ : twoStarNoDot;
var flags = options.nocase ? "i" : "";
var re = (set as any)
@@ -885,8 +885,8 @@ export class Minimatch {
return p === GLOBSTAR
? twoStar
: typeof p === "string"
- ? regExpEscape(p)
- : (p as any)._src;
+ ? regExpEscape(p)
+ : (p as any)._src;
})
.join("\\/");
})
diff --git a/packages/taler-util/src/helpers.ts b/packages/taler-util/src/helpers.ts
index 7d84d434e..d4c3c86b5 100644
--- a/packages/taler-util/src/helpers.ts
+++ b/packages/taler-util/src/helpers.ts
@@ -121,3 +121,19 @@ export function j2s(x: any): string {
export function notEmpty<T>(value: T | null | undefined): value is T {
return value !== null && value !== undefined;
}
+
+/**
+ * Safe function to stringify errors.
+ */
+export function stringifyError(x: any): string {
+ if (typeof x === "undefined") {
+ return "<thrown undefined>";
+ }
+ if (x === null) {
+ return `<thrown null>`;
+ }
+ if (typeof x === "object") {
+ return x.toString();
+ }
+ return `<thrown ${typeof x}>`;
+}
diff --git a/packages/taler-util/src/http-client/README.md b/packages/taler-util/src/http-client/README.md
new file mode 100644
index 000000000..33d1a8645
--- /dev/null
+++ b/packages/taler-util/src/http-client/README.md
@@ -0,0 +1,19 @@
+## HTTP Cclients
+
+This folder contain class or function specifically designed to facilitate HTTP client
+interactions with a the core systems.
+
+These API defines:
+
+1. **API Communication**: Handle communication with the component API,
+ abstracting away the details of HTTP requests and responses.
+ This includes making GET, POST, PUT, and DELETE requests to the servers.
+2. **Data Formatting**: Responsible for formatting requests to the API in a
+ way that's expected by the servers (JSON) and parsing the responses back
+ into formats usable by the client.
+3. **Authentication and Security**: Handling authentication with the server API,
+ which could involve sending API keys, client credentials, or managing tokens.
+ It might also implement security features to ensure data integrity and confidentiality during transit.
+4. **Error Handling**: Providing robust error handling and retry mechanisms
+ for failed HTTP requests, including logging and potentially user notifications for critical failures.
+5. **Data Validation**: Before sending requests, it could validate the data to ensure it meets the API's expected format, types, and value ranges, reducing the likelihood of errors and improving system reliability.
diff --git a/packages/taler-util/src/http-client/authentication.ts b/packages/taler-util/src/http-client/authentication.ts
new file mode 100644
index 000000000..8897a2fa0
--- /dev/null
+++ b/packages/taler-util/src/http-client/authentication.ts
@@ -0,0 +1,137 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 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 { HttpStatusCode } from "../http-status-codes.js";
+import {
+ HttpRequestLibrary,
+ createPlatformHttpLib,
+ makeBasicAuthHeader,
+ readTalerErrorResponse,
+} from "../http.js";
+import { LibtoolVersion } from "../libtool-version.js";
+import {
+ opEmptySuccess,
+ opKnownHttpFailure,
+ opSuccessFromHttp,
+ opUnknownFailure,
+} from "../operation.js";
+import {
+ AccessToken,
+ TalerAuthentication,
+ codecForTokenSuccessResponse,
+ codecForTokenSuccessResponseMerchant,
+} from "./types.js";
+import { makeBearerTokenAuthHeader } from "./utils.js";
+
+export class TalerAuthenticationHttpClient {
+ public readonly PROTOCOL_VERSION = "0:0:0";
+
+ httpLib: HttpRequestLibrary;
+
+ constructor(
+ readonly baseUrl: string,
+ httpClient?: HttpRequestLibrary,
+ ) {
+ this.httpLib = httpClient ?? createPlatformHttpLib();
+ }
+
+ isCompatible(version: string): boolean {
+ const compare = LibtoolVersion.compare(this.PROTOCOL_VERSION, version);
+ return compare?.compatible ?? false;
+ }
+
+ /**
+ * https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-token
+ *
+ * @returns
+ */
+ async createAccessTokenBasic(
+ username: string,
+ password: string,
+ body: TalerAuthentication.TokenRequest,
+ ) {
+ const url = new URL(`token`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ headers: {
+ Authorization: makeBasicAuthHeader(username, password),
+ },
+ body,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForTokenSuccessResponse());
+ //FIXME: missing in docs
+ case HttpStatusCode.Unauthorized:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ *
+ * @returns
+ */
+ async createAccessTokenBearer(
+ token: AccessToken,
+ body: TalerAuthentication.TokenRequest,
+ ) {
+ const url = new URL(`token`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ headers: {
+ Authorization: makeBearerTokenAuthHeader(token),
+ },
+ body,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForTokenSuccessResponseMerchant());
+ //FIXME: missing in docs
+ case HttpStatusCode.Unauthorized:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ async deleteAccessToken(token: AccessToken) {
+ const url = new URL(`token`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "DELETE",
+ headers: {
+ Authorization: makeBearerTokenAuthHeader(token),
+ },
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opEmptySuccess(resp);
+ //FIXME: missing in docs
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+}
diff --git a/packages/taler-util/src/http-client/bank-conversion.ts b/packages/taler-util/src/http-client/bank-conversion.ts
new file mode 100644
index 000000000..cb14d8b34
--- /dev/null
+++ b/packages/taler-util/src/http-client/bank-conversion.ts
@@ -0,0 +1,223 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 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 { AmountJson, Amounts } from "../amounts.js";
+import { HttpRequestLibrary, readTalerErrorResponse } from "../http-common.js";
+import { HttpStatusCode } from "../http-status-codes.js";
+import { createPlatformHttpLib } from "../http.js";
+import { LibtoolVersion } from "../libtool-version.js";
+import {
+ FailCasesByMethod,
+ ResultByMethod,
+ opEmptySuccess,
+ opKnownHttpFailure,
+ opSuccessFromHttp,
+ opUnknownFailure,
+} from "../operation.js";
+import { TalerErrorCode } from "../taler-error-codes.js";
+import { codecForTalerErrorDetail } from "../wallet-types.js";
+import {
+ AccessToken,
+ TalerBankConversionApi,
+ codecForCashinConversionResponse,
+ codecForCashoutConversionResponse,
+ codecForConversionBankConfig,
+} from "./types.js";
+import {
+ CacheEvictor,
+ makeBearerTokenAuthHeader,
+ nullEvictor,
+} from "./utils.js";
+
+export type TalerBankConversionResultByMethod<
+ prop extends keyof TalerBankConversionHttpClient,
+> = ResultByMethod<TalerBankConversionHttpClient, prop>;
+export type TalerBankConversionErrorsByMethod<
+ prop extends keyof TalerBankConversionHttpClient,
+> = FailCasesByMethod<TalerBankConversionHttpClient, prop>;
+
+export enum TalerBankConversionCacheEviction {
+ UPDATE_RATE,
+}
+
+/**
+ * The API is used by the wallets.
+ */
+export class TalerBankConversionHttpClient {
+ public readonly PROTOCOL_VERSION = "0:0:0";
+
+ httpLib: HttpRequestLibrary;
+ cacheEvictor: CacheEvictor<TalerBankConversionCacheEviction>;
+
+ constructor(
+ readonly baseUrl: string,
+ httpClient?: HttpRequestLibrary,
+ cacheEvictor?: CacheEvictor<TalerBankConversionCacheEviction>,
+ ) {
+ this.httpLib = httpClient ?? createPlatformHttpLib();
+ this.cacheEvictor = cacheEvictor ?? nullEvictor;
+ }
+
+ isCompatible(version: string): boolean {
+ const compare = LibtoolVersion.compare(this.PROTOCOL_VERSION, version);
+ return compare?.compatible ?? false;
+ }
+
+ /**
+ * https://docs.taler.net/core/api-bank-conversion-info.html#get--config
+ *
+ */
+ async getConfig() {
+ const url = new URL(`config`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForConversionBankConfig());
+ case HttpStatusCode.NotImplemented:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-bank-conversion-info.html#get--cashin-rate
+ *
+ */
+ async getCashinRate(conversion: { debit?: AmountJson; credit?: AmountJson }) {
+ const url = new URL(`cashin-rate`, this.baseUrl);
+ if (conversion.debit) {
+ url.searchParams.set("amount_debit", Amounts.stringify(conversion.debit));
+ }
+ if (conversion.credit) {
+ url.searchParams.set(
+ "amount_credit",
+ Amounts.stringify(conversion.credit),
+ );
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForCashinConversionResponse());
+ case HttpStatusCode.BadRequest: {
+ const body = await resp.json();
+ const details = codecForTalerErrorDetail().decode(body);
+ switch (details.code) {
+ case TalerErrorCode.GENERIC_PARAMETER_MISSING:
+ return opKnownHttpFailure(resp.status, resp);
+ case TalerErrorCode.GENERIC_PARAMETER_MALFORMED:
+ return opKnownHttpFailure(resp.status, resp);
+ case TalerErrorCode.GENERIC_CURRENCY_MISMATCH:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, body);
+ }
+ }
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotImplemented:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-bank-conversion-info.html#get--cashout-rate
+ *
+ */
+ async getCashoutRate(conversion: {
+ debit?: AmountJson;
+ credit?: AmountJson;
+ }) {
+ const url = new URL(`cashout-rate`, this.baseUrl);
+ if (conversion.debit) {
+ url.searchParams.set("amount_debit", Amounts.stringify(conversion.debit));
+ }
+ if (conversion.credit) {
+ url.searchParams.set(
+ "amount_credit",
+ Amounts.stringify(conversion.credit),
+ );
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForCashoutConversionResponse());
+ case HttpStatusCode.BadRequest: {
+ const body = await resp.json();
+ const details = codecForTalerErrorDetail().decode(body);
+ switch (details.code) {
+ case TalerErrorCode.GENERIC_PARAMETER_MISSING:
+ return opKnownHttpFailure(resp.status, resp);
+ case TalerErrorCode.GENERIC_PARAMETER_MALFORMED:
+ return opKnownHttpFailure(resp.status, resp);
+ case TalerErrorCode.GENERIC_CURRENCY_MISMATCH:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, body);
+ }
+ }
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotImplemented:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-bank-conversion-info.html#post--conversion-rate
+ *
+ */
+ async updateConversionRate(
+ auth: AccessToken,
+ body: TalerBankConversionApi.ConversionRate,
+ ) {
+ const url = new URL(`conversion-rate`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ headers: {
+ Authorization: makeBearerTokenAuthHeader(auth),
+ },
+ body,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerBankConversionCacheEviction.UPDATE_RATE,
+ );
+ return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotImplemented:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+}
diff --git a/packages/taler-util/src/http-client/bank-core.ts b/packages/taler-util/src/http-client/bank-core.ts
new file mode 100644
index 000000000..6c8051ada
--- /dev/null
+++ b/packages/taler-util/src/http-client/bank-core.ts
@@ -0,0 +1,1038 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 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 {
+ AbsoluteTime,
+ HttpStatusCode,
+ LibtoolVersion,
+ LongPollParams,
+ OperationAlternative,
+ OperationFail,
+ OperationOk,
+ TalerErrorCode,
+ codecForChallenge,
+ codecForTanTransmission,
+ opKnownAlternativeFailure,
+ opKnownHttpFailure,
+ opKnownTalerFailure,
+} from "@gnu-taler/taler-util";
+import {
+ HttpRequestLibrary,
+ createPlatformHttpLib,
+ readTalerErrorResponse,
+} from "@gnu-taler/taler-util/http";
+import {
+ FailCasesByMethod,
+ ResultByMethod,
+ opEmptySuccess,
+ opFixedSuccess,
+ opSuccessFromHttp,
+ opUnknownFailure,
+} from "../operation.js";
+import {
+ AccessToken,
+ PaginationParams,
+ TalerCorebankApi,
+ UserAndToken,
+ WithdrawalOperationStatus,
+ codecForAccountData,
+ codecForBankAccountCreateWithdrawalResponse,
+ codecForBankAccountTransactionInfo,
+ codecForBankAccountTransactionsResponse,
+ codecForCashoutPending,
+ codecForCashoutStatusResponse,
+ codecForCashouts,
+ codecForCoreBankConfig,
+ codecForCreateTransactionResponse,
+ codecForGlobalCashouts,
+ codecForListBankAccountsResponse,
+ codecForMonitorResponse,
+ codecForPublicAccountsResponse,
+ codecForRegisterAccountResponse,
+ codecForWithdrawalPublicInfo,
+} from "./types.js";
+import {
+ CacheEvictor,
+ IdempotencyRetry,
+ addLongPollingParam,
+ addPaginationParams,
+ makeBearerTokenAuthHeader,
+ nullEvictor,
+} from "./utils.js";
+
+export type TalerCoreBankResultByMethod<
+ prop extends keyof TalerCoreBankHttpClient,
+> = ResultByMethod<TalerCoreBankHttpClient, prop>;
+export type TalerCoreBankErrorsByMethod<
+ prop extends keyof TalerCoreBankHttpClient,
+> = FailCasesByMethod<TalerCoreBankHttpClient, prop>;
+
+export enum TalerCoreBankCacheEviction {
+ DELETE_ACCOUNT,
+ CREATE_ACCOUNT,
+ UPDATE_ACCOUNT,
+ UPDATE_PASSWORD,
+ CREATE_TRANSACTION,
+ CONFIRM_WITHDRAWAL,
+ ABORT_WITHDRAWAL,
+ CREATE_WITHDRAWAL,
+ CREATE_CASHOUT,
+}
+/**
+ * Protocol version spoken with the core bank.
+ *
+ * Endpoint must be ordered in the same way that in the docs
+ * Response code (http and taler) must have the same order that in the docs
+ * That way is easier to see changes
+ *
+ * Uses libtool's current:revision:age versioning.
+ */
+export class TalerCoreBankHttpClient {
+ public readonly PROTOCOL_VERSION = "4:0:0";
+
+ httpLib: HttpRequestLibrary;
+ cacheEvictor: CacheEvictor<TalerCoreBankCacheEviction>;
+ constructor(
+ readonly baseUrl: string,
+ httpClient?: HttpRequestLibrary,
+ cacheEvictor?: CacheEvictor<TalerCoreBankCacheEviction>,
+ ) {
+ this.httpLib = httpClient ?? createPlatformHttpLib();
+ this.cacheEvictor = cacheEvictor ?? nullEvictor;
+ }
+
+ isCompatible(version: string): boolean {
+ const compare = LibtoolVersion.compare(this.PROTOCOL_VERSION, version);
+ return compare?.compatible ?? false;
+ }
+
+ /**
+ * https://docs.taler.net/core/api-corebank.html#config
+ *
+ */
+ async getConfig() {
+ const url = new URL(`config`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForCoreBankConfig());
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ //
+ // ACCOUNTS
+ //
+
+ /**
+ * https://docs.taler.net/core/api-corebank.html#post--accounts
+ *
+ */
+ async createAccount(
+ auth: AccessToken | undefined,
+ body: TalerCorebankApi.RegisterAccountRequest,
+ ) {
+ const url = new URL(`accounts`, this.baseUrl);
+ const headers: Record<string, string> = {};
+ if (auth) {
+ headers.Authorization = makeBearerTokenAuthHeader(auth);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body,
+ headers: headers,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok: {
+ await this.cacheEvictor.notifySuccess(
+ TalerCoreBankCacheEviction.CREATE_ACCOUNT,
+ );
+ return opSuccessFromHttp(resp, codecForRegisterAccountResponse());
+ }
+ case HttpStatusCode.BadRequest:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Unauthorized:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict: {
+ const details = await readTalerErrorResponse(resp);
+ switch (details.code) {
+ case TalerErrorCode.BANK_REGISTER_USERNAME_REUSE:
+ return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_REGISTER_PAYTO_URI_REUSE:
+ return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_UNALLOWED_DEBIT:
+ return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT:
+ return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT:
+ return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_NON_ADMIN_SET_MIN_CASHOUT:
+ return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_NON_ADMIN_SET_TAN_CHANNEL:
+ return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_TAN_CHANNEL_NOT_SUPPORTED:
+ return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_MISSING_TAN_INFO:
+ return opKnownTalerFailure(details.code, details);
+ default:
+ return opUnknownFailure(resp, details);
+ }
+ }
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+ /**
+ * https://docs.taler.net/core/api-corebank.html#delete--accounts-$USERNAME
+ *
+ */
+ async deleteAccount(auth: UserAndToken, cid?: string) {
+ const url = new URL(`accounts/${auth.username}`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "DELETE",
+ headers: {
+ Authorization: makeBearerTokenAuthHeader(auth.token),
+ "X-Challenge-Id": cid,
+ },
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Accepted:
+ return opKnownAlternativeFailure(
+ resp,
+ resp.status,
+ codecForChallenge(),
+ );
+ case HttpStatusCode.NoContent:
+ return opEmptySuccess(resp);
+ case HttpStatusCode.Unauthorized:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict: {
+ const details = await readTalerErrorResponse(resp);
+ switch (details.code) {
+ case TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT:
+ return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_ACCOUNT_BALANCE_NOT_ZERO:
+ return opKnownTalerFailure(details.code, details);
+ default:
+ return opUnknownFailure(resp, details);
+ }
+ }
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-corebank.html#patch--accounts-$USERNAME
+ *
+ */
+ async updateAccount(
+ auth: UserAndToken,
+ body: TalerCorebankApi.AccountReconfiguration,
+ cid?: string,
+ ) {
+ const url = new URL(`accounts/${auth.username}`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "PATCH",
+ body,
+ headers: {
+ Authorization: makeBearerTokenAuthHeader(auth.token),
+ "X-Challenge-Id": cid,
+ },
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Accepted:
+ return opKnownAlternativeFailure(
+ resp,
+ resp.status,
+ codecForChallenge(),
+ );
+ case HttpStatusCode.NoContent:
+ return opEmptySuccess(resp);
+ case HttpStatusCode.Unauthorized:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict: {
+ const details = await readTalerErrorResponse(resp);
+ switch (details.code) {
+ case TalerErrorCode.BANK_NON_ADMIN_PATCH_LEGAL_NAME:
+ return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_NON_ADMIN_PATCH_CASHOUT:
+ return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT:
+ return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_NON_ADMIN_SET_MIN_CASHOUT:
+ return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_TAN_CHANNEL_NOT_SUPPORTED:
+ return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_MISSING_TAN_INFO:
+ return opKnownTalerFailure(details.code, details);
+ default:
+ return opUnknownFailure(resp, details);
+ }
+ }
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-corebank.html#patch--accounts-$USERNAME-auth
+ *
+ */
+ async updatePassword(
+ auth: UserAndToken,
+ body: TalerCorebankApi.AccountPasswordChange,
+ cid?: string,
+ ) {
+ const url = new URL(`accounts/${auth.username}/auth`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "PATCH",
+ body,
+ headers: {
+ Authorization: makeBearerTokenAuthHeader(auth.token),
+ "X-Challenge-Id": cid,
+ },
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Accepted:
+ return opKnownAlternativeFailure(
+ resp,
+ resp.status,
+ codecForChallenge(),
+ );
+ case HttpStatusCode.NoContent:
+ return opEmptySuccess(resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Unauthorized:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict: {
+ const details = await readTalerErrorResponse(resp);
+ switch (details.code) {
+ case TalerErrorCode.BANK_NON_ADMIN_PATCH_MISSING_OLD_PASSWORD:
+ return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_PATCH_BAD_OLD_PASSWORD:
+ return opKnownTalerFailure(details.code, details);
+ default:
+ return opUnknownFailure(resp, details);
+ }
+ }
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-corebank.html#get--public-accounts
+ *
+ */
+ async getPublicAccounts(
+ filter: { account?: string } = {},
+ pagination?: PaginationParams,
+ ) {
+ const url = new URL(`public-accounts`, this.baseUrl);
+ addPaginationParams(url, pagination);
+ if (filter.account !== undefined) {
+ url.searchParams.set("filter_name", filter.account);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForPublicAccountsResponse());
+ case HttpStatusCode.NoContent:
+ return opFixedSuccess({ public_accounts: [] });
+ case HttpStatusCode.NotFound:
+ return opFixedSuccess({ public_accounts: [] });
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-corebank.html#get--accounts
+ *
+ */
+ async getAccounts(
+ auth: AccessToken,
+ filter: { account?: string } = {},
+ pagination?: PaginationParams,
+ ) {
+ const url = new URL(`accounts`, this.baseUrl);
+ addPaginationParams(url, pagination);
+ if (filter.account !== undefined) {
+ url.searchParams.set("filter_name", filter.account);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers: {
+ Authorization: makeBearerTokenAuthHeader(auth),
+ },
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForListBankAccountsResponse());
+ case HttpStatusCode.NoContent:
+ return opFixedSuccess({ accounts: [] });
+ case HttpStatusCode.Unauthorized:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-corebank.html#get--accounts-$USERNAME
+ *
+ */
+ async getAccount(auth: UserAndToken) {
+ const url = new URL(`accounts/${auth.username}`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers: {
+ Authorization: makeBearerTokenAuthHeader(auth.token),
+ },
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForAccountData());
+ case HttpStatusCode.Unauthorized:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ //
+ // TRANSACTIONS
+ //
+
+ /**
+ * https://docs.taler.net/core/api-corebank.html#get--accounts-$USERNAME-transactions
+ *
+ */
+ async getTransactions(
+ auth: UserAndToken,
+ params?: PaginationParams & LongPollParams,
+ ) {
+ const url = new URL(`accounts/${auth.username}/transactions`, this.baseUrl);
+ addPaginationParams(url, params);
+ addLongPollingParam(url, params);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers: {
+ Authorization: makeBearerTokenAuthHeader(auth.token),
+ },
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(
+ resp,
+ codecForBankAccountTransactionsResponse(),
+ );
+ case HttpStatusCode.NoContent:
+ return opFixedSuccess({ transactions: [] });
+ case HttpStatusCode.Unauthorized:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-corebank.html#get--accounts-$USERNAME-transactions-$TRANSACTION_ID
+ *
+ */
+ async getTransactionById(auth: UserAndToken, txid: number) {
+ const url = new URL(
+ `accounts/${auth.username}/transactions/${String(txid)}`,
+ this.baseUrl,
+ );
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers: {
+ Authorization: makeBearerTokenAuthHeader(auth.token),
+ },
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForBankAccountTransactionInfo());
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Unauthorized:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-transactions
+ *
+ */
+ async createTransaction(
+ auth: UserAndToken,
+ body: TalerCorebankApi.CreateTransactionRequest,
+ idempotencyCheck: IdempotencyRetry | undefined,
+ cid?: string,
+ ): Promise<
+ //manually definition all return types because of recursion
+ | OperationOk<TalerCorebankApi.CreateTransactionResponse>
+ | OperationAlternative<HttpStatusCode.Accepted, TalerCorebankApi.Challenge>
+ | OperationFail<HttpStatusCode.NotFound>
+ | OperationFail<HttpStatusCode.BadRequest>
+ | OperationFail<HttpStatusCode.Unauthorized>
+ | OperationFail<TalerErrorCode.BANK_UNALLOWED_DEBIT>
+ | OperationFail<TalerErrorCode.BANK_ADMIN_CREDITOR>
+ | OperationFail<TalerErrorCode.BANK_SAME_ACCOUNT>
+ | OperationFail<TalerErrorCode.BANK_UNKNOWN_CREDITOR>
+ | OperationFail<TalerErrorCode.BANK_TRANSFER_REQUEST_UID_REUSED>
+ > {
+ const url = new URL(`accounts/${auth.username}/transactions`, this.baseUrl);
+ if (idempotencyCheck) {
+ body.request_uid = idempotencyCheck.uid;
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ headers: {
+ Authorization: makeBearerTokenAuthHeader(auth.token),
+ "X-Challenge-Id": cid,
+ },
+ body,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForCreateTransactionResponse());
+ case HttpStatusCode.Accepted:
+ return opKnownAlternativeFailure(
+ resp,
+ resp.status,
+ codecForChallenge(),
+ );
+ case HttpStatusCode.BadRequest:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Unauthorized:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict: {
+ const details = await readTalerErrorResponse(resp);
+ switch (details.code) {
+ case TalerErrorCode.BANK_ADMIN_CREDITOR:
+ return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_SAME_ACCOUNT:
+ return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_UNKNOWN_CREDITOR:
+ return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_UNALLOWED_DEBIT:
+ return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_TRANSFER_REQUEST_UID_REUSED:
+ if (!idempotencyCheck) {
+ return opKnownTalerFailure(details.code, details);
+ }
+ const nextRetry = idempotencyCheck.next();
+ return this.createTransaction(auth, body, nextRetry, cid);
+ default:
+ return opUnknownFailure(resp, details);
+ }
+ }
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ //
+ // WITHDRAWALS
+ //
+
+ /**
+ * https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-withdrawals
+ *
+ */
+ async createWithdrawal(
+ auth: UserAndToken,
+ body: TalerCorebankApi.BankAccountCreateWithdrawalRequest,
+ ) {
+ const url = new URL(`accounts/${auth.username}/withdrawals`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ headers: {
+ Authorization: makeBearerTokenAuthHeader(auth.token),
+ },
+ body,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(
+ resp,
+ codecForBankAccountCreateWithdrawalResponse(),
+ );
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ //FIXME: missing in docs
+ case HttpStatusCode.Unauthorized:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-withdrawals-$WITHDRAWAL_ID-confirm
+ *
+ */
+ async confirmWithdrawalById(auth: UserAndToken, wid: string, cid?: string) {
+ const url = new URL(
+ `accounts/${auth.username}/withdrawals/${wid}/confirm`,
+ this.baseUrl,
+ );
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ headers: {
+ Authorization: makeBearerTokenAuthHeader(auth.token),
+ "X-Challenge-Id": cid,
+ },
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Accepted:
+ return opKnownAlternativeFailure(
+ resp,
+ resp.status,
+ codecForChallenge(),
+ );
+ case HttpStatusCode.NoContent:
+ return opEmptySuccess(resp);
+ //FIXME: missing in docs
+ case HttpStatusCode.BadRequest:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict: {
+ const details = await readTalerErrorResponse(resp);
+ switch (details.code) {
+ case TalerErrorCode.BANK_CONFIRM_ABORT_CONFLICT:
+ return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_CONFIRM_INCOMPLETE:
+ return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_UNALLOWED_DEBIT:
+ return opKnownTalerFailure(details.code, details);
+ default:
+ return opUnknownFailure(resp, details);
+ }
+ }
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-withdrawals-$WITHDRAWAL_ID-abort
+ *
+ */
+ async abortWithdrawalById(auth: UserAndToken, wid: string) {
+ const url = new URL(
+ `accounts/${auth.username}/withdrawals/${wid}/abort`,
+ this.baseUrl,
+ );
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ headers: {
+ Authorization: makeBearerTokenAuthHeader(auth.token),
+ },
+ });
+ switch (resp.status) {
+ case HttpStatusCode.NoContent:
+ return opEmptySuccess(resp);
+ //FIXME: missing in docs
+ case HttpStatusCode.BadRequest:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-corebank.html#get--withdrawals-$WITHDRAWAL_ID
+ *
+ */
+ async getWithdrawalById(
+ wid: string,
+ params?: {
+ old_state?: WithdrawalOperationStatus;
+ } & LongPollParams,
+ ) {
+ const url = new URL(`withdrawals/${wid}`, this.baseUrl);
+ addLongPollingParam(url, params);
+ if (params) {
+ url.searchParams.set(
+ "old_state",
+ !params.old_state ? "pending" : params.old_state,
+ );
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForWithdrawalPublicInfo());
+ //FIXME: missing in docs
+ case HttpStatusCode.BadRequest:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ //
+ // CASHOUTS
+ //
+
+ /**
+ * https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-cashouts
+ *
+ */
+ async createCashout(
+ auth: UserAndToken,
+ body: TalerCorebankApi.CashoutRequest,
+ cid?: string,
+ ) {
+ const url = new URL(`accounts/${auth.username}/cashouts`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ headers: {
+ Authorization: makeBearerTokenAuthHeader(auth.token),
+ "X-Challenge-Id": cid,
+ },
+ body,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForCashoutPending());
+ case HttpStatusCode.Accepted:
+ return opKnownAlternativeFailure(
+ resp,
+ resp.status,
+ codecForChallenge(),
+ );
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict: {
+ const details = await readTalerErrorResponse(resp);
+ switch (details.code) {
+ case TalerErrorCode.BANK_TRANSFER_REQUEST_UID_REUSED:
+ return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_CONVERSION_AMOUNT_TO_SMALL:
+ return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_BAD_CONVERSION:
+ return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_UNALLOWED_DEBIT:
+ return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_CONFIRM_INCOMPLETE:
+ return opKnownTalerFailure(details.code, details);
+ default:
+ return opUnknownFailure(resp, details);
+ }
+ }
+ case HttpStatusCode.BadGateway: {
+ const details = await readTalerErrorResponse(resp);
+ switch (details.code) {
+ case TalerErrorCode.BANK_TAN_CHANNEL_SCRIPT_FAILED:
+ return opKnownTalerFailure(details.code, details);
+ default:
+ return opUnknownFailure(resp, details);
+ }
+ }
+ case HttpStatusCode.NotImplemented:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-corebank.html#get--accounts-$USERNAME-cashouts-$CASHOUT_ID
+ *
+ */
+ async getCashoutById(auth: UserAndToken, cid: number) {
+ const url = new URL(
+ `accounts/${auth.username}/cashouts/${cid}`,
+ this.baseUrl,
+ );
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers: {
+ Authorization: makeBearerTokenAuthHeader(auth.token),
+ },
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForCashoutStatusResponse());
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotImplemented:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-corebank.html#get--accounts-$USERNAME-cashouts
+ *
+ */
+ async getAccountCashouts(auth: UserAndToken, pagination?: PaginationParams) {
+ const url = new URL(`accounts/${auth.username}/cashouts`, this.baseUrl);
+ addPaginationParams(url, pagination);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers: {
+ Authorization: makeBearerTokenAuthHeader(auth.token),
+ },
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForCashouts());
+ case HttpStatusCode.NoContent:
+ return opFixedSuccess({ cashouts: [] });
+ case HttpStatusCode.NotImplemented:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-corebank.html#get--cashouts
+ *
+ */
+ async getGlobalCashouts(auth: AccessToken, pagination?: PaginationParams) {
+ const url = new URL(`cashouts`, this.baseUrl);
+ addPaginationParams(url, pagination);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers: {
+ Authorization: makeBearerTokenAuthHeader(auth),
+ },
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForGlobalCashouts());
+ case HttpStatusCode.NoContent:
+ return opFixedSuccess({ cashouts: [] });
+ case HttpStatusCode.NotImplemented:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ //
+ // 2FA
+ //
+
+ /**
+ * https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-challenge-$CHALLENGE_ID
+ *
+ */
+ async sendChallenge(auth: UserAndToken, cid: string) {
+ const url = new URL(
+ `accounts/${auth.username}/challenge/${cid}`,
+ this.baseUrl,
+ );
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ headers: {
+ Authorization: makeBearerTokenAuthHeader(auth.token),
+ },
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForTanTransmission());
+ case HttpStatusCode.Unauthorized:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.BadGateway: {
+ const details = await readTalerErrorResponse(resp);
+ switch (details.code) {
+ case TalerErrorCode.BANK_TAN_CHANNEL_SCRIPT_FAILED:
+ return opKnownTalerFailure(details.code, details);
+ default:
+ return opUnknownFailure(resp, details);
+ }
+ }
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-challenge-$CHALLENGE_ID-confirm
+ *
+ */
+ async confirmChallenge(
+ auth: UserAndToken,
+ cid: string,
+ body: TalerCorebankApi.ChallengeSolve,
+ ) {
+ const url = new URL(
+ `accounts/${auth.username}/challenge/${cid}/confirm`,
+ this.baseUrl,
+ );
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ headers: {
+ Authorization: makeBearerTokenAuthHeader(auth.token),
+ },
+ body,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.NoContent:
+ return opEmptySuccess(resp);
+ case HttpStatusCode.Unauthorized:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict: {
+ const details = await readTalerErrorResponse(resp);
+ switch (details.code) {
+ case TalerErrorCode.BANK_TAN_CHALLENGE_EXPIRED:
+ return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_TAN_CHALLENGE_FAILED:
+ return opKnownTalerFailure(details.code, details);
+ default:
+ return opUnknownFailure(resp, details);
+ }
+ }
+ case HttpStatusCode.TooManyRequests:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ //
+ // MONITOR
+ //
+
+ /**
+ * https://docs.taler.net/core/api-corebank.html#get--monitor
+ *
+ */
+ async getMonitor(
+ auth: AccessToken,
+ params: {
+ timeframe?: TalerCorebankApi.MonitorTimeframeParam;
+ date?: AbsoluteTime;
+ } = {},
+ ) {
+ const url = new URL(`monitor`, this.baseUrl);
+ if (params.timeframe) {
+ url.searchParams.set(
+ "timeframe",
+ TalerCorebankApi.MonitorTimeframeParam[params.timeframe],
+ );
+ }
+ if (params.date) {
+ const { t_s: seconds } = AbsoluteTime.toProtocolTimestamp(params.date);
+ if (seconds !== "never") {
+ url.searchParams.set("date_s", String(seconds));
+ }
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers: {
+ Authorization: makeBearerTokenAuthHeader(auth),
+ },
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForMonitorResponse());
+ case HttpStatusCode.BadRequest:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Unauthorized:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ //
+ // Others API
+ //
+
+ /**
+ * https://docs.taler.net/core/api-corebank.html#taler-bank-integration-api
+ *
+ */
+ getIntegrationAPI(): URL {
+ return new URL(`taler-integration/`, this.baseUrl);
+ }
+
+ /**
+ * https://docs.taler.net/core/api-corebank.html#taler-bank-integration-api
+ *
+ */
+ getWireGatewayAPI(username: string): URL {
+ return new URL(`accounts/${username}/taler-wire-gateway/`, this.baseUrl);
+ }
+
+ /**
+ * https://docs.taler.net/core/api-corebank.html#taler-bank-integration-api
+ *
+ */
+ getRevenueAPI(username: string): URL {
+ return new URL(`accounts/${username}/taler-revenue/`, this.baseUrl);
+ }
+
+ /**
+ * https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-token
+ *
+ */
+ getAuthenticationAPI(username: string): URL {
+ return new URL(`accounts/${username}/`, this.baseUrl);
+ }
+
+ /**
+ * https://docs.taler.net/core/api-corebank.html#post--accounts-$USERNAME-token
+ *
+ */
+ getConversionInfoAPI(): URL {
+ return new URL(`conversion-info/`, this.baseUrl);
+ }
+}
diff --git a/packages/taler-util/src/http-client/bank-integration.ts b/packages/taler-util/src/http-client/bank-integration.ts
new file mode 100644
index 000000000..75e6a627a
--- /dev/null
+++ b/packages/taler-util/src/http-client/bank-integration.ts
@@ -0,0 +1,179 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 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, readTalerErrorResponse } from "../http-common.js";
+import { HttpStatusCode } from "../http-status-codes.js";
+import { createPlatformHttpLib } from "../http.js";
+import { LibtoolVersion } from "../libtool-version.js";
+import {
+ FailCasesByMethod,
+ ResultByMethod,
+ opEmptySuccess,
+ opKnownHttpFailure,
+ opKnownTalerFailure,
+ opSuccessFromHttp,
+ opUnknownFailure,
+} from "../operation.js";
+import { TalerErrorCode } from "../taler-error-codes.js";
+import { codecForTalerErrorDetail } from "../wallet-types.js";
+import {
+ LongPollParams,
+ TalerBankIntegrationApi,
+ WithdrawalOperationStatus,
+ codecForBankWithdrawalOperationPostResponse,
+ codecForBankWithdrawalOperationStatus,
+ codecForIntegrationBankConfig,
+} from "./types.js";
+import { addLongPollingParam } from "./utils.js";
+
+export type TalerBankIntegrationResultByMethod<
+ prop extends keyof TalerBankIntegrationHttpClient,
+> = ResultByMethod<TalerBankIntegrationHttpClient, prop>;
+export type TalerBankIntegrationErrorsByMethod<
+ prop extends keyof TalerBankIntegrationHttpClient,
+> = FailCasesByMethod<TalerBankIntegrationHttpClient, prop>;
+
+/**
+ * The API is used by the wallets.
+ */
+export class TalerBankIntegrationHttpClient {
+ public readonly PROTOCOL_VERSION = "2:0:2";
+
+ httpLib: HttpRequestLibrary;
+
+ constructor(
+ readonly baseUrl: string,
+ httpClient?: HttpRequestLibrary,
+ ) {
+ this.httpLib = httpClient ?? createPlatformHttpLib();
+ }
+
+ isCompatible(version: string): boolean {
+ const compare = LibtoolVersion.compare(this.PROTOCOL_VERSION, version);
+ return compare?.compatible ?? false;
+ }
+
+ /**
+ * https://docs.taler.net/core/api-bank-integration.html#get--config
+ *
+ */
+ async getConfig() {
+ const url = new URL(`config`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForIntegrationBankConfig());
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-bank-integration.html#get--withdrawal-operation-$WITHDRAWAL_ID
+ *
+ */
+ async getWithdrawalOperationById(
+ woid: string,
+ params?: {
+ old_state?: WithdrawalOperationStatus;
+ } & LongPollParams,
+ ) {
+ const url = new URL(`withdrawal-operation/${woid}`, this.baseUrl);
+ addLongPollingParam(url, params);
+ if (params) {
+ url.searchParams.set(
+ "old_state",
+ !params.old_state ? "pending" : params.old_state,
+ );
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForBankWithdrawalOperationStatus());
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-bank-integration.html#post-$BANK_API_BASE_URL-withdrawal-operation-$wopid
+ *
+ */
+ async completeWithdrawalOperationById(
+ woid: string,
+ body: TalerBankIntegrationApi.BankWithdrawalOperationPostRequest,
+ ) {
+ const url = new URL(`withdrawal-operation/${woid}`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(
+ resp,
+ codecForBankWithdrawalOperationPostResponse(),
+ );
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict: {
+ const body = await readTalerErrorResponse(resp);
+ const details = codecForTalerErrorDetail().decode(body);
+ switch (details.code) {
+ case TalerErrorCode.BANK_WITHDRAWAL_OPERATION_RESERVE_SELECTION_CONFLICT:
+ return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_DUPLICATE_RESERVE_PUB_SUBJECT:
+ return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_UNKNOWN_ACCOUNT:
+ return opKnownTalerFailure(details.code, details);
+ case TalerErrorCode.BANK_ACCOUNT_IS_NOT_EXCHANGE:
+ return opKnownTalerFailure(details.code, details);
+ default:
+ return opUnknownFailure(resp, details);
+ }
+ }
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-bank-integration.html#post-$BANK_API_BASE_URL-withdrawal-operation-$wopid
+ *
+ */
+ async abortWithdrawalOperationById(woid: string) {
+ const url = new URL(`withdrawal-operation/${woid}/abort`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ });
+ switch (resp.status) {
+ case HttpStatusCode.NoContent:
+ return opEmptySuccess(resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+}
diff --git a/packages/taler-util/src/http-client/bank-revenue.ts b/packages/taler-util/src/http-client/bank-revenue.ts
new file mode 100644
index 000000000..34afe7d86
--- /dev/null
+++ b/packages/taler-util/src/http-client/bank-revenue.ts
@@ -0,0 +1,130 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 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,
+ makeBasicAuthHeader,
+ readTalerErrorResponse,
+} from "../http-common.js";
+import { HttpStatusCode } from "../http-status-codes.js";
+import { createPlatformHttpLib } from "../http.js";
+import { LibtoolVersion } from "../libtool-version.js";
+import {
+ FailCasesByMethod,
+ ResultByMethod,
+ opKnownHttpFailure,
+ opSuccessFromHttp,
+ opUnknownFailure,
+} from "../operation.js";
+import {
+ LongPollParams,
+ PaginationParams,
+ codecForRevenueConfig,
+ codecForRevenueIncomingHistory,
+} from "./types.js";
+import { addLongPollingParam, addPaginationParams } from "./utils.js";
+
+export type TalerBankRevenueResultByMethod<
+ prop extends keyof TalerRevenueHttpClient,
+> = ResultByMethod<TalerRevenueHttpClient, prop>;
+export type TalerBankRevenueErrorsByMethod<
+ prop extends keyof TalerRevenueHttpClient,
+> = FailCasesByMethod<TalerRevenueHttpClient, prop>;
+
+type UsernameAndPassword = {
+ username: string;
+ password: string;
+};
+/**
+ * The API is used by the merchant (or other parties) to query
+ * for incoming transactions to their account.
+ */
+export class TalerRevenueHttpClient {
+ httpLib: HttpRequestLibrary;
+
+ constructor(
+ readonly baseUrl: string,
+ httpClient?: HttpRequestLibrary,
+ ) {
+ this.httpLib = httpClient ?? createPlatformHttpLib();
+ }
+
+ public readonly PROTOCOL_VERSION = "0:0:0";
+
+ isCompatible(version: string): boolean {
+ const compare = LibtoolVersion.compare(this.PROTOCOL_VERSION, version);
+ return compare?.compatible ?? false;
+ }
+
+ /**
+ * https://docs.taler.net/core/api-bank-revenue.html#get--config
+ *
+ */
+ async getConfig(auth?: UsernameAndPassword) {
+ const url = new URL(`config`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers: {
+ Authorization: auth
+ ? makeBasicAuthHeader(auth.username, auth.password)
+ : undefined,
+ },
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForRevenueConfig());
+ case HttpStatusCode.Unauthorized:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+ /**
+ * https://docs.taler.net/core/api-bank-revenue.html#get--history
+ *
+ * @returns
+ */
+ async getHistory(
+ auth?: UsernameAndPassword,
+ params?: PaginationParams & LongPollParams,
+ ) {
+ const url = new URL(`history`, this.baseUrl);
+ addPaginationParams(url, params);
+ addLongPollingParam(url, params);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers: {
+ Authorization: auth
+ ? makeBasicAuthHeader(auth.username, auth.password)
+ : undefined,
+ },
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForRevenueIncomingHistory());
+ case HttpStatusCode.BadRequest:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Unauthorized:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+}
diff --git a/packages/taler-util/src/http-client/bank-wire.ts b/packages/taler-util/src/http-client/bank-wire.ts
new file mode 100644
index 000000000..a8c976a80
--- /dev/null
+++ b/packages/taler-util/src/http-client/bank-wire.ts
@@ -0,0 +1,226 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 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, makeBasicAuthHeader, readTalerErrorResponse } from "../http-common.js";
+import { HttpStatusCode } from "../http-status-codes.js";
+import { createPlatformHttpLib } from "../http.js";
+import {
+ FailCasesByMethod,
+ ResultByMethod,
+ opFixedSuccess,
+ opKnownHttpFailure,
+ opSuccessFromHttp,
+ opUnknownFailure,
+} from "../operation.js";
+import {
+ LongPollParams,
+ PaginationParams,
+ TalerWireGatewayApi,
+ codecForAddIncomingResponse,
+ codecForIncomingHistory,
+ codecForOutgoingHistory,
+ codecForTransferResponse,
+} from "./types.js";
+import { addLongPollingParam, addPaginationParams } from "./utils.js";
+
+export type TalerWireGatewayResultByMethod<
+ prop extends keyof TalerWireGatewayHttpClient,
+> = ResultByMethod<TalerWireGatewayHttpClient, prop>;
+export type TalerWireGatewayErrorsByMethod<
+ prop extends keyof TalerWireGatewayHttpClient,
+> = FailCasesByMethod<TalerWireGatewayHttpClient, prop>;
+
+/**
+ * The API is used by the exchange to trigger transactions and query
+ * incoming transactions, as well as by the auditor to query incoming
+ * and outgoing transactions.
+ *
+ * https://docs.taler.net/core/api-bank-wire.html
+ */
+export class TalerWireGatewayHttpClient {
+ httpLib: HttpRequestLibrary;
+
+ constructor(
+ readonly baseUrl: string,
+ readonly username: string,
+ httpClient?: HttpRequestLibrary,
+ ) {
+ this.httpLib = httpClient ?? createPlatformHttpLib();
+ }
+ // public readonly PROTOCOL_VERSION = "4:0:0";
+ // isCompatible(version: string): boolean {
+ // const compare = LibtoolVersion.compare(this.PROTOCOL_VERSION, version)
+ // return compare?.compatible ?? false
+ // }
+
+ // /**
+ // * https://docs.taler.net/core/api-corebank.html#config
+ // *
+ // */
+ // async getConfig() {
+ // const url = new URL(`config`, this.baseUrl);
+ // const resp = await this.httpLib.fetch(url.href, {
+ // method: "GET"
+ // });
+ // switch (resp.status) {
+ // case HttpStatusCode.Ok: return opSuccess(resp, codecForCoreBankConfig())
+ // default: return opUnknownFailure(resp, await readTalerErrorResponse(resp))
+ // }
+ // }
+
+ /**
+ * https://docs.taler.net/core/api-bank-wire.html#post--transfer
+ *
+ */
+ async transfer(auth: string, body: TalerWireGatewayApi.TransferRequest) {
+ const url = new URL(`transfer`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ headers: {
+ Authorization: makeBasicAuthHeader(this.username, auth),
+ },
+ body,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForTransferResponse());
+ //FIXME: show more details in docs
+ case HttpStatusCode.BadRequest:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Unauthorized:
+ return opKnownHttpFailure(resp.status, resp);
+ //FIXME: show more details in docs
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-bank-wire.html#get--history-incoming
+ *
+ */
+ async getHistoryIncoming(
+ auth: string,
+ params?: PaginationParams & LongPollParams,
+ ) {
+ const url = new URL(`history/incoming`, this.baseUrl);
+ addPaginationParams(url, params);
+ addLongPollingParam(url, params);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers: {
+ Authorization: makeBasicAuthHeader(this.username, auth),
+ },
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForIncomingHistory());
+ //FIXME: account should not be returned or make it optional
+ case HttpStatusCode.NoContent:
+ return opFixedSuccess({
+ incoming_transactions: [],
+ credit_account: undefined,
+ });
+ //FIXME: show more details in docs
+ case HttpStatusCode.BadRequest:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Unauthorized:
+ return opKnownHttpFailure(resp.status, resp);
+ //FIXME: show more details in docs
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-bank-wire.html#get--history-outgoing
+ *
+ */
+ async getHistoryOutgoing(
+ auth: string,
+ params?: PaginationParams & LongPollParams,
+ ) {
+ const url = new URL(`history/outgoing`, this.baseUrl);
+ addPaginationParams(url, params);
+ addLongPollingParam(url, params);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers: {
+ Authorization: makeBasicAuthHeader(this.username, auth),
+ },
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForOutgoingHistory());
+ //FIXME: account should not be returned or make it optional
+ case HttpStatusCode.NoContent:
+ return opFixedSuccess({
+ outgoing_transactions: [],
+ debit_account: undefined,
+ });
+ //FIXME: show more details in docs
+ case HttpStatusCode.BadRequest:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Unauthorized:
+ return opKnownHttpFailure(resp.status, resp);
+ //FIXME: show more details in docs
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-bank-wire.html#post--admin-add-incoming
+ *
+ */
+ async addIncoming(
+ auth: string,
+ body: TalerWireGatewayApi.AddIncomingRequest,
+ ) {
+ const url = new URL(`admin/add-incoming`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ headers: {
+ Authorization: makeBasicAuthHeader(this.username, auth),
+ },
+ body,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForAddIncomingResponse());
+ //FIXME: show more details in docs
+ case HttpStatusCode.BadRequest:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Unauthorized:
+ return opKnownHttpFailure(resp.status, resp);
+ //FIXME: show more details in docs
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+}
diff --git a/packages/taler-util/src/http-client/challenger.ts b/packages/taler-util/src/http-client/challenger.ts
new file mode 100644
index 000000000..aa530570d
--- /dev/null
+++ b/packages/taler-util/src/http-client/challenger.ts
@@ -0,0 +1,291 @@
+import { HttpRequestLibrary, readTalerErrorResponse } from "../http-common.js";
+import { HttpStatusCode } from "../http-status-codes.js";
+import { createPlatformHttpLib } from "../http.js";
+import { TalerCoreBankCacheEviction } from "../index.node.js";
+import { LibtoolVersion } from "../libtool-version.js";
+import {
+ FailCasesByMethod,
+ RedirectResult,
+ ResultByMethod,
+ opFixedSuccess,
+ opKnownAlternativeFailure,
+ opKnownHttpFailure,
+ opSuccessFromHttp,
+ opUnknownFailure,
+} from "../operation.js";
+import {
+ AccessToken,
+ codecForChallengeCreateResponse,
+ codecForChallengeSetupResponse,
+ codecForChallengeStatus,
+ codecForChallengerAuthResponse,
+ codecForChallengerInfoResponse,
+ codecForChallengerTermsOfServiceResponse,
+ codecForInvalidPinResponse,
+} from "./types.js";
+import { CacheEvictor, makeBearerTokenAuthHeader, nullEvictor } from "./utils.js";
+
+export type ChallengerResultByMethod<prop extends keyof ChallengerHttpClient> =
+ ResultByMethod<ChallengerHttpClient, prop>;
+export type ChallengerErrorsByMethod<prop extends keyof ChallengerHttpClient> =
+ FailCasesByMethod<ChallengerHttpClient, prop>;
+
+export enum ChallengerCacheEviction {
+ CREATE_CHALLENGE,
+}
+
+/**
+ */
+export class ChallengerHttpClient {
+ httpLib: HttpRequestLibrary;
+ cacheEvictor: CacheEvictor<ChallengerCacheEviction>;
+ public readonly PROTOCOL_VERSION = "1:0:0";
+
+ constructor(
+ readonly baseUrl: string,
+ httpClient?: HttpRequestLibrary,
+ cacheEvictor?: CacheEvictor<ChallengerCacheEviction>,
+ ) {
+ this.httpLib = httpClient ?? createPlatformHttpLib();
+ this.cacheEvictor = cacheEvictor ?? nullEvictor;
+ }
+
+ isCompatible(version: string): boolean {
+ const compare = LibtoolVersion.compare(this.PROTOCOL_VERSION, version);
+ return compare?.compatible ?? false;
+ }
+ /**
+ * https://docs.taler.net/core/api-challenger.html#get--config
+ *
+ */
+ async getConfig() {
+ const url = new URL(`config`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(
+ resp,
+ codecForChallengerTermsOfServiceResponse(),
+ );
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+ /**
+ * https://docs.taler.net/core/api-challenger.html#post--setup-$CLIENT_ID
+ *
+ */
+ async setup(clientId: string, token: AccessToken) {
+ const url = new URL(`setup/${clientId}`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ headers: {
+ Authorization: makeBearerTokenAuthHeader(token),
+ },
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForChallengeSetupResponse());
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ // LOGIN
+
+ /**
+ * https://docs.taler.net/core/api-challenger.html#post--authorize-$NONCE
+ *
+ */
+ async login(
+ nonce: string,
+ clientId: string,
+ redirectUri: string,
+ state: string | undefined,
+ ) {
+ const url = new URL(`authorize/${nonce}`, this.baseUrl);
+ url.searchParams.set("response_type", "code");
+ url.searchParams.set("client_id", clientId);
+ url.searchParams.set("redirect_uri", redirectUri);
+ if (state) {
+ url.searchParams.set("state", state);
+ }
+ // url.searchParams.set("scope", "code");
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForChallengeStatus());
+ case HttpStatusCode.BadRequest:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotAcceptable:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.InternalServerError:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ // CHALLENGE
+
+ /**
+ * https://docs.taler.net/core/api-challenger.html#post--challenge-$NONCE
+ *
+ */
+ async challenge(nonce: string, body: Record<"email", string>) {
+ const url = new URL(`challenge/${nonce}`, this.baseUrl);
+
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body: new URLSearchParams(Object.entries(body)).toString(),
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded",
+ },
+ redirect: "manual",
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok: {
+ await this.cacheEvictor.notifySuccess(
+ ChallengerCacheEviction.CREATE_CHALLENGE,
+ );
+ return opSuccessFromHttp(resp, codecForChallengeCreateResponse());
+ }
+ case HttpStatusCode.Found:
+ const redirect = resp.headers.get("Location")!;
+ return opFixedSuccess<RedirectResult>({
+ redirectURL: new URL(redirect),
+ });
+ case HttpStatusCode.BadRequest:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotAcceptable:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.TooManyRequests:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.InternalServerError:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ // SOLVE
+
+ /**
+ * https://docs.taler.net/core/api-challenger.html#post--solve-$NONCE
+ *
+ */
+ async solve(nonce: string, body: Record<string, string>) {
+ const url = new URL(`solve/${nonce}`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body: new URLSearchParams(Object.entries(body)).toString(),
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded",
+ },
+ redirect: "manual",
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Found:
+ const redirect = resp.headers.get("Location")!;
+ return opFixedSuccess<RedirectResult>({
+ redirectURL: new URL(redirect),
+ });
+ case HttpStatusCode.BadRequest:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Forbidden:
+ return opKnownAlternativeFailure(
+ resp,
+ resp.status,
+ codecForInvalidPinResponse(),
+ );
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotAcceptable:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.TooManyRequests:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.InternalServerError:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ // AUTH
+
+ /**
+ * https://docs.taler.net/core/api-challenger.html#post--token
+ *
+ */
+ async token(
+ client_id: string,
+ redirect_uri: string,
+ client_secret: AccessToken,
+ code: string,
+ ) {
+ const url = new URL(`token`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded",
+ },
+ body: new URLSearchParams(
+ Object.entries({
+ client_id,
+ redirect_uri,
+ client_secret,
+ code,
+ grant_type: "authorization_code",
+ }),
+ ).toString(),
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForChallengerAuthResponse());
+ case HttpStatusCode.Forbidden:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ // INFO
+
+ /**
+ * https://docs.taler.net/core/api-challenger.html#get--info
+ *
+ */
+ async info(token: AccessToken) {
+ const url = new URL(`info`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers: {
+ Authorization: makeBearerTokenAuthHeader(token),
+ },
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForChallengerInfoResponse());
+ case HttpStatusCode.Forbidden:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+}
diff --git a/packages/taler-util/src/http-client/exchange.ts b/packages/taler-util/src/http-client/exchange.ts
new file mode 100644
index 000000000..68d68267f
--- /dev/null
+++ b/packages/taler-util/src/http-client/exchange.ts
@@ -0,0 +1,271 @@
+import { HttpRequestLibrary, readTalerErrorResponse } from "../http-common.js";
+import { HttpStatusCode } from "../http-status-codes.js";
+import { createPlatformHttpLib } from "../http.js";
+import { LibtoolVersion } from "../libtool-version.js";
+import { hash } from "../nacl-fast.js";
+import {
+ FailCasesByMethod,
+ ResultByMethod,
+ opEmptySuccess,
+ opFixedSuccess,
+ opKnownHttpFailure,
+ opSuccessFromHttp,
+ opUnknownFailure,
+} from "../operation.js";
+import {
+ TalerSignaturePurpose,
+ amountToBuffer,
+ bufferForUint32,
+ buildSigPS,
+ decodeCrock,
+ eddsaSign,
+ encodeCrock,
+ stringToBytes,
+ timestampRoundedToBuffer,
+} from "../taler-crypto.js";
+import {
+ OfficerAccount,
+ PaginationParams,
+ SigningKey,
+ TalerExchangeApi,
+ codecForAmlDecisionDetails,
+ codecForAmlRecords,
+ codecForExchangeConfig,
+ codecForExchangeKeys,
+} from "./types.js";
+import { CacheEvictor, addPaginationParams, nullEvictor } from "./utils.js";
+
+export type TalerExchangeResultByMethod<
+ prop extends keyof TalerExchangeHttpClient,
+> = ResultByMethod<TalerExchangeHttpClient, prop>;
+export type TalerExchangeErrorsByMethod<
+ prop extends keyof TalerExchangeHttpClient,
+> = FailCasesByMethod<TalerExchangeHttpClient, prop>;
+
+export enum TalerExchangeCacheEviction {
+ CREATE_DESCISION,
+}
+
+
+/**
+ */
+export class TalerExchangeHttpClient {
+ httpLib: HttpRequestLibrary;
+ public readonly PROTOCOL_VERSION = "18:0:1";
+ cacheEvictor: CacheEvictor<TalerExchangeCacheEviction>;
+
+ constructor(
+ readonly baseUrl: string,
+ httpClient?: HttpRequestLibrary,
+ cacheEvictor?: CacheEvictor<TalerExchangeCacheEviction>,
+ ) {
+ this.httpLib = httpClient ?? createPlatformHttpLib();
+ this.cacheEvictor = cacheEvictor ?? nullEvictor;
+ }
+
+ isCompatible(version: string): boolean {
+ const compare = LibtoolVersion.compare(this.PROTOCOL_VERSION, version);
+ return compare?.compatible ?? false;
+ }
+ /**
+ * https://docs.taler.net/core/api-exchange.html#get--seed
+ *
+ */
+ async getSeed() {
+ const url = new URL(`seed`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ const buffer = await resp.bytes();
+ const uintar = new Uint8Array(buffer);
+
+ return opFixedSuccess(uintar);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+ /**
+ * https://docs.taler.net/core/api-exchange.html#get--config
+ *
+ */
+ async getConfig() {
+ const url = new URL(`config`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForExchangeConfig());
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+ /**
+ * https://docs.taler.net/core/api-merchant.html#get--config
+ *
+ * PARTIALLY IMPLEMENTED!!
+ */
+ async getKeys() {
+ const url = new URL(`keys`, this.baseUrl);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForExchangeKeys());
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ // TERMS
+
+ //
+ // AML operations
+ //
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-decisions-$STATE
+ *
+ */
+ async getDecisionsByState(
+ auth: OfficerAccount,
+ state: TalerExchangeApi.AmlState,
+ pagination?: PaginationParams,
+ ) {
+ const url = new URL(
+ `aml/${auth.id}/decisions/${TalerExchangeApi.AmlState[state]}`,
+ this.baseUrl,
+ );
+ addPaginationParams(url, pagination);
+
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers: {
+ "Taler-AML-Officer-Signature": buildQuerySignature(auth.signingKey),
+ },
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForAmlRecords());
+ case HttpStatusCode.NoContent:
+ return opFixedSuccess({ records: [] });
+ //this should be unauthorized
+ case HttpStatusCode.Forbidden:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Unauthorized:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#get--aml-$OFFICER_PUB-decision-$H_PAYTO
+ *
+ */
+ async getDecisionDetails(auth: OfficerAccount, account: string) {
+ const url = new URL(`aml/${auth.id}/decision/${account}`, this.baseUrl);
+
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers: {
+ "Taler-AML-Officer-Signature": buildQuerySignature(auth.signingKey),
+ },
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForAmlDecisionDetails());
+ case HttpStatusCode.NoContent:
+ return opFixedSuccess({ aml_history: [], kyc_attributes: [] });
+ //this should be unauthorized
+ case HttpStatusCode.Forbidden:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Unauthorized:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-exchange.html#post--aml-$OFFICER_PUB-decision
+ *
+ */
+ async addDecisionDetails(
+ auth: OfficerAccount,
+ decision: Omit<TalerExchangeApi.AmlDecision, "officer_sig">,
+ ) {
+ const url = new URL(`aml/${auth.id}/decision`, this.baseUrl);
+
+ const body = buildDecisionSignature(auth.signingKey, decision);
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.NoContent:
+ return opEmptySuccess(resp);
+ //FIXME: this should be unauthorized
+ case HttpStatusCode.Forbidden:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Unauthorized:
+ return opKnownHttpFailure(resp.status, resp);
+ //FIXME: this two need to be split by error code
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+}
+
+function buildQuerySignature(key: SigningKey): string {
+ const sigBlob = buildSigPS(
+ TalerSignaturePurpose.TALER_SIGNATURE_AML_QUERY,
+ ).build();
+
+ return encodeCrock(eddsaSign(sigBlob, key));
+}
+
+function buildDecisionSignature(
+ key: SigningKey,
+ decision: Omit<TalerExchangeApi.AmlDecision, "officer_sig">,
+): TalerExchangeApi.AmlDecision {
+ const zero = new Uint8Array(new ArrayBuffer(64));
+
+ const sigBlob = buildSigPS(TalerSignaturePurpose.TALER_SIGNATURE_AML_DECISION)
+ //TODO: new need the null terminator, also in the exchange
+ .put(hash(stringToBytes(decision.justification))) //check null
+ .put(timestampRoundedToBuffer(decision.decision_time))
+ .put(amountToBuffer(decision.new_threshold))
+ .put(decodeCrock(decision.h_payto))
+ .put(zero) //kyc_requirement
+ .put(bufferForUint32(decision.new_state))
+ .build();
+
+ const officer_sig = encodeCrock(eddsaSign(sigBlob, key));
+ return {
+ ...decision,
+ officer_sig,
+ };
+}
diff --git a/packages/taler-util/src/http-client/merchant.ts b/packages/taler-util/src/http-client/merchant.ts
new file mode 100644
index 000000000..cad2c839e
--- /dev/null
+++ b/packages/taler-util/src/http-client/merchant.ts
@@ -0,0 +1,2370 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 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 {
+ AccessToken,
+ FailCasesByMethod,
+ HttpStatusCode,
+ LibtoolVersion,
+ PaginationParams,
+ ResultByMethod,
+ TalerMerchantApi,
+ codecForAbortResponse,
+ codecForAccountAddResponse,
+ codecForAccountKycRedirects,
+ codecForAccountsSummaryResponse,
+ codecForBankAccountEntry,
+ codecForClaimResponse,
+ codecForInstancesResponse,
+ codecForInventorySummaryResponse,
+ codecForMerchantConfig,
+ codecForMerchantOrderPrivateStatusResponse,
+ codecForMerchantPosProductDetail,
+ codecForMerchantRefundResponse,
+ codecForOrderHistory,
+ codecForOtpDeviceDetails,
+ codecForOtpDeviceSummaryResponse,
+ codecForOutOfStockResponse,
+ codecForPaidRefundStatusResponse,
+ codecForPaymentResponse,
+ codecForPostOrderResponse,
+ codecForProductDetail,
+ codecForQueryInstancesResponse,
+ codecForStatusGoto,
+ codecForStatusPaid,
+ codecForStatusStatusUnpaid,
+ codecForTansferList,
+ codecForTemplateDetails,
+ codecForTemplateSummaryResponse,
+ codecForTokenFamiliesList,
+ codecForTokenFamilyDetails,
+ codecForWalletRefundResponse,
+ codecForWalletTemplateDetails,
+ codecForWebhookDetails,
+ codecForWebhookSummaryResponse,
+ opEmptySuccess,
+ opKnownAlternativeFailure,
+ opKnownHttpFailure,
+} from "@gnu-taler/taler-util";
+import {
+ HttpRequestLibrary,
+ HttpResponse,
+ createPlatformHttpLib,
+ readTalerErrorResponse,
+} from "@gnu-taler/taler-util/http";
+import { opSuccessFromHttp, opUnknownFailure } from "../operation.js";
+import {
+ CacheEvictor,
+ addMerchantPaginationParams,
+ makeBearerTokenAuthHeader,
+ nullEvictor,
+} from "./utils.js";
+
+export type TalerMerchantInstanceResultByMethod<
+ prop extends keyof TalerMerchantInstanceHttpClient,
+> = ResultByMethod<TalerMerchantInstanceHttpClient, prop>;
+export type TalerMerchantInstanceErrorsByMethod<
+ prop extends keyof TalerMerchantInstanceHttpClient,
+> = FailCasesByMethod<TalerMerchantInstanceHttpClient, prop>;
+
+export enum TalerMerchantInstanceCacheEviction {
+ CREATE_ORDER,
+ UPDATE_ORDER,
+ DELETE_ORDER,
+ UPDATE_CURRENT_INSTANCE,
+ DELETE_CURRENT_INSTANCE,
+ CREATE_BANK_ACCOUNT,
+ UPDATE_BANK_ACCOUNT,
+ DELETE_BANK_ACCOUNT,
+ CREATE_PRODUCT,
+ UPDATE_PRODUCT,
+ DELETE_PRODUCT,
+ CREATE_TRANSFER,
+ DELETE_TRANSFER,
+ CREATE_DEVICE,
+ UPDATE_DEVICE,
+ DELETE_DEVICE,
+ CREATE_TEMPLATE,
+ UPDATE_TEMPLATE,
+ DELETE_TEMPLATE,
+ CREATE_WEBHOOK,
+ UPDATE_WEBHOOK,
+ DELETE_WEBHOOK,
+ CREATE_TOKENFAMILY,
+ UPDATE_TOKENFAMILY,
+ DELETE_TOKENFAMILY,
+ LAST,
+}
+export enum TalerMerchantManagementCacheEviction {
+ CREATE_INSTANCE = TalerMerchantInstanceCacheEviction.LAST + 1,
+ UPDATE_INSTANCE,
+ DELETE_INSTANCE,
+}
+/**
+ * Protocol version spoken with the core bank.
+ *
+ * Endpoint must be ordered in the same way that in the docs
+ * Response code (http and taler) must have the same order that in the docs
+ * That way is easier to see changes
+ *
+ * Uses libtool's current:revision:age versioning.
+ */
+export class TalerMerchantInstanceHttpClient {
+ public readonly PROTOCOL_VERSION = "15:0:0";
+
+ readonly httpLib: HttpRequestLibrary;
+ readonly cacheEvictor: CacheEvictor<TalerMerchantInstanceCacheEviction>;
+
+ constructor(
+ readonly baseUrl: string,
+ httpClient?: HttpRequestLibrary,
+ cacheEvictor?: CacheEvictor<TalerMerchantInstanceCacheEviction>,
+ ) {
+ this.httpLib = httpClient ?? createPlatformHttpLib();
+ this.cacheEvictor = cacheEvictor ?? nullEvictor;
+ }
+
+ isCompatible(version: string): boolean {
+ const compare = LibtoolVersion.compare(this.PROTOCOL_VERSION, version);
+ return compare?.compatible ?? false;
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#get--config
+ *
+ */
+ async getConfig() {
+ const url = new URL(`config`, this.baseUrl);
+
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForMerchantConfig());
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ //
+ // Wallet API
+ //
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-orders-$ORDER_ID-claim
+ */
+ async claimOrder(orderId: string, body: TalerMerchantApi.ClaimRequest) {
+ const url = new URL(`orders/${orderId}/claim`, this.baseUrl);
+
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.UPDATE_ORDER,
+ );
+ return opSuccessFromHttp(resp, codecForClaimResponse());
+ }
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-orders-$ORDER_ID-pay
+ */
+ async makePayment(orderId: string, body: TalerMerchantApi.PayRequest) {
+ const url = new URL(`orders/${orderId}/pay`, this.baseUrl);
+
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.UPDATE_ORDER,
+ );
+ return opSuccessFromHttp(resp, codecForPaymentResponse());
+ }
+ case HttpStatusCode.BadRequest:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.PaymentRequired:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Forbidden:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.RequestTimeout:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Gone:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.PreconditionFailed:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.BadGateway:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.GatewayTimeout:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-orders-$ORDER_ID
+ */
+
+ async getPaymentStatus(
+ orderId: string,
+ params: TalerMerchantApi.PaymentStatusRequestParams = {},
+ ) {
+ const url = new URL(`orders/${orderId}`, this.baseUrl);
+
+ if (params.allowRefundedForRepurchase !== undefined) {
+ url.searchParams.set(
+ "allow_refunded_for_repurchase",
+ params.allowRefundedForRepurchase ? "YES" : "NO",
+ );
+ }
+ if (params.awaitRefundObtained !== undefined) {
+ url.searchParams.set(
+ "await_refund_obtained",
+ params.allowRefundedForRepurchase ? "YES" : "NO",
+ );
+ }
+ if (params.claimToken !== undefined) {
+ url.searchParams.set("token", params.claimToken);
+ }
+ if (params.contractTermHash !== undefined) {
+ url.searchParams.set("h_contract", params.contractTermHash);
+ }
+ if (params.refund !== undefined) {
+ url.searchParams.set("refund", params.refund);
+ }
+ if (params.sessionId !== undefined) {
+ url.searchParams.set("session_id", params.sessionId);
+ }
+ if (params.timeout !== undefined) {
+ url.searchParams.set("timeout_ms", String(params.timeout));
+ }
+
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ // body,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForStatusPaid());
+ case HttpStatusCode.Accepted:
+ return opSuccessFromHttp(resp, codecForStatusGoto());
+ // case HttpStatusCode.Found: not possible since content is not HTML
+ case HttpStatusCode.PaymentRequired:
+ return opSuccessFromHttp(resp, codecForStatusStatusUnpaid());
+ case HttpStatusCode.Forbidden:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotAcceptable:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#demonstrating-payment
+ */
+ async demostratePayment(orderId: string, body: TalerMerchantApi.PaidRequest) {
+ const url = new URL(`orders/${orderId}/paid`, this.baseUrl);
+
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.UPDATE_ORDER,
+ );
+ return opSuccessFromHttp(resp, codecForPaidRefundStatusResponse());
+ }
+ case HttpStatusCode.BadRequest:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Forbidden:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#aborting-incomplete-payments
+ */
+ async abortIncompletePayment(
+ orderId: string,
+ body: TalerMerchantApi.AbortRequest,
+ ) {
+ const url = new URL(`orders/${orderId}/abort`, this.baseUrl);
+
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.UPDATE_ORDER,
+ );
+ return opSuccessFromHttp(resp, codecForAbortResponse());
+ }
+ case HttpStatusCode.BadRequest:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Forbidden:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#obtaining-refunds
+ */
+ async obtainRefund(
+ orderId: string,
+ body: TalerMerchantApi.WalletRefundRequest,
+ ) {
+ const url = new URL(`orders/${orderId}/refund`, this.baseUrl);
+
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.UPDATE_ORDER,
+ );
+ return opSuccessFromHttp(resp, codecForWalletRefundResponse());
+ }
+ case HttpStatusCode.BadRequest:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Forbidden:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ //
+ // Management
+ //
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-auth
+ */
+ async updateCurrentInstanceAuthentication(
+ token: AccessToken | undefined,
+ body: TalerMerchantApi.InstanceAuthConfigurationMessage,
+ ) {
+ const url = new URL(`private/auth`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body,
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok: // FIXME: missing in docs
+ return opEmptySuccess(resp);
+ case HttpStatusCode.NoContent:
+ return opEmptySuccess(resp);
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#patch-[-instances-$INSTANCE]-private
+ */
+ async updateCurrentInstance(
+ token: AccessToken | undefined,
+ body: TalerMerchantApi.InstanceReconfigurationMessage,
+ ) {
+ const url = new URL(`private`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "PATCH",
+ body,
+ headers,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.UPDATE_CURRENT_INSTANCE,
+ );
+ return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private
+ *
+ */
+ async getCurrentInstanceDetails(token: AccessToken) {
+ const url = new URL(`private`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForQueryInstancesResponse());
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCE]-private
+ */
+ async deleteCurrentInstance(
+ token: AccessToken | undefined,
+ params: { purge?: boolean } = {},
+ ) {
+ const url = new URL(`private`, this.baseUrl);
+
+ if (params.purge !== undefined) {
+ url.searchParams.set("purge", params.purge ? "YES" : "NO");
+ }
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "DELETE",
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.DELETE_CURRENT_INSTANCE,
+ );
+ return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#get--instances-$INSTANCE-private-kyc
+ */
+ async getCurrentIntanceKycStatus(
+ token: AccessToken | undefined,
+ params: TalerMerchantApi.GetKycStatusRequestParams = {},
+ ) {
+ const url = new URL(`private/kyc`, this.baseUrl);
+
+ if (params.wireHash) {
+ url.searchParams.set("h_wire", params.wireHash);
+ }
+ if (params.exchangeURL) {
+ url.searchParams.set("exchange_url", params.exchangeURL);
+ }
+ if (params.timeout) {
+ url.searchParams.set("timeout_ms", String(params.timeout));
+ }
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Accepted:
+ return opSuccessFromHttp(resp, codecForAccountKycRedirects());
+ case HttpStatusCode.NoContent:
+ return opEmptySuccess(resp);
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.BadGateway:
+ return opKnownAlternativeFailure(
+ resp,
+ resp.status,
+ codecForAccountKycRedirects(),
+ );
+ case HttpStatusCode.ServiceUnavailable:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.GatewayTimeout:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ //
+ // Bank Accounts
+ //
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-accounts
+ */
+ async addBankAccount(
+ token: AccessToken | undefined,
+ body: TalerMerchantApi.AccountAddDetails,
+ ) {
+ const url = new URL(`private/accounts`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body,
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.CREATE_BANK_ACCOUNT,
+ );
+ return opSuccessFromHttp(resp, codecForAccountAddResponse());
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#patch-[-instances-$INSTANCE]-private-accounts-$H_WIRE
+ */
+ async updateBankAccount(
+ token: AccessToken | undefined,
+ wireAccount: string,
+ body: TalerMerchantApi.AccountPatchDetails,
+ ) {
+ const url = new URL(`private/accounts/${wireAccount}`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "PATCH",
+ body,
+ headers,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.UPDATE_BANK_ACCOUNT,
+ );
+ return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-accounts
+ */
+ async listBankAccounts(token: AccessToken, params?: PaginationParams) {
+ const url = new URL(`private/accounts`, this.baseUrl);
+
+ // addMerchantPaginationParams(url, params);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForAccountsSummaryResponse());
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-accounts-$H_WIRE
+ */
+ async getBankAccountDetails(
+ token: AccessToken | undefined,
+ wireAccount: string,
+ ) {
+ const url = new URL(`private/accounts/${wireAccount}`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForBankAccountEntry());
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCE]-private-accounts-$H_WIRE
+ */
+ async deleteBankAccount(token: AccessToken | undefined, wireAccount: string) {
+ const url = new URL(`private/accounts/${wireAccount}`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "DELETE",
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.DELETE_BANK_ACCOUNT,
+ );
+ return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ //
+ // Inventory Management
+ //
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-products
+ */
+ async addProduct(
+ token: AccessToken | undefined,
+ body: TalerMerchantApi.ProductAddDetail,
+ ) {
+ const url = new URL(`private/products`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body,
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.CREATE_PRODUCT,
+ );
+ return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#patch-[-instances-$INSTANCE]-private-products-$PRODUCT_ID
+ */
+ async updateProduct(
+ token: AccessToken | undefined,
+ productId: string,
+ body: TalerMerchantApi.ProductPatchDetail,
+ ) {
+ const url = new URL(`private/products/${productId}`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "PATCH",
+ body,
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.UPDATE_PRODUCT,
+ );
+ return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-products
+ */
+ async listProducts(
+ token: AccessToken | undefined,
+ params?: PaginationParams,
+ ) {
+ const url = new URL(`private/products`, this.baseUrl);
+
+ addMerchantPaginationParams(url, params);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForInventorySummaryResponse());
+ case HttpStatusCode.Unauthorized: // FIXME: not in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-pos
+ */
+ async getPointOfSaleInventory(token: AccessToken | undefined) {
+ const url = new URL(`private/pos`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForMerchantPosProductDetail());
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-products-$PRODUCT_ID
+ */
+ async getProductDetails(token: AccessToken | undefined, productId: string) {
+ const url = new URL(`private/products/${productId}`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForProductDetail());
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#reserving-inventory
+ */
+ async lockProduct(
+ token: AccessToken | undefined,
+ productId: string,
+ body: TalerMerchantApi.LockRequest,
+ ) {
+ const url = new URL(`private/products/${productId}/lock`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body,
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.UPDATE_PRODUCT,
+ );
+ return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Gone:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#removing-products-from-inventory
+ */
+ async deleteProduct(token: AccessToken | undefined, productId: string) {
+ const url = new URL(`private/products/${productId}`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "DELETE",
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.DELETE_PRODUCT,
+ );
+ return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ //
+ // Payment processing
+ //
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-orders
+ */
+ async createOrder(
+ token: AccessToken | undefined,
+ body: TalerMerchantApi.PostOrderRequest,
+ ) {
+ const url = new URL(`private/orders`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body,
+ headers,
+ });
+ return this.procesOrderCreationResponse(resp);
+ }
+
+ private async procesOrderCreationResponse(resp: HttpResponse) {
+ switch (resp.status) {
+ case HttpStatusCode.Ok: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.CREATE_ORDER,
+ );
+ return opSuccessFromHttp(resp, codecForPostOrderResponse());
+ }
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Gone:
+ return opKnownAlternativeFailure(
+ resp,
+ resp.status,
+ codecForOutOfStockResponse(),
+ );
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#inspecting-orders
+ */
+ async listOrders(
+ token: AccessToken | undefined,
+ params: TalerMerchantApi.ListOrdersRequestParams = {},
+ ) {
+ const url = new URL(`private/orders`, this.baseUrl);
+
+ if (params.date) {
+ url.searchParams.set("date_s", String(params.date));
+ }
+ if (params.fulfillmentUrl) {
+ url.searchParams.set("fulfillment_url", params.fulfillmentUrl);
+ }
+ if (params.paid !== undefined) {
+ url.searchParams.set("paid", params.paid ? "YES" : "NO");
+ }
+ if (params.refunded !== undefined) {
+ url.searchParams.set("refunded", params.refunded ? "YES" : "NO");
+ }
+ if (params.sessionId) {
+ url.searchParams.set("session_id", params.sessionId);
+ }
+ if (params.timeout) {
+ url.searchParams.set("timeout", String(params.timeout));
+ }
+ if (params.wired !== undefined) {
+ url.searchParams.set("wired", params.wired ? "YES" : "NO");
+ }
+ addMerchantPaginationParams(url, params);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForOrderHistory());
+ case HttpStatusCode.NotFound: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-orders-$ORDER_ID
+ */
+ async getOrderDetails(
+ token: AccessToken | undefined,
+ orderId: string,
+ params: TalerMerchantApi.GetOrderRequestParams = {},
+ ) {
+ const url = new URL(`private/orders/${orderId}`, this.baseUrl);
+
+ if (params.allowRefundedForRepurchase !== undefined) {
+ url.searchParams.set(
+ "allow_refunded_for_repurchase",
+ params.allowRefundedForRepurchase ? "YES" : "NO",
+ );
+ }
+ if (params.sessionId) {
+ url.searchParams.set("session_id", params.sessionId);
+ }
+ if (params.timeout) {
+ url.searchParams.set("timeout_ms", String(params.timeout));
+ }
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(
+ resp,
+ codecForMerchantOrderPrivateStatusResponse(),
+ );
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.BadGateway:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.GatewayTimeout:
+ return opKnownAlternativeFailure(
+ resp,
+ resp.status,
+ codecForOutOfStockResponse(),
+ );
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#private-order-data-cleanup
+ */
+ async forgetOrder(
+ token: AccessToken | undefined,
+ orderId: string,
+ body: TalerMerchantApi.ForgetRequest,
+ ) {
+ const url = new URL(`private/orders/${orderId}/forget`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "PATCH",
+ body,
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.UPDATE_ORDER,
+ );
+ return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.NoContent:
+ return opEmptySuccess(resp);
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.BadRequest:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCE]-private-orders-$ORDER_ID
+ */
+ async deleteOrder(token: AccessToken | undefined, orderId: string) {
+ const url = new URL(`private/orders/${orderId}`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "DELETE",
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.DELETE_ORDER,
+ );
+ return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ //
+ // Refunds
+ //
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-orders-$ORDER_ID-refund
+ */
+ async addRefund(
+ token: AccessToken | undefined,
+ orderId: string,
+ body: TalerMerchantApi.RefundRequest,
+ ) {
+ const url = new URL(`private/orders/${orderId}/refund`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body,
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.UPDATE_ORDER,
+ );
+ return opSuccessFromHttp(resp, codecForMerchantRefundResponse());
+ }
+ case HttpStatusCode.Forbidden:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Gone:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ //
+ // Wire Transfer
+ //
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-transfers
+ */
+ async informWireTransfer(
+ token: AccessToken | undefined,
+ body: TalerMerchantApi.TransferInformation,
+ ) {
+ const url = new URL(`private/transfers`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body,
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.CREATE_TRANSFER,
+ );
+ return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-transfers
+ */
+ async listWireTransfers(
+ token: AccessToken | undefined,
+ params: TalerMerchantApi.ListWireTransferRequestParams = {},
+ ) {
+ const url = new URL(`private/transfers`, this.baseUrl);
+
+ if (params.after) {
+ url.searchParams.set("after", String(params.after));
+ }
+ if (params.before) {
+ url.searchParams.set("before", String(params.before));
+ }
+ if (params.paytoURI) {
+ url.searchParams.set("payto_uri", params.paytoURI);
+ }
+ if (params.verified !== undefined) {
+ url.searchParams.set("verified", params.verified ? "YES" : "NO");
+ }
+ addMerchantPaginationParams(url, params);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForTansferList());
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCE]-private-transfers-$TID
+ */
+ async deleteWireTransfer(token: AccessToken | undefined, transferId: string) {
+ const url = new URL(`private/transfers/${transferId}`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "DELETE",
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.DELETE_TRANSFER,
+ );
+ return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ //
+ // OTP Devices
+ //
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-otp-devices
+ */
+ async addOtpDevice(
+ token: AccessToken | undefined,
+ body: TalerMerchantApi.OtpDeviceAddDetails,
+ ) {
+ const url = new URL(`private/otp-devices`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body,
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.CREATE_DEVICE,
+ );
+ return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#patch-[-instances-$INSTANCE]-private-otp-devices-$DEVICE_ID
+ */
+ async updateOtpDevice(
+ token: AccessToken | undefined,
+ deviceId: string,
+ body: TalerMerchantApi.OtpDevicePatchDetails,
+ ) {
+ const url = new URL(`private/otp-devices/${deviceId}`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "PATCH",
+ body,
+ headers,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.UPDATE_DEVICE,
+ );
+ return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-otp-devices
+ */
+ async listOtpDevices(
+ token: AccessToken | undefined,
+ params?: PaginationParams,
+ ) {
+ const url = new URL(`private/otp-devices`, this.baseUrl);
+
+ addMerchantPaginationParams(url, params);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForOtpDeviceSummaryResponse());
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-otp-devices-$DEVICE_ID
+ */
+ async getOtpDeviceDetails(
+ token: AccessToken | undefined,
+ deviceId: string,
+ params: TalerMerchantApi.GetOtpDeviceRequestParams = {},
+ ) {
+ const url = new URL(`private/otp-devices/${deviceId}`, this.baseUrl);
+
+ if (params.faketime) {
+ url.searchParams.set("faketime", String(params.faketime));
+ }
+ if (params.price) {
+ url.searchParams.set("price", params.price);
+ }
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForOtpDeviceDetails());
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCE]-private-otp-devices-$DEVICE_ID
+ */
+ async deleteOtpDevice(token: AccessToken | undefined, deviceId: string) {
+ const url = new URL(`private/otp-devices/${deviceId}`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "DELETE",
+ headers,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.DELETE_DEVICE,
+ );
+ return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ //
+ // Templates
+ //
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-templates
+ */
+ async addTemplate(
+ token: AccessToken | undefined,
+ body: TalerMerchantApi.TemplateAddDetails,
+ ) {
+ const url = new URL(`private/templates`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body,
+ headers,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.CREATE_TEMPLATE,
+ );
+ return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#patch-[-instances-$INSTANCE]-private-templates-$TEMPLATE_ID
+ */
+ async updateTemplate(
+ token: AccessToken | undefined,
+ templateId: string,
+ body: TalerMerchantApi.TemplatePatchDetails,
+ ) {
+ const url = new URL(`private/templates/${templateId}`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "PATCH",
+ body,
+ headers,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.UPDATE_TEMPLATE,
+ );
+ return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#inspecting-template
+ */
+ async listTemplates(
+ token: AccessToken | undefined,
+ params?: PaginationParams,
+ ) {
+ const url = new URL(`private/templates`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForTemplateSummaryResponse());
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-templates-$TEMPLATE_ID
+ */
+ async getTemplateDetails(token: AccessToken | undefined, templateId: string) {
+ const url = new URL(`private/templates/${templateId}`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForTemplateDetails());
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCE]-private-templates-$TEMPLATE_ID
+ */
+ async deleteTemplate(token: AccessToken | undefined, templateId: string) {
+ const url = new URL(`private/templates/${templateId}`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "DELETE",
+ headers,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.DELETE_TEMPLATE,
+ );
+ return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-templates-$TEMPLATE_ID
+ */
+ async useTemplateGetInfo(templateId: string) {
+ const url = new URL(`templates/${templateId}`, this.baseUrl);
+
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForWalletTemplateDetails());
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCES]-templates-$TEMPLATE_ID
+ */
+ async useTemplateCreateOrder(
+ templateId: string,
+ body: TalerMerchantApi.UsingTemplateDetails,
+ ) {
+ const url = new URL(`templates/${templateId}`, this.baseUrl);
+
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body,
+ });
+
+ return this.procesOrderCreationResponse(resp);
+ }
+
+ //
+ // Webhooks
+ //
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCES]-private-webhooks
+ */
+ async addWebhook(
+ token: AccessToken | undefined,
+ body: TalerMerchantApi.WebhookAddDetails,
+ ) {
+ const url = new URL(`private/webhooks`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body,
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.CREATE_WEBHOOK,
+ );
+ return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#patch-[-instances-$INSTANCES]-private-webhooks-$WEBHOOK_ID
+ */
+ async updateWebhook(
+ token: AccessToken | undefined,
+ webhookId: string,
+ body: TalerMerchantApi.WebhookPatchDetails,
+ ) {
+ const url = new URL(`private/webhooks/${webhookId}`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "PATCH",
+ body,
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.UPDATE_WEBHOOK,
+ );
+ return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCES]-private-webhooks
+ */
+ async listWebhooks(
+ token: AccessToken | undefined,
+ params?: PaginationParams,
+ ) {
+ const url = new URL(`private/webhooks`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForWebhookSummaryResponse());
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCES]-private-webhooks-$WEBHOOK_ID
+ */
+ async getWebhookDetails(token: AccessToken | undefined, webhookId: string) {
+ const url = new URL(`private/webhooks/${webhookId}`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.NoContent:
+ return opSuccessFromHttp(resp, codecForWebhookDetails());
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCES]-private-webhooks-$WEBHOOK_ID
+ */
+ async deleteWebhook(token: AccessToken | undefined, webhookId: string) {
+ const url = new URL(`private/webhooks/${webhookId}`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "DELETE",
+ headers,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.DELETE_WEBHOOK,
+ );
+ return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ //
+ // token families
+ //
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCES]-private-tokenfamilies
+ */
+ async createTokenFamily(
+ token: AccessToken | undefined,
+ body: TalerMerchantApi.TokenFamilyCreateRequest,
+ ) {
+ const url = new URL(`private/tokenfamilies`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body,
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.CREATE_TOKENFAMILY,
+ );
+ return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#patch-[-instances-$INSTANCES]-private-tokenfamilies-$TOKEN_FAMILY_SLUG
+ */
+ async updateTokenFamily(
+ token: AccessToken | undefined,
+ tokenSlug: string,
+ body: TalerMerchantApi.TokenFamilyUpdateRequest,
+ ) {
+ const url = new URL(`private/tokenfamilies/${tokenSlug}`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body,
+ headers,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.UPDATE_TOKENFAMILY,
+ );
+ return opSuccessFromHttp(resp, codecForTokenFamilyDetails());
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCES]-private-tokenfamilies
+ */
+ async listTokenFamilies(
+ token: AccessToken | undefined,
+ params?: PaginationParams,
+ ) {
+ const url = new URL(`private/tokenfamilies`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForTokenFamiliesList());
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCES]-private-tokenfamilies-$TOKEN_FAMILY_SLUG
+ */
+ async getTokenFamilyDetails(
+ token: AccessToken | undefined,
+ tokenSlug: string,
+ ) {
+ const url = new URL(`private/tokenfamilies/${tokenSlug}`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForTokenFamilyDetails());
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCES]-private-tokenfamilies-$TOKEN_FAMILY_SLUG
+ */
+ async deleteTokenFamily(token: AccessToken | undefined, tokenSlug: string) {
+ const url = new URL(`private/tokenfamilies/${tokenSlug}`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "DELETE",
+ headers,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.DELETE_TOKENFAMILY,
+ );
+ return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * Get the auth api against the current instance
+ *
+ * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-token
+ * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCE]-private-token
+ */
+ getAuthenticationAPI(): URL {
+ return new URL(`private/`, this.baseUrl);
+ }
+}
+
+export type TalerMerchantManagementResultByMethod<
+ prop extends keyof TalerMerchantManagementHttpClient,
+> = ResultByMethod<TalerMerchantManagementHttpClient, prop>;
+export type TalerMerchantManagementErrorsByMethod<
+ prop extends keyof TalerMerchantManagementHttpClient,
+> = FailCasesByMethod<TalerMerchantManagementHttpClient, prop>;
+
+export class TalerMerchantManagementHttpClient extends TalerMerchantInstanceHttpClient {
+ readonly cacheManagementEvictor: CacheEvictor<
+ TalerMerchantInstanceCacheEviction | TalerMerchantManagementCacheEviction
+ >;
+ constructor(
+ readonly baseUrl: string,
+ httpClient?: HttpRequestLibrary,
+ // cacheManagementEvictor?: CacheEvictor<TalerMerchantManagementCacheEviction>,
+ cacheEvictor?: CacheEvictor<
+ TalerMerchantInstanceCacheEviction | TalerMerchantManagementCacheEviction
+ >,
+ ) {
+ super(baseUrl, httpClient, cacheEvictor);
+ this.cacheManagementEvictor = cacheEvictor ?? nullEvictor;
+ }
+
+ getSubInstanceAPI(instanceId: string) {
+ return new URL(`instances/${instanceId}/`, this.baseUrl);
+ }
+
+ //
+ // Instance Management
+ //
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#post--management-instances
+ */
+ async createInstance(
+ token: AccessToken | undefined,
+ body: TalerMerchantApi.InstanceConfigurationMessage,
+ ) {
+ const url = new URL(`management/instances`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body,
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.NoContent: {
+ this.cacheManagementEvictor.notifySuccess(
+ TalerMerchantManagementCacheEviction.CREATE_INSTANCE,
+ );
+ return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#post--management-instances-$INSTANCE-auth
+ */
+ async updateInstanceAuthentication(
+ token: AccessToken | undefined,
+ instanceId: string,
+ body: TalerMerchantApi.InstanceAuthConfigurationMessage,
+ ) {
+ const url = new URL(
+ `management/instances/${instanceId}/auth`,
+ this.baseUrl,
+ );
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ body,
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.NoContent:
+ return opEmptySuccess(resp);
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#patch--management-instances-$INSTANCE
+ */
+ async updateInstance(
+ token: AccessToken | undefined,
+ instanceId: string,
+ body: TalerMerchantApi.InstanceReconfigurationMessage,
+ ) {
+ const url = new URL(`management/instances/${instanceId}`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "PATCH",
+ body,
+ headers,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.NoContent: {
+ this.cacheManagementEvictor.notifySuccess(
+ TalerMerchantManagementCacheEviction.UPDATE_INSTANCE,
+ );
+ return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#get--management-instances
+ */
+ async listInstances(
+ token: AccessToken | undefined,
+ params?: PaginationParams,
+ ) {
+ const url = new URL(`management/instances`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForInstancesResponse());
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#get--management-instances-$INSTANCE
+ *
+ */
+ async getInstanceDetails(token: AccessToken | undefined, instanceId: string) {
+ const url = new URL(`management/instances/${instanceId}`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(resp, codecForQueryInstancesResponse());
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#delete--management-instances-$INSTANCE
+ */
+ async deleteInstance(
+ token: AccessToken | undefined,
+ instanceId: string,
+ params: { purge?: boolean } = {},
+ ) {
+ const url = new URL(`management/instances/${instanceId}`, this.baseUrl);
+
+ if (params.purge !== undefined) {
+ url.searchParams.set("purge", params.purge ? "YES" : "NO");
+ }
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "DELETE",
+ headers,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.NoContent: {
+ this.cacheManagementEvictor.notifySuccess(
+ TalerMerchantManagementCacheEviction.DELETE_INSTANCE,
+ );
+ return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+
+ /**
+ * https://docs.taler.net/core/api-merchant.html#get--management-instances-$INSTANCE-kyc
+ */
+ async getIntanceKycStatus(
+ token: AccessToken | undefined,
+ instanceId: string,
+ params: TalerMerchantApi.GetKycStatusRequestParams,
+ ) {
+ const url = new URL(`management/instances/${instanceId}/kyc`, this.baseUrl);
+
+ if (params.wireHash) {
+ url.searchParams.set("h_wire", params.wireHash);
+ }
+ if (params.exchangeURL) {
+ url.searchParams.set("exchange_url", params.exchangeURL);
+ }
+ if (params.timeout) {
+ url.searchParams.set("timeout_ms", String(params.timeout));
+ }
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Accepted:
+ return opSuccessFromHttp(resp, codecForAccountKycRedirects());
+ case HttpStatusCode.NoContent:
+ return opEmptySuccess(resp);
+ case HttpStatusCode.NotFound:
+ return opEmptySuccess(resp);
+ case HttpStatusCode.Unauthorized: // FIXME: missing in docs
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.BadGateway:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.ServiceUnavailable:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownFailure(resp, await readTalerErrorResponse(resp));
+ }
+ }
+}
diff --git a/packages/taler-util/src/http-client/officer-account.ts b/packages/taler-util/src/http-client/officer-account.ts
new file mode 100644
index 000000000..2c1426be2
--- /dev/null
+++ b/packages/taler-util/src/http-client/officer-account.ts
@@ -0,0 +1,105 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 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 {
+ EncryptionNonce,
+ LockedAccount,
+ OfficerAccount,
+ OfficerId,
+ SigningKey,
+ createEddsaKeyPair,
+ decodeCrock,
+ decryptWithDerivedKey,
+ eddsaGetPublic,
+ encodeCrock,
+ encryptWithDerivedKey,
+ getRandomBytesF,
+ kdf,
+ stringToBytes,
+} from "@gnu-taler/taler-util";
+
+/**
+ * Restore previous session and unlock account with password
+ *
+ * @param salt string from which crypto params will be derived
+ * @param key secured private key
+ * @param password password for the private key
+ * @returns
+ */
+export async function unlockOfficerAccount(
+ account: LockedAccount,
+ password: string,
+): Promise<OfficerAccount> {
+ const rawKey = decodeCrock(account);
+ const rawPassword = stringToBytes(password);
+
+ const signingKey = (await decryptWithDerivedKey(
+ rawKey,
+ rawPassword,
+ password,
+ ).catch((e: Error) => {
+ throw new UnwrapKeyError(e.message);
+ })) as SigningKey;
+
+ const publicKey = eddsaGetPublic(signingKey);
+
+ const accountId = encodeCrock(publicKey) as OfficerId;
+
+ return { id: accountId, signingKey };
+}
+
+/**
+ * Create new account (secured private key)
+ * secured with the given password
+ *
+ * @param sessionId
+ * @param password
+ * @returns
+ */
+export async function createNewOfficerAccount(
+ password: string,
+ extraNonce: EncryptionNonce,
+): Promise<OfficerAccount & { safe: LockedAccount }> {
+ const { eddsaPriv, eddsaPub } = createEddsaKeyPair();
+
+ const key = stringToBytes(password);
+
+ const localRnd = getRandomBytesF(24);
+ const mergedRnd: EncryptionNonce = extraNonce
+ ? kdf(24, stringToBytes("aml-officer"), extraNonce, localRnd)
+ : localRnd;
+
+ const protectedPrivKey = await encryptWithDerivedKey(
+ mergedRnd,
+ key,
+ eddsaPriv,
+ password,
+ );
+
+ const signingKey = eddsaPriv as SigningKey;
+ const accountId = encodeCrock(eddsaPub) as OfficerId;
+ const safe = encodeCrock(protectedPrivKey) as LockedAccount;
+
+ return { id: accountId, signingKey, safe };
+}
+
+export class UnwrapKeyError extends Error {
+ public cause: string;
+ constructor(cause: string) {
+ super(`Recovering private key failed on: ${cause}`);
+ this.cause = cause;
+ }
+}
diff --git a/packages/taler-util/src/http-client/types.ts b/packages/taler-util/src/http-client/types.ts
new file mode 100644
index 000000000..2613bd663
--- /dev/null
+++ b/packages/taler-util/src/http-client/types.ts
@@ -0,0 +1,5461 @@
+import { codecForAmountString } from "../amounts.js";
+import {
+ Codec,
+ buildCodecForObject,
+ buildCodecForUnion,
+ codecForAny,
+ codecForBoolean,
+ codecForConstNumber,
+ codecForConstString,
+ codecForEither,
+ codecForList,
+ codecForMap,
+ codecForNumber,
+ codecForString,
+ codecOptional,
+ codecOptionalDefault,
+} from "../codec.js";
+import { PaytoString, codecForPaytoString } from "../payto.js";
+import {
+ AmountString,
+ ExchangeWireAccount,
+ InternationalizedString,
+ codecForExchangeWireAccount,
+ codecForInternationalizedString,
+ codecForLocation,
+} from "../taler-types.js";
+import { TalerUriString, codecForTalerUriString } from "../taleruri.js";
+import {
+ AbsoluteTime,
+ TalerProtocolDuration,
+ TalerProtocolTimestamp,
+ codecForDuration,
+ codecForTimestamp,
+} from "../time.js";
+
+export type UserAndPassword = {
+ username: string;
+ password: string;
+};
+
+export type UserAndToken = {
+ username: string;
+ token: AccessToken;
+};
+
+declare const opaque_OfficerAccount: unique symbol;
+export type LockedAccount = string & { [opaque_OfficerAccount]: true };
+
+declare const opaque_OfficerId: unique symbol;
+export type OfficerId = string & { [opaque_OfficerId]: true };
+
+declare const opaque_OfficerSigningKey: unique symbol;
+export type SigningKey = Uint8Array & { [opaque_OfficerSigningKey]: true };
+
+export interface OfficerAccount {
+ id: OfficerId;
+ signingKey: SigningKey;
+}
+
+export type PaginationParams = {
+ /**
+ * row identifier as the starting point of the query
+ */
+ offset?: string;
+ /**
+ * max number of element in the result response
+ * always greater than 0
+ */
+ limit?: number;
+ /**
+ * order
+ */
+ order?: "asc" | "dec";
+};
+
+export type LongPollParams = {
+ /**
+ * milliseconds the server should wait for at least one result to be shown
+ */
+ timeoutMs?: number;
+};
+///
+/// HASH
+///
+
+// 64-byte hash code.
+type HashCode = string;
+
+type PaytoHash = string;
+
+type AmlOfficerPublicKeyP = string;
+
+// 32-byte hash code.
+type ShortHashCode = string;
+
+// 16-byte salt.
+type WireSalt = string;
+
+type SHA256HashCode = ShortHashCode;
+
+type SHA512HashCode = HashCode;
+
+// 32-byte nonce value, must only be used once.
+type CSNonce = string;
+
+// 32-byte nonce value, must only be used once.
+type RefreshMasterSeed = string;
+
+// 32-byte value representing a point on Curve25519.
+type Cs25519Point = string;
+
+// 32-byte value representing a scalar multiplier
+// for scalar operations on points on Curve25519.
+type Cs25519Scalar = string;
+
+///
+/// KEYS
+///
+
+// 16-byte access token used to authorize access.
+type ClaimToken = string;
+
+// EdDSA and ECDHE public keys always point on Curve25519
+// and represented using the standard 256 bits Ed25519 compact format,
+// converted to Crockford Base32.
+type EddsaPublicKey = string;
+
+// EdDSA and ECDHE public keys always point on Curve25519
+// and represented using the standard 256 bits Ed25519 compact format,
+// converted to Crockford Base32.
+type EddsaPrivateKey = string;
+
+// Edx25519 public keys are points on Curve25519 and represented using the
+// standard 256 bits Ed25519 compact format converted to Crockford
+// Base32.
+type Edx25519PublicKey = string;
+
+// Edx25519 private keys are always points on Curve25519
+// and represented using the standard 256 bits Ed25519 compact format,
+// converted to Crockford Base32.
+type Edx25519PrivateKey = string;
+
+// EdDSA and ECDHE public keys always point on Curve25519
+// and represented using the standard 256 bits Ed25519 compact format,
+// converted to Crockford Base32.
+type EcdhePublicKey = string;
+
+// Point on Curve25519 represented using the standard 256 bits Ed25519 compact format,
+// converted to Crockford Base32.
+type CsRPublic = string;
+
+// EdDSA and ECDHE public keys always point on Curve25519
+// and represented using the standard 256 bits Ed25519 compact format,
+// converted to Crockford Base32.
+type EcdhePrivateKey = string;
+
+type CoinPublicKey = EddsaPublicKey;
+
+// RSA public key converted to Crockford Base32.
+type RsaPublicKey = string;
+
+type Integer = number;
+
+type WireTransferIdentifierRawP = string;
+// Subset of numbers: Integers in the
+// inclusive range 0 .. (2^53 - 1).
+type SafeUint64 = number;
+
+// The string must be a data URL according to RFC 2397
+// with explicit mediatype and base64 parameters.
+//
+// data:<mediatype>;base64,<data>
+//
+// Supported mediatypes are image/jpeg and image/png.
+// Invalid strings will be rejected by the wallet.
+type ImageDataUrl = string;
+
+type WadId = string;
+
+type Timestamp = TalerProtocolTimestamp;
+
+type RelativeTime = TalerProtocolDuration;
+
+export interface LoginToken {
+ token: AccessToken;
+ expiration: Timestamp;
+}
+
+declare const __ac_token: unique symbol;
+/**
+ * Use `createAccessToken(string)` function to build one.
+ */
+export type AccessToken = string & {
+ [__ac_token]: true;
+};
+
+/**
+ * Create a rfc8959 access token.
+ * Adds secret-token: prefix if there is none.
+ * Encode the token with rfc7230 to send in a http header.
+ *
+ * @param token
+ * @returns
+ */
+export function createRFC8959AccessTokenEncoded(token: string): AccessToken {
+ return (
+ token.startsWith("secret-token:")
+ ? token
+ : `secret-token:${encodeURIComponent(token)}`
+ ) as AccessToken;
+}
+
+/**
+ * Create a rfc8959 access token.
+ * Adds secret-token: prefix if there is none.
+ *
+ * @param token
+ * @returns
+ */
+export function createRFC8959AccessTokenPlain(token: string): AccessToken {
+ return (
+ token.startsWith("secret-token:") ? token : `secret-token:${token}`
+ ) as AccessToken;
+}
+
+/**
+ * Convert string to access token.
+ *
+ * @param clientSecret
+ * @returns
+ */
+export function createClientSecretAccessToken(
+ clientSecret: string,
+): AccessToken {
+ return clientSecret as AccessToken;
+}
+
+declare const __officer_signature: unique symbol;
+export type OfficerSignature = string & {
+ [__officer_signature]: true;
+};
+
+export namespace TalerAuthentication {
+ export interface TokenRequest {
+ // Service-defined scope for the token.
+ // Typical scopes would be "readonly" or "readwrite".
+ scope: string;
+
+ // Server may impose its own upper bound
+ // on the token validity duration
+ duration?: RelativeTime;
+
+ // Is the token refreshable into a new token during its
+ // validity?
+ // Refreshable tokens effectively provide indefinite
+ // access if they are refreshed in time.
+ refreshable?: boolean;
+ }
+
+ export interface TokenSuccessResponse {
+ // Expiration determined by the server.
+ // Can be based on the token_duration
+ // from the request, but ultimately the
+ // server decides the expiration.
+ expiration: Timestamp;
+
+ // Opque access token.
+ access_token: AccessToken;
+ }
+ export interface TokenSuccessResponseMerchant {
+ // Expiration determined by the server.
+ // Can be based on the token_duration
+ // from the request, but ultimately the
+ // server decides the expiration.
+ expiration: Timestamp;
+
+ // Opque access token.
+ token: AccessToken;
+ }
+}
+
+// DD51 https://docs.taler.net/design-documents/051-fractional-digits.html
+export interface CurrencySpecification {
+ // Name of the currency.
+ name: string;
+
+ // how many digits the user may enter after the decimal_separator
+ num_fractional_input_digits: Integer;
+
+ // Number of fractional digits to render in normal font and size.
+ num_fractional_normal_digits: Integer;
+
+ // Number of fractional digits to render always, if needed by
+ // padding with zeros.
+ num_fractional_trailing_zero_digits: Integer;
+
+ // map of powers of 10 to alternative currency names / symbols, must
+ // always have an entry under "0" that defines the base name,
+ // e.g. "0 => €" or "3 => k€". For BTC, would be "0 => BTC, -3 => mBTC".
+ // Communicates the currency symbol to be used.
+ alt_unit_names: { [log10: string]: string };
+}
+
+//FIXME: implement this codec
+export const codecForAccessToken = codecForString as () => Codec<AccessToken>;
+export const codecForTokenSuccessResponse =
+ (): Codec<TalerAuthentication.TokenSuccessResponse> =>
+ buildCodecForObject<TalerAuthentication.TokenSuccessResponse>()
+ .property("access_token", codecForAccessToken())
+ .property("expiration", codecForTimestamp)
+ .build("TalerAuthentication.TokenSuccessResponse");
+
+export const codecForTokenSuccessResponseMerchant =
+ (): Codec<TalerAuthentication.TokenSuccessResponseMerchant> =>
+ buildCodecForObject<TalerAuthentication.TokenSuccessResponseMerchant>()
+ .property("token", codecForAccessToken())
+ .property("expiration", codecForTimestamp)
+ .build("TalerAuthentication.TokenSuccessResponseMerchant");
+
+export const codecForCurrencySpecificiation =
+ (): Codec<CurrencySpecification> =>
+ buildCodecForObject<CurrencySpecification>()
+ .property("name", codecForString())
+ .property("num_fractional_input_digits", codecForNumber())
+ .property("num_fractional_normal_digits", codecForNumber())
+ .property("num_fractional_trailing_zero_digits", codecForNumber())
+ .property("alt_unit_names", codecForMap(codecForString()))
+ .build("CurrencySpecification");
+
+export const codecForIntegrationBankConfig =
+ (): Codec<TalerCorebankApi.IntegrationConfig> =>
+ buildCodecForObject<TalerCorebankApi.IntegrationConfig>()
+ .property("name", codecForConstString("taler-bank-integration"))
+ .property("version", codecForString())
+ .property("currency", codecForString())
+ .property("currency_specification", codecForCurrencySpecificiation())
+ .build("TalerCorebankApi.IntegrationConfig");
+
+export const codecForCoreBankConfig = (): Codec<TalerCorebankApi.Config> =>
+ buildCodecForObject<TalerCorebankApi.Config>()
+ .property("name", codecForConstString("libeufin-bank"))
+ .property("version", codecForString())
+ .property("bank_name", codecForString())
+ .property("base_url", codecOptional(codecForString()))
+ .property("allow_conversion", codecForBoolean())
+ .property("allow_registrations", codecForBoolean())
+ .property("allow_deletions", codecForBoolean())
+ .property("allow_edit_name", codecForBoolean())
+ .property("allow_edit_cashout_payto_uri", codecForBoolean())
+ .property("default_debit_threshold", codecForAmountString())
+ .property("currency", codecForString())
+ .property("currency_specification", codecForCurrencySpecificiation())
+ .property(
+ "supported_tan_channels",
+ codecForList(
+ codecForEither(
+ codecForConstString(TalerCorebankApi.TanChannel.SMS),
+ codecForConstString(TalerCorebankApi.TanChannel.EMAIL),
+ ),
+ ),
+ )
+ .property("wire_type", codecOptionalDefault(codecForString(), "iban"))
+ .build("TalerCorebankApi.Config");
+
+//FIXME: implement this codec
+export const codecForURN = codecForString;
+
+export const codecForExchangeConfigInfo =
+ (): Codec<TalerMerchantApi.ExchangeConfigInfo> =>
+ buildCodecForObject<TalerMerchantApi.ExchangeConfigInfo>()
+ .property("base_url", codecForString())
+ .property("currency", codecForString())
+ .property("master_pub", codecForString())
+ .build("TalerMerchantApi.ExchangeConfigInfo");
+
+export const codecForMerchantConfig =
+ (): Codec<TalerMerchantApi.VersionResponse> =>
+ buildCodecForObject<TalerMerchantApi.VersionResponse>()
+ .property("name", codecForConstString("taler-merchant"))
+ .property("currency", codecForString())
+ .property("version", codecForString())
+ .property("currencies", codecForMap(codecForCurrencySpecificiation()))
+ .property("exchanges", codecForList(codecForExchangeConfigInfo()))
+ .build("TalerMerchantApi.VersionResponse");
+
+export const codecForClaimResponse =
+ (): Codec<TalerMerchantApi.ClaimResponse> =>
+ buildCodecForObject<TalerMerchantApi.ClaimResponse>()
+ .property("contract_terms", codecForContractTerms())
+ .property("sig", codecForString())
+ .build("TalerMerchantApi.ClaimResponse");
+
+export const codecForPaymentResponse =
+ (): Codec<TalerMerchantApi.PaymentResponse> =>
+ buildCodecForObject<TalerMerchantApi.PaymentResponse>()
+ .property("pos_confirmation", codecOptional(codecForString()))
+ .property("sig", codecForString())
+ .build("TalerMerchantApi.PaymentResponse");
+
+export const codecForStatusPaid = (): Codec<TalerMerchantApi.StatusPaid> =>
+ buildCodecForObject<TalerMerchantApi.StatusPaid>()
+ .property("refund_amount", codecForAmountString())
+ .property("refund_pending", codecForBoolean())
+ .property("refund_taken", codecForAmountString())
+ .property("refunded", codecForBoolean())
+ .property("type", codecForConstString("paid"))
+ .build("TalerMerchantApi.StatusPaid");
+
+export const codecForStatusGoto =
+ (): Codec<TalerMerchantApi.StatusGotoResponse> =>
+ buildCodecForObject<TalerMerchantApi.StatusGotoResponse>()
+ .property("public_reorder_url", codecForURL())
+ .property("type", codecForConstString("goto"))
+ .build("TalerMerchantApi.StatusGotoResponse");
+
+export const codecForStatusStatusUnpaid =
+ (): Codec<TalerMerchantApi.StatusUnpaidResponse> =>
+ buildCodecForObject<TalerMerchantApi.StatusUnpaidResponse>()
+ .property("type", codecForConstString("unpaid"))
+ .property("already_paid_order_id", codecOptional(codecForString()))
+ .property("fulfillment_url", codecOptional(codecForString()))
+ .property("taler_pay_uri", codecForTalerUriString())
+ .build("TalerMerchantApi.PaymentResponse");
+
+export const codecForPaidRefundStatusResponse =
+ (): Codec<TalerMerchantApi.PaidRefundStatusResponse> =>
+ buildCodecForObject<TalerMerchantApi.PaidRefundStatusResponse>()
+ .property("pos_confirmation", codecOptional(codecForString()))
+ .property("refunded", codecForBoolean())
+ .build("TalerMerchantApi.PaidRefundStatusResponse");
+
+export const codecForMerchantAbortPayRefundSuccessStatus =
+ (): Codec<TalerMerchantApi.MerchantAbortPayRefundSuccessStatus> =>
+ buildCodecForObject<TalerMerchantApi.MerchantAbortPayRefundSuccessStatus>()
+ .property("exchange_pub", codecForString())
+ .property("exchange_sig", codecForString())
+ .property("exchange_status", codecForConstNumber(200))
+ .property("type", codecForConstString("success"))
+ .build("TalerMerchantApi.MerchantAbortPayRefundSuccessStatus");
+
+export const codecForMerchantAbortPayRefundFailureStatus =
+ (): Codec<TalerMerchantApi.MerchantAbortPayRefundFailureStatus> =>
+ buildCodecForObject<TalerMerchantApi.MerchantAbortPayRefundFailureStatus>()
+ .property("exchange_code", codecForNumber())
+ .property("exchange_reply", codecForAny())
+ .property("exchange_status", codecForNumber())
+ .property("type", codecForConstString("failure"))
+ .build("TalerMerchantApi.MerchantAbortPayRefundFailureStatus");
+
+export const codecForMerchantAbortPayRefundStatus =
+ (): Codec<TalerMerchantApi.MerchantAbortPayRefundStatus> =>
+ buildCodecForUnion<TalerMerchantApi.MerchantAbortPayRefundStatus>()
+ .discriminateOn("type")
+ .alternative("success", codecForMerchantAbortPayRefundSuccessStatus())
+ .alternative("failure", codecForMerchantAbortPayRefundFailureStatus())
+ .build("TalerMerchantApi.MerchantAbortPayRefundStatus");
+
+export const codecForAbortResponse =
+ (): Codec<TalerMerchantApi.AbortResponse> =>
+ buildCodecForObject<TalerMerchantApi.AbortResponse>()
+ .property("refunds", codecForList(codecForMerchantAbortPayRefundStatus()))
+ .build("TalerMerchantApi.AbortResponse");
+
+export const codecForWalletRefundResponse =
+ (): Codec<TalerMerchantApi.WalletRefundResponse> =>
+ buildCodecForObject<TalerMerchantApi.WalletRefundResponse>()
+ .property("merchant_pub", codecForString())
+ .property("refund_amount", codecForAmountString())
+ .property("refunds", codecForList(codecForMerchantCoinRefundStatus()))
+ .build("TalerMerchantApi.AbortResponse");
+
+export const codecForMerchantCoinRefundSuccessStatus =
+ (): Codec<TalerMerchantApi.MerchantCoinRefundSuccessStatus> =>
+ buildCodecForObject<TalerMerchantApi.MerchantCoinRefundSuccessStatus>()
+ .property("type", codecForConstString("success"))
+ .property("coin_pub", codecForString())
+ .property("exchange_status", codecForConstNumber(200))
+ .property("exchange_sig", codecForString())
+ .property("rtransaction_id", codecForNumber())
+ .property("refund_amount", codecForAmountString())
+ .property("exchange_pub", codecForString())
+ .property("execution_time", codecForTimestamp)
+ .build("TalerMerchantApi.MerchantCoinRefundSuccessStatus");
+
+export const codecForMerchantCoinRefundFailureStatus =
+ (): Codec<TalerMerchantApi.MerchantCoinRefundFailureStatus> =>
+ buildCodecForObject<TalerMerchantApi.MerchantCoinRefundFailureStatus>()
+ .property("type", codecForConstString("failure"))
+ .property("coin_pub", codecForString())
+ .property("exchange_status", codecForNumber())
+ .property("rtransaction_id", codecForNumber())
+ .property("refund_amount", codecForAmountString())
+ .property("exchange_code", codecOptional(codecForNumber()))
+ .property("exchange_reply", codecOptional(codecForAny()))
+ .property("execution_time", codecForTimestamp)
+ .build("TalerMerchantApi.MerchantCoinRefundFailureStatus");
+
+export const codecForMerchantCoinRefundStatus =
+ (): Codec<TalerMerchantApi.MerchantCoinRefundStatus> =>
+ buildCodecForUnion<TalerMerchantApi.MerchantCoinRefundStatus>()
+ .discriminateOn("type")
+ .alternative("success", codecForMerchantCoinRefundSuccessStatus())
+ .alternative("failure", codecForMerchantCoinRefundFailureStatus())
+ .build("TalerMerchantApi.MerchantCoinRefundStatus");
+
+export const codecForQueryInstancesResponse =
+ (): Codec<TalerMerchantApi.QueryInstancesResponse> =>
+ buildCodecForObject<TalerMerchantApi.QueryInstancesResponse>()
+ .property("name", codecForString())
+ .property("user_type", codecForString())
+ .property("email", codecOptional(codecForString()))
+ .property("website", codecOptional(codecForString()))
+ .property("logo", codecOptional(codecForString()))
+ .property("merchant_pub", codecForString())
+ .property("address", codecForLocation())
+ .property("jurisdiction", codecForLocation())
+ .property("use_stefan", codecForBoolean())
+ .property("default_wire_transfer_delay", codecForDuration)
+ .property("default_pay_delay", codecForDuration)
+ .property(
+ "auth",
+ buildCodecForObject<{
+ method: "external" | "token";
+ }>()
+ .property(
+ "method",
+ codecForEither(
+ codecForConstString("token"),
+ codecForConstString("external"),
+ ),
+ )
+ .build("TalerMerchantApi.QueryInstancesResponse.auth"),
+ )
+ .build("TalerMerchantApi.QueryInstancesResponse");
+
+export const codecForAccountKycRedirects =
+ (): Codec<TalerMerchantApi.AccountKycRedirects> =>
+ buildCodecForObject<TalerMerchantApi.AccountKycRedirects>()
+ .property(
+ "pending_kycs",
+ codecForList(codecForMerchantAccountKycRedirect()),
+ )
+ .property("timeout_kycs", codecForList(codecForExchangeKycTimeout()))
+
+ .build("TalerMerchantApi.AccountKycRedirects");
+
+export const codecForMerchantAccountKycRedirect =
+ (): Codec<TalerMerchantApi.MerchantAccountKycRedirect> =>
+ buildCodecForObject<TalerMerchantApi.MerchantAccountKycRedirect>()
+ .property("kyc_url", codecForURL())
+ .property("aml_status", codecForNumber())
+ .property("exchange_url", codecForURL())
+ .property("payto_uri", codecForPaytoString())
+ .build("TalerMerchantApi.MerchantAccountKycRedirect");
+
+export const codecForExchangeKycTimeout =
+ (): Codec<TalerMerchantApi.ExchangeKycTimeout> =>
+ buildCodecForObject<TalerMerchantApi.ExchangeKycTimeout>()
+ .property("exchange_url", codecForURL())
+ .property("exchange_code", codecForNumber())
+ .property("exchange_http_status", codecForNumber())
+ .build("TalerMerchantApi.ExchangeKycTimeout");
+
+export const codecForAccountAddResponse =
+ (): Codec<TalerMerchantApi.AccountAddResponse> =>
+ buildCodecForObject<TalerMerchantApi.AccountAddResponse>()
+ .property("h_wire", codecForString())
+ .property("salt", codecForString())
+ .build("TalerMerchantApi.AccountAddResponse");
+
+export const codecForAccountsSummaryResponse =
+ (): Codec<TalerMerchantApi.AccountsSummaryResponse> =>
+ buildCodecForObject<TalerMerchantApi.AccountsSummaryResponse>()
+ .property("accounts", codecForList(codecForBankAccountSummaryEntry()))
+ .build("TalerMerchantApi.AccountsSummaryResponse");
+
+export const codecForBankAccountSummaryEntry =
+ (): Codec<TalerMerchantApi.BankAccountSummaryEntry> =>
+ buildCodecForObject<TalerMerchantApi.BankAccountSummaryEntry>()
+ .property("payto_uri", codecForPaytoString())
+ .property("h_wire", codecForString())
+ .build("TalerMerchantApi.BankAccountSummaryEntry");
+
+export const codecForBankAccountEntry =
+ (): Codec<TalerMerchantApi.BankAccountEntry> =>
+ buildCodecForObject<TalerMerchantApi.BankAccountEntry>()
+ .property("payto_uri", codecForPaytoString())
+ .property("h_wire", codecForString())
+ .property("salt", codecForString())
+ .property("credit_facade_url", codecOptional(codecForURL()))
+ .property("active", codecOptional(codecForBoolean()))
+ .build("TalerMerchantApi.BankAccountEntry");
+
+export const codecForInventorySummaryResponse =
+ (): Codec<TalerMerchantApi.InventorySummaryResponse> =>
+ buildCodecForObject<TalerMerchantApi.InventorySummaryResponse>()
+ .property("products", codecForList(codecForInventoryEntry()))
+ .build("TalerMerchantApi.InventorySummaryResponse");
+
+export const codecForInventoryEntry =
+ (): Codec<TalerMerchantApi.InventoryEntry> =>
+ buildCodecForObject<TalerMerchantApi.InventoryEntry>()
+ .property("product_id", codecForString())
+ .property("product_serial", codecForNumber())
+ .build("TalerMerchantApi.InventoryEntry");
+
+export const codecForMerchantPosProductDetail =
+ (): Codec<TalerMerchantApi.MerchantPosProductDetail> =>
+ buildCodecForObject<TalerMerchantApi.MerchantPosProductDetail>()
+ .property("product_serial", codecForNumber())
+ .property("product_id", codecOptional(codecForString()))
+ .property("categories", codecForList(codecForNumber()))
+ .property("description", codecForString())
+ .property("description_i18n", codecForInternationalizedString())
+ .property("unit", codecForString())
+ .property("price", codecForAmountString())
+ .property("image", codecForString())
+ .property("taxes", codecOptional(codecForList(codecForTax())))
+ .property("total_stock", codecForNumber())
+ .property("minimum_age", codecOptional(codecForNumber()))
+ .build("TalerMerchantApi.MerchantPosProductDetail");
+
+export const codecForMerchantCategory =
+ (): Codec<TalerMerchantApi.MerchantCategory> =>
+ buildCodecForObject<TalerMerchantApi.MerchantCategory>()
+ .property("id", codecForNumber())
+ .property("name", codecForString())
+ .property("name_i18n", codecForInternationalizedString())
+ .build("TalerMerchantApi.MerchantCategory");
+
+export const codecForFullInventoryDetailsResponse =
+ (): Codec<TalerMerchantApi.FullInventoryDetailsResponse> =>
+ buildCodecForObject<TalerMerchantApi.FullInventoryDetailsResponse>()
+ .property("categories", codecForList(codecForMerchantCategory()))
+ .property("products", codecForList(codecForMerchantPosProductDetail()))
+ .build("TalerMerchantApi.FullInventoryDetailsResponse");
+
+export const codecForProductDetail =
+ (): Codec<TalerMerchantApi.ProductDetail> =>
+ buildCodecForObject<TalerMerchantApi.ProductDetail>()
+ .property("description", codecForString())
+ .property("description_i18n", codecForInternationalizedString())
+ .property("unit", codecForString())
+ .property("price", codecForAmountString())
+ .property("image", codecForString())
+ .property("taxes", codecOptional(codecForList(codecForTax())))
+ .property("address", codecOptional(codecForLocation()))
+ .property("next_restock", codecOptional(codecForTimestamp))
+ .property("total_stock", codecForNumber())
+ .property("total_sold", codecForNumber())
+ .property("total_lost", codecForNumber())
+ .property("minimum_age", codecOptional(codecForNumber()))
+ .build("TalerMerchantApi.ProductDetail");
+
+export const codecForTax = (): Codec<TalerMerchantApi.Tax> =>
+ buildCodecForObject<TalerMerchantApi.Tax>()
+ .property("name", codecForString())
+ .property("tax", codecForAmountString())
+ .build("TalerMerchantApi.Tax");
+
+export const codecForPostOrderResponse =
+ (): Codec<TalerMerchantApi.PostOrderResponse> =>
+ buildCodecForObject<TalerMerchantApi.PostOrderResponse>()
+ .property("order_id", codecForString())
+ .property("token", codecOptional(codecForString()))
+ .build("TalerMerchantApi.PostOrderResponse");
+
+export const codecForOutOfStockResponse =
+ (): Codec<TalerMerchantApi.OutOfStockResponse> =>
+ buildCodecForObject<TalerMerchantApi.OutOfStockResponse>()
+ .property("product_id", codecForString())
+ .property("available_quantity", codecForNumber())
+ .property("requested_quantity", codecForNumber())
+ .property("restock_expected", codecForTimestamp)
+ .build("TalerMerchantApi.OutOfStockResponse");
+
+export const codecForOrderHistory = (): Codec<TalerMerchantApi.OrderHistory> =>
+ buildCodecForObject<TalerMerchantApi.OrderHistory>()
+ .property("orders", codecForList(codecForOrderHistoryEntry()))
+ .build("TalerMerchantApi.OrderHistory");
+
+export const codecForOrderHistoryEntry =
+ (): Codec<TalerMerchantApi.OrderHistoryEntry> =>
+ buildCodecForObject<TalerMerchantApi.OrderHistoryEntry>()
+ .property("order_id", codecForString())
+ .property("row_id", codecForNumber())
+ .property("timestamp", codecForTimestamp)
+ .property("amount", codecForAmountString())
+ .property("summary", codecForString())
+ .property("refundable", codecForBoolean())
+ .property("paid", codecForBoolean())
+ .build("TalerMerchantApi.OrderHistoryEntry");
+
+export const codecForMerchant = (): Codec<TalerMerchantApi.Merchant> =>
+ buildCodecForObject<TalerMerchantApi.Merchant>()
+ .property("name", codecForString())
+ .property("email", codecOptional(codecForString()))
+ .property("logo", codecOptional(codecForString()))
+ .property("website", codecOptional(codecForString()))
+ .property("address", codecOptional(codecForLocation()))
+ .property("jurisdiction", codecOptional(codecForLocation()))
+ .build("TalerMerchantApi.MerchantInfo");
+
+export const codecForExchange = (): Codec<TalerMerchantApi.Exchange> =>
+ buildCodecForObject<TalerMerchantApi.Exchange>()
+ .property("master_pub", codecForString())
+ .property("priority", codecForNumber())
+ .property("url", codecForString())
+ .build("TalerMerchantApi.Exchange");
+
+export const codecForContractTerms =
+ (): Codec<TalerMerchantApi.ContractTerms> =>
+ buildCodecForObject<TalerMerchantApi.ContractTerms>()
+ .property("order_id", codecForString())
+ .property("fulfillment_url", codecOptional(codecForString()))
+ .property("fulfillment_message", codecOptional(codecForString()))
+ .property(
+ "fulfillment_message_i18n",
+ codecOptional(codecForInternationalizedString()),
+ )
+ .property("merchant_base_url", codecForString())
+ .property("h_wire", codecForString())
+ .property("auto_refund", codecOptional(codecForDuration))
+ .property("wire_method", codecForString())
+ .property("summary", codecForString())
+ .property(
+ "summary_i18n",
+ codecOptional(codecForInternationalizedString()),
+ )
+ .property("nonce", codecForString())
+ .property("amount", codecForAmountString())
+ .property("pay_deadline", codecForTimestamp)
+ .property("refund_deadline", codecForTimestamp)
+ .property("wire_transfer_deadline", codecForTimestamp)
+ .property("timestamp", codecForTimestamp)
+ .property("delivery_location", codecOptional(codecForLocation()))
+ .property("delivery_date", codecOptional(codecForTimestamp))
+ .property("max_fee", codecForAmountString())
+ .property("merchant", codecForMerchant())
+ .property("merchant_pub", codecForString())
+ .property("exchanges", codecForList(codecForExchange()))
+ .property("products", codecForList(codecForProduct()))
+ .property("extra", codecForAny())
+ .build("TalerMerchantApi.ContractTerms");
+
+export const codecForProduct = (): Codec<TalerMerchantApi.Product> =>
+ buildCodecForObject<TalerMerchantApi.Product>()
+ .property("product_id", codecOptional(codecForString()))
+ .property("description", codecForString())
+ .property(
+ "description_i18n",
+ codecOptional(codecForInternationalizedString()),
+ )
+ .property("quantity", codecOptional(codecForNumber()))
+ .property("unit", codecOptional(codecForString()))
+ .property("price", codecOptional(codecForAmountString()))
+ .property("image", codecOptional(codecForString()))
+ .property("taxes", codecOptional(codecForList(codecForTax())))
+ .property("delivery_date", codecOptional(codecForTimestamp))
+ .build("TalerMerchantApi.Product");
+
+export const codecForCheckPaymentPaidResponse =
+ (): Codec<TalerMerchantApi.CheckPaymentPaidResponse> =>
+ buildCodecForObject<TalerMerchantApi.CheckPaymentPaidResponse>()
+ .property("order_status", codecForConstString("paid"))
+ .property("refunded", codecForBoolean())
+ .property("refund_pending", codecForBoolean())
+ .property("wired", codecForBoolean())
+ .property("deposit_total", codecForAmountString())
+ .property("exchange_code", codecForNumber())
+ .property("exchange_http_status", codecForNumber())
+ .property("refund_amount", codecForAmountString())
+ .property("contract_terms", codecForContractTerms())
+ .property("wire_reports", codecForList(codecForTransactionWireReport()))
+ .property("wire_details", codecForList(codecForTransactionWireTransfer()))
+ .property("refund_details", codecForList(codecForRefundDetails()))
+ .property("order_status_url", codecForURL())
+ .build("TalerMerchantApi.CheckPaymentPaidResponse");
+
+export const codecForCheckPaymentUnpaidResponse =
+ (): Codec<TalerMerchantApi.CheckPaymentUnpaidResponse> =>
+ buildCodecForObject<TalerMerchantApi.CheckPaymentUnpaidResponse>()
+ .property("order_status", codecForConstString("unpaid"))
+ .property("taler_pay_uri", codecForTalerUriString())
+ .property("creation_time", codecForTimestamp)
+ .property("summary", codecForString())
+ .property("total_amount", codecForAmountString())
+ .property("already_paid_order_id", codecOptional(codecForString()))
+ .property("already_paid_fulfillment_url", codecOptional(codecForString()))
+ .property("order_status_url", codecForString())
+ .build("TalerMerchantApi.CheckPaymentPaidResponse");
+
+export const codecForCheckPaymentClaimedResponse =
+ (): Codec<TalerMerchantApi.CheckPaymentClaimedResponse> =>
+ buildCodecForObject<TalerMerchantApi.CheckPaymentClaimedResponse>()
+ .property("order_status", codecForConstString("claimed"))
+ .property("contract_terms", codecForContractTerms())
+ .build("TalerMerchantApi.CheckPaymentClaimedResponse");
+
+export const codecForMerchantOrderPrivateStatusResponse =
+ (): Codec<TalerMerchantApi.MerchantOrderStatusResponse> =>
+ buildCodecForUnion<TalerMerchantApi.MerchantOrderStatusResponse>()
+ .discriminateOn("order_status")
+ .alternative("paid", codecForCheckPaymentPaidResponse())
+ .alternative("unpaid", codecForCheckPaymentUnpaidResponse())
+ .alternative("claimed", codecForCheckPaymentClaimedResponse())
+ .build("TalerMerchantApi.MerchantOrderStatusResponse");
+
+export const codecForRefundDetails =
+ (): Codec<TalerMerchantApi.RefundDetails> =>
+ buildCodecForObject<TalerMerchantApi.RefundDetails>()
+ .property("reason", codecForString())
+ .property("pending", codecForBoolean())
+ .property("timestamp", codecForTimestamp)
+ .property("amount", codecForAmountString())
+ .build("TalerMerchantApi.RefundDetails");
+
+export const codecForTransactionWireTransfer =
+ (): Codec<TalerMerchantApi.TransactionWireTransfer> =>
+ buildCodecForObject<TalerMerchantApi.TransactionWireTransfer>()
+ .property("exchange_url", codecForURL())
+ .property("wtid", codecForString())
+ .property("execution_time", codecForTimestamp)
+ .property("amount", codecForAmountString())
+ .property("confirmed", codecForBoolean())
+ .build("TalerMerchantApi.TransactionWireTransfer");
+
+export const codecForTransactionWireReport =
+ (): Codec<TalerMerchantApi.TransactionWireReport> =>
+ buildCodecForObject<TalerMerchantApi.TransactionWireReport>()
+ .property("code", codecForNumber())
+ .property("hint", codecForString())
+ .property("exchange_code", codecForNumber())
+ .property("exchange_http_status", codecForNumber())
+ .property("coin_pub", codecForString())
+ .build("TalerMerchantApi.TransactionWireReport");
+
+export const codecForMerchantRefundResponse =
+ (): Codec<TalerMerchantApi.MerchantRefundResponse> =>
+ buildCodecForObject<TalerMerchantApi.MerchantRefundResponse>()
+ .property("taler_refund_uri", codecForTalerUriString())
+ .property("h_contract", codecForString())
+ .build("TalerMerchantApi.MerchantRefundResponse");
+
+export const codecForTansferList = (): Codec<TalerMerchantApi.TransferList> =>
+ buildCodecForObject<TalerMerchantApi.TransferList>()
+ .property("transfers", codecForList(codecForTransferDetails()))
+ .build("TalerMerchantApi.TransferList");
+
+export const codecForTransferDetails =
+ (): Codec<TalerMerchantApi.TransferDetails> =>
+ buildCodecForObject<TalerMerchantApi.TransferDetails>()
+ .property("credit_amount", codecForAmountString())
+ .property("wtid", codecForString())
+ .property("payto_uri", codecForPaytoString())
+ .property("exchange_url", codecForURL())
+ .property("transfer_serial_id", codecForNumber())
+ .property("execution_time", codecOptional(codecForTimestamp))
+ .property("verified", codecOptional(codecForBoolean()))
+ .property("confirmed", codecOptional(codecForBoolean()))
+ .build("TalerMerchantApi.TransferDetails");
+
+export const codecForOtpDeviceSummaryResponse =
+ (): Codec<TalerMerchantApi.OtpDeviceSummaryResponse> =>
+ buildCodecForObject<TalerMerchantApi.OtpDeviceSummaryResponse>()
+ .property("otp_devices", codecForList(codecForOtpDeviceEntry()))
+ .build("TalerMerchantApi.OtpDeviceSummaryResponse");
+
+export const codecForOtpDeviceEntry =
+ (): Codec<TalerMerchantApi.OtpDeviceEntry> =>
+ buildCodecForObject<TalerMerchantApi.OtpDeviceEntry>()
+ .property("otp_device_id", codecForString())
+ .property("device_description", codecForString())
+ .build("TalerMerchantApi.OtpDeviceEntry");
+
+export const codecForOtpDeviceDetails =
+ (): Codec<TalerMerchantApi.OtpDeviceDetails> =>
+ buildCodecForObject<TalerMerchantApi.OtpDeviceDetails>()
+ .property("device_description", codecForString())
+ .property("otp_algorithm", codecForNumber())
+ .property("otp_ctr", codecOptional(codecForNumber()))
+ .property("otp_timestamp", codecForNumber())
+ .property("otp_code", codecOptional(codecForString()))
+ .build("TalerMerchantApi.OtpDeviceDetails");
+
+export const codecForTemplateSummaryResponse =
+ (): Codec<TalerMerchantApi.TemplateSummaryResponse> =>
+ buildCodecForObject<TalerMerchantApi.TemplateSummaryResponse>()
+ .property("templates", codecForList(codecForTemplateEntry()))
+ .build("TalerMerchantApi.TemplateSummaryResponse");
+
+export const codecForTemplateEntry =
+ (): Codec<TalerMerchantApi.TemplateEntry> =>
+ buildCodecForObject<TalerMerchantApi.TemplateEntry>()
+ .property("template_id", codecForString())
+ .property("template_description", codecForString())
+ .build("TalerMerchantApi.TemplateEntry");
+
+export const codecForTemplateDetails =
+ (): Codec<TalerMerchantApi.TemplateDetails> =>
+ buildCodecForObject<TalerMerchantApi.TemplateDetails>()
+ .property("template_description", codecForString())
+ .property("otp_id", codecOptional(codecForString()))
+ .property("template_contract", codecForTemplateContractDetails())
+ .property("required_currency", codecOptional(codecForString()))
+ .property(
+ "editable_defaults",
+ codecOptional(codecForTemplateContractDetailsDefaults()),
+ )
+ .build("TalerMerchantApi.TemplateDetails");
+
+export const codecForTemplateContractDetails =
+ (): Codec<TalerMerchantApi.TemplateContractDetails> =>
+ buildCodecForObject<TalerMerchantApi.TemplateContractDetails>()
+ .property("summary", codecOptional(codecForString()))
+ .property("currency", codecOptional(codecForString()))
+ .property("amount", codecOptional(codecForAmountString()))
+ .property("minimum_age", codecForNumber())
+ .property("pay_duration", codecForDuration)
+ .build("TalerMerchantApi.TemplateContractDetails");
+
+export const codecForTemplateContractDetailsDefaults =
+ (): Codec<TalerMerchantApi.TemplateContractDetailsDefaults> =>
+ buildCodecForObject<TalerMerchantApi.TemplateContractDetailsDefaults>()
+ .property("summary", codecOptional(codecForString()))
+ .property("currency", codecOptional(codecForString()))
+ .property("amount", codecOptional(codecForAmountString()))
+ .property("minimum_age", codecOptional(codecForNumber()))
+ .build("TalerMerchantApi.TemplateContractDetailsDefaults");
+
+export const codecForWalletTemplateDetails =
+ (): Codec<TalerMerchantApi.WalletTemplateDetails> =>
+ buildCodecForObject<TalerMerchantApi.WalletTemplateDetails>()
+ .property("template_contract", codecForTemplateContractDetails())
+ .property("required_currency", codecOptional(codecForString()))
+ .property(
+ "editable_defaults",
+ codecOptional(codecForTemplateContractDetailsDefaults()),
+ )
+ .build("TalerMerchantApi.WalletTemplateDetails");
+
+export const codecForWebhookSummaryResponse =
+ (): Codec<TalerMerchantApi.WebhookSummaryResponse> =>
+ buildCodecForObject<TalerMerchantApi.WebhookSummaryResponse>()
+ .property("webhooks", codecForList(codecForWebhookEntry()))
+ .build("TalerMerchantApi.WebhookSummaryResponse");
+
+export const codecForWebhookEntry = (): Codec<TalerMerchantApi.WebhookEntry> =>
+ buildCodecForObject<TalerMerchantApi.WebhookEntry>()
+ .property("webhook_id", codecForString())
+ .property("event_type", codecForString())
+ .build("TalerMerchantApi.WebhookEntry");
+
+export const codecForWebhookDetails =
+ (): Codec<TalerMerchantApi.WebhookDetails> =>
+ buildCodecForObject<TalerMerchantApi.WebhookDetails>()
+ .property("event_type", codecForString())
+ .property("url", codecForString())
+ .property("http_method", codecForString())
+ .property("header_template", codecOptional(codecForString()))
+ .property("body_template", codecOptional(codecForString()))
+ .build("TalerMerchantApi.WebhookDetails");
+
+export const codecForTokenFamilyKind =
+ (): Codec<TalerMerchantApi.TokenFamilyKind> =>
+ codecForEither(
+ codecForConstString("discount"),
+ codecForConstString("subscription"),
+ ) as any; //FIXME: create a codecForEnum
+export const codecForTokenFamilyDetails =
+ (): Codec<TalerMerchantApi.TokenFamilyDetails> =>
+ buildCodecForObject<TalerMerchantApi.TokenFamilyDetails>()
+ .property("slug", codecForString())
+ .property("name", codecForString())
+ .property("description", codecForString())
+ .property("description_i18n", codecForInternationalizedString())
+ .property("valid_after", codecForTimestamp)
+ .property("valid_before", codecForTimestamp)
+ .property("duration", codecForDuration)
+ .property("kind", codecForTokenFamilyKind())
+ .property("issued", codecForNumber())
+ .property("redeemed", codecForNumber())
+ .build("TalerMerchantApi.TokenFamilyDetails");
+
+export const codecForTokenFamiliesList =
+ (): Codec<TalerMerchantApi.TokenFamiliesList> =>
+ buildCodecForObject<TalerMerchantApi.TokenFamiliesList>()
+ .property("token_families", codecForList(codecForTokenFamilySummary()))
+ .build("TalerMerchantApi.TokenFamiliesList");
+
+export const codecForTokenFamilySummary =
+ (): Codec<TalerMerchantApi.TokenFamilySummary> =>
+ buildCodecForObject<TalerMerchantApi.TokenFamilySummary>()
+ .property("slug", codecForString())
+ .property("name", codecForString())
+ .property("valid_after", codecForTimestamp)
+ .property("valid_before", codecForTimestamp)
+ .property("kind", codecForTokenFamilyKind())
+ .build("TalerMerchantApi.TokenFamilySummary");
+
+export const codecForInstancesResponse =
+ (): Codec<TalerMerchantApi.InstancesResponse> =>
+ buildCodecForObject<TalerMerchantApi.InstancesResponse>()
+ .property("instances", codecForList(codecForInstance()))
+ .build("TalerMerchantApi.InstancesResponse");
+
+export const codecForInstance = (): Codec<TalerMerchantApi.Instance> =>
+ buildCodecForObject<TalerMerchantApi.Instance>()
+ .property("name", codecForString())
+ .property("user_type", codecForString())
+ .property("website", codecOptional(codecForString()))
+ .property("logo", codecOptional(codecForString()))
+ .property("id", codecForString())
+ .property("merchant_pub", codecForString())
+ .property("payment_targets", codecForList(codecForString()))
+ .property("deleted", codecForBoolean())
+ .build("TalerMerchantApi.Instance");
+
+export const codecForExchangeConfig =
+ (): Codec<TalerExchangeApi.ExchangeVersionResponse> =>
+ buildCodecForObject<TalerExchangeApi.ExchangeVersionResponse>()
+ .property("version", codecForString())
+ .property("name", codecForConstString("taler-exchange"))
+ .property("implementation", codecOptional(codecForURN()))
+ .property("currency", codecForString())
+ .property("currency_specification", codecForCurrencySpecificiation())
+ .property("supported_kyc_requirements", codecForList(codecForString()))
+ .build("TalerExchangeApi.ExchangeVersionResponse");
+
+export const codecForExchangeKeys =
+ (): Codec<TalerExchangeApi.ExchangeKeysResponse> =>
+ buildCodecForObject<TalerExchangeApi.ExchangeKeysResponse>()
+ .property("version", codecForString())
+ .property("base_url", codecForString())
+ .property("currency", codecForString())
+ .build("TalerExchangeApi.ExchangeKeysResponse");
+
+const codecForBalance = (): Codec<TalerCorebankApi.Balance> =>
+ buildCodecForObject<TalerCorebankApi.Balance>()
+ .property("amount", codecForAmountString())
+ .property(
+ "credit_debit_indicator",
+ codecForEither(
+ codecForConstString("credit"),
+ codecForConstString("debit"),
+ ),
+ )
+ .build("TalerCorebankApi.Balance");
+
+const codecForPublicAccount = (): Codec<TalerCorebankApi.PublicAccount> =>
+ buildCodecForObject<TalerCorebankApi.PublicAccount>()
+ .property("username", codecForString())
+ .property("balance", codecForBalance())
+ .property("payto_uri", codecForPaytoString())
+ .property("is_taler_exchange", codecForBoolean())
+ .property("row_id", codecOptional(codecForNumber()))
+ .build("TalerCorebankApi.PublicAccount");
+
+export const codecForPublicAccountsResponse =
+ (): Codec<TalerCorebankApi.PublicAccountsResponse> =>
+ buildCodecForObject<TalerCorebankApi.PublicAccountsResponse>()
+ .property("public_accounts", codecForList(codecForPublicAccount()))
+ .build("TalerCorebankApi.PublicAccountsResponse");
+
+export const codecForAccountMinimalData =
+ (): Codec<TalerCorebankApi.AccountMinimalData> =>
+ buildCodecForObject<TalerCorebankApi.AccountMinimalData>()
+ .property("username", codecForString())
+ .property("name", codecForString())
+ .property("payto_uri", codecForPaytoString())
+ .property("balance", codecForBalance())
+ .property("row_id", codecForNumber())
+ .property("debit_threshold", codecForAmountString())
+ .property("min_cashout", codecOptional(codecForAmountString()))
+ .property("is_public", codecForBoolean())
+ .property("is_taler_exchange", codecForBoolean())
+ .property(
+ "status",
+ codecOptional(
+ codecForEither(
+ codecForConstString("active"),
+ codecForConstString("deleted"),
+ ),
+ ),
+ )
+ .build("TalerCorebankApi.AccountMinimalData");
+
+export const codecForListBankAccountsResponse =
+ (): Codec<TalerCorebankApi.ListBankAccountsResponse> =>
+ buildCodecForObject<TalerCorebankApi.ListBankAccountsResponse>()
+ .property("accounts", codecForList(codecForAccountMinimalData()))
+ .build("TalerCorebankApi.ListBankAccountsResponse");
+
+export const codecForAccountData = (): Codec<TalerCorebankApi.AccountData> =>
+ buildCodecForObject<TalerCorebankApi.AccountData>()
+ .property("name", codecForString())
+ .property("balance", codecForBalance())
+ .property("payto_uri", codecForPaytoString())
+ .property("debit_threshold", codecForAmountString())
+ .property("min_cashout", codecOptional(codecForAmountString()))
+ .property("contact_data", codecOptional(codecForChallengeContactData()))
+ .property("cashout_payto_uri", codecOptional(codecForPaytoString()))
+ .property("is_public", codecForBoolean())
+ .property("is_taler_exchange", codecForBoolean())
+ .property(
+ "tan_channel",
+ codecOptional(
+ codecForEither(
+ codecForConstString(TalerCorebankApi.TanChannel.SMS),
+ codecForConstString(TalerCorebankApi.TanChannel.EMAIL),
+ ),
+ ),
+ )
+ .property(
+ "status",
+ codecOptional(
+ codecForEither(
+ codecForConstString("active"),
+ codecForConstString("deleted"),
+ ),
+ ),
+ )
+ .build("TalerCorebankApi.AccountData");
+
+export const codecForChallengeContactData =
+ (): Codec<TalerCorebankApi.ChallengeContactData> =>
+ buildCodecForObject<TalerCorebankApi.ChallengeContactData>()
+ .property("email", codecOptional(codecForString()))
+ .property("phone", codecOptional(codecForString()))
+ .build("TalerCorebankApi.ChallengeContactData");
+
+export const codecForWithdrawalPublicInfo =
+ (): Codec<TalerCorebankApi.WithdrawalPublicInfo> =>
+ buildCodecForObject<TalerCorebankApi.WithdrawalPublicInfo>()
+ .property(
+ "status",
+ codecForEither(
+ codecForConstString("pending"),
+ codecForConstString("selected"),
+ codecForConstString("aborted"),
+ codecForConstString("confirmed"),
+ ),
+ )
+ .property("amount", codecForAmountString())
+ .property("username", codecForString())
+ .property("selected_reserve_pub", codecOptional(codecForString()))
+ .property(
+ "selected_exchange_account",
+ codecOptional(codecForPaytoString()),
+ )
+ .build("TalerCorebankApi.WithdrawalPublicInfo");
+
+export const codecForBankAccountTransactionsResponse =
+ (): Codec<TalerCorebankApi.BankAccountTransactionsResponse> =>
+ buildCodecForObject<TalerCorebankApi.BankAccountTransactionsResponse>()
+ .property(
+ "transactions",
+ codecForList(codecForBankAccountTransactionInfo()),
+ )
+ .build("TalerCorebankApi.BankAccountTransactionsResponse");
+
+export const codecForBankAccountTransactionInfo =
+ (): Codec<TalerCorebankApi.BankAccountTransactionInfo> =>
+ buildCodecForObject<TalerCorebankApi.BankAccountTransactionInfo>()
+ .property("creditor_payto_uri", codecForPaytoString())
+ .property("debtor_payto_uri", codecForPaytoString())
+ .property("amount", codecForAmountString())
+ .property(
+ "direction",
+ codecForEither(
+ codecForConstString("debit"),
+ codecForConstString("credit"),
+ ),
+ )
+ .property("subject", codecForString())
+ .property("row_id", codecForNumber())
+ .property("date", codecForTimestamp)
+ .build("TalerCorebankApi.BankAccountTransactionInfo");
+
+export const codecForCreateTransactionResponse =
+ (): Codec<TalerCorebankApi.CreateTransactionResponse> =>
+ buildCodecForObject<TalerCorebankApi.CreateTransactionResponse>()
+ .property("row_id", codecForNumber())
+ .build("TalerCorebankApi.CreateTransactionResponse");
+
+export const codecForRegisterAccountResponse =
+ (): Codec<TalerCorebankApi.RegisterAccountResponse> =>
+ buildCodecForObject<TalerCorebankApi.RegisterAccountResponse>()
+ .property("internal_payto_uri", codecForPaytoString())
+ .build("TalerCorebankApi.RegisterAccountResponse");
+
+export const codecForBankAccountCreateWithdrawalResponse =
+ (): Codec<TalerCorebankApi.BankAccountCreateWithdrawalResponse> =>
+ buildCodecForObject<TalerCorebankApi.BankAccountCreateWithdrawalResponse>()
+ .property("taler_withdraw_uri", codecForTalerUriString())
+ .property("withdrawal_id", codecForString())
+ .build("TalerCorebankApi.BankAccountCreateWithdrawalResponse");
+
+export const codecForCashoutPending =
+ (): Codec<TalerCorebankApi.CashoutResponse> =>
+ buildCodecForObject<TalerCorebankApi.CashoutResponse>()
+ .property("cashout_id", codecForNumber())
+ .build("TalerCorebankApi.CashoutPending");
+
+export const codecForCashoutConversionResponse =
+ (): Codec<TalerBankConversionApi.CashoutConversionResponse> =>
+ buildCodecForObject<TalerBankConversionApi.CashoutConversionResponse>()
+ .property("amount_credit", codecForAmountString())
+ .property("amount_debit", codecForAmountString())
+ .build("TalerCorebankApi.CashoutConversionResponse");
+
+export const codecForCashinConversionResponse =
+ (): Codec<TalerBankConversionApi.CashinConversionResponse> =>
+ buildCodecForObject<TalerBankConversionApi.CashinConversionResponse>()
+ .property("amount_credit", codecForAmountString())
+ .property("amount_debit", codecForAmountString())
+ .build("TalerCorebankApi.CashinConversionResponse");
+
+export const codecForCashouts = (): Codec<TalerCorebankApi.Cashouts> =>
+ buildCodecForObject<TalerCorebankApi.Cashouts>()
+ .property("cashouts", codecForList(codecForCashoutInfo()))
+ .build("TalerCorebankApi.Cashouts");
+
+export const codecForCashoutInfo = (): Codec<TalerCorebankApi.CashoutInfo> =>
+ buildCodecForObject<TalerCorebankApi.CashoutInfo>()
+ .property("cashout_id", codecForNumber())
+ .build("TalerCorebankApi.CashoutInfo");
+
+export const codecForGlobalCashouts =
+ (): Codec<TalerCorebankApi.GlobalCashouts> =>
+ buildCodecForObject<TalerCorebankApi.GlobalCashouts>()
+ .property("cashouts", codecForList(codecForGlobalCashoutInfo()))
+ .build("TalerCorebankApi.GlobalCashouts");
+
+export const codecForGlobalCashoutInfo =
+ (): Codec<TalerCorebankApi.GlobalCashoutInfo> =>
+ buildCodecForObject<TalerCorebankApi.GlobalCashoutInfo>()
+ .property("cashout_id", codecForNumber())
+ .property("username", codecForString())
+ .build("TalerCorebankApi.GlobalCashoutInfo");
+
+export const codecForCashoutStatusResponse =
+ (): Codec<TalerCorebankApi.CashoutStatusResponse> =>
+ buildCodecForObject<TalerCorebankApi.CashoutStatusResponse>()
+ .property("amount_debit", codecForAmountString())
+ .property("amount_credit", codecForAmountString())
+ .property("subject", codecForString())
+ .property("creation_time", codecForTimestamp)
+ .build("TalerCorebankApi.CashoutStatusResponse");
+
+export const codecForConversionRatesResponse =
+ (): Codec<TalerCorebankApi.ConversionRatesResponse> =>
+ buildCodecForObject<TalerCorebankApi.ConversionRatesResponse>()
+ .property("buy_at_ratio", codecForDecimalNumber())
+ .property("buy_in_fee", codecForDecimalNumber())
+ .property("sell_at_ratio", codecForDecimalNumber())
+ .property("sell_out_fee", codecForDecimalNumber())
+ .build("TalerCorebankApi.ConversionRatesResponse");
+
+export const codecForMonitorResponse =
+ (): Codec<TalerCorebankApi.MonitorResponse> =>
+ buildCodecForUnion<TalerCorebankApi.MonitorResponse>()
+ .discriminateOn("type")
+ .alternative("no-conversions", codecForMonitorNoConversion())
+ .alternative("with-conversions", codecForMonitorWithCashout())
+ .build("TalerWireGatewayApi.IncomingBankTransaction");
+
+export const codecForMonitorNoConversion =
+ (): Codec<TalerCorebankApi.MonitorNoConversion> =>
+ buildCodecForObject<TalerCorebankApi.MonitorNoConversion>()
+ .property("type", codecForConstString("no-conversions"))
+ .property("talerInCount", codecForNumber())
+ .property("talerInVolume", codecForAmountString())
+ .property("talerOutCount", codecForNumber())
+ .property("talerOutVolume", codecForAmountString())
+ .build("TalerCorebankApi.MonitorJustPayouts");
+
+export const codecForMonitorWithCashout =
+ (): Codec<TalerCorebankApi.MonitorWithConversion> =>
+ buildCodecForObject<TalerCorebankApi.MonitorWithConversion>()
+ .property("type", codecForConstString("with-conversions"))
+ .property("cashinCount", codecForNumber())
+ .property("cashinFiatVolume", codecForAmountString())
+ .property("cashinRegionalVolume", codecForAmountString())
+ .property("cashoutCount", codecForNumber())
+ .property("cashoutFiatVolume", codecForAmountString())
+ .property("cashoutRegionalVolume", codecForAmountString())
+ .property("talerInCount", codecForNumber())
+ .property("talerInVolume", codecForAmountString())
+ .property("talerOutCount", codecForNumber())
+ .property("talerOutVolume", codecForAmountString())
+ .build("TalerCorebankApi.MonitorWithCashout");
+
+export const codecForBankVersion =
+ (): Codec<TalerBankIntegrationApi.BankVersion> =>
+ buildCodecForObject<TalerBankIntegrationApi.BankVersion>()
+ .property("currency", codecForCurrencyName())
+ .property("currency_specification", codecForCurrencySpecificiation())
+ .property("name", codecForConstString("taler-bank-integration"))
+ .property("version", codecForLibtoolVersion())
+ .build("TalerBankIntegrationApi.BankVersion");
+
+export const codecForBankWithdrawalOperationStatus =
+ (): Codec<TalerBankIntegrationApi.BankWithdrawalOperationStatus> =>
+ buildCodecForObject<TalerBankIntegrationApi.BankWithdrawalOperationStatus>()
+ .property(
+ "status",
+ codecForEither(
+ codecForConstString("pending"),
+ codecForConstString("selected"),
+ codecForConstString("aborted"),
+ codecForConstString("confirmed"),
+ ),
+ )
+ .property("amount", codecForAmountString())
+ .property("sender_wire", codecOptional(codecForPaytoString()))
+ .property("suggested_exchange", codecOptional(codecForString()))
+ .property("confirm_transfer_url", codecOptional(codecForURL()))
+ .property("wire_types", codecForList(codecForString()))
+ .property("selected_reserve_pub", codecOptional(codecForString()))
+ .property("selected_exchange_account", codecOptional(codecForString()))
+ .build("TalerBankIntegrationApi.BankWithdrawalOperationStatus");
+
+export const codecForBankWithdrawalOperationPostResponse =
+ (): Codec<TalerBankIntegrationApi.BankWithdrawalOperationPostResponse> =>
+ buildCodecForObject<TalerBankIntegrationApi.BankWithdrawalOperationPostResponse>()
+ .property(
+ "status",
+ codecForEither(
+ codecForConstString("selected"),
+ codecForConstString("aborted"),
+ codecForConstString("confirmed"),
+ ),
+ )
+ .property("confirm_transfer_url", codecOptional(codecForURL()))
+ .build("TalerBankIntegrationApi.BankWithdrawalOperationPostResponse");
+
+export const codecForRevenueConfig = (): Codec<TalerRevenueApi.RevenueConfig> =>
+ buildCodecForObject<TalerRevenueApi.RevenueConfig>()
+ .property("name", codecForConstString("taler-revenue"))
+ .property("version", codecForString())
+ .property("currency", codecForString())
+ .property("implementation", codecOptional(codecForString()))
+ .build("TalerRevenueApi.RevenueConfig");
+
+export const codecForRevenueIncomingHistory =
+ (): Codec<TalerRevenueApi.RevenueIncomingHistory> =>
+ buildCodecForObject<TalerRevenueApi.RevenueIncomingHistory>()
+ .property("credit_account", codecForPaytoString())
+ .property(
+ "incoming_transactions",
+ codecForList(codecForRevenueIncomingBankTransaction()),
+ )
+ .build("TalerRevenueApi.MerchantIncomingHistory");
+
+export const codecForRevenueIncomingBankTransaction =
+ (): Codec<TalerRevenueApi.RevenueIncomingBankTransaction> =>
+ buildCodecForObject<TalerRevenueApi.RevenueIncomingBankTransaction>()
+ .property("amount", codecForAmountString())
+ .property("date", codecForTimestamp)
+ .property("debit_account", codecForPaytoString())
+ .property("row_id", codecForNumber())
+ .property("subject", codecForString())
+ .build("TalerRevenueApi.RevenueIncomingBankTransaction");
+
+export const codecForTransferResponse =
+ (): Codec<TalerWireGatewayApi.TransferResponse> =>
+ buildCodecForObject<TalerWireGatewayApi.TransferResponse>()
+ .property("row_id", codecForNumber())
+ .property("timestamp", codecForTimestamp)
+ .build("TalerWireGatewayApi.TransferResponse");
+
+export const codecForIncomingHistory =
+ (): Codec<TalerWireGatewayApi.IncomingHistory> =>
+ buildCodecForObject<TalerWireGatewayApi.IncomingHistory>()
+ .property("credit_account", codecForPaytoString())
+ .property(
+ "incoming_transactions",
+ codecForList(codecForIncomingBankTransaction()),
+ )
+ .build("TalerWireGatewayApi.IncomingHistory");
+
+export const codecForIncomingBankTransaction =
+ (): Codec<TalerWireGatewayApi.IncomingBankTransaction> =>
+ buildCodecForUnion<TalerWireGatewayApi.IncomingBankTransaction>()
+ .discriminateOn("type")
+ .alternative("RESERVE", codecForIncomingReserveTransaction())
+ .alternative("WAD", codecForIncomingWadTransaction())
+ .build("TalerWireGatewayApi.IncomingBankTransaction");
+
+export const codecForIncomingReserveTransaction =
+ (): Codec<TalerWireGatewayApi.IncomingReserveTransaction> =>
+ buildCodecForObject<TalerWireGatewayApi.IncomingReserveTransaction>()
+ .property("amount", codecForAmountString())
+ .property("date", codecForTimestamp)
+ .property("debit_account", codecForPaytoString())
+ .property("reserve_pub", codecForString())
+ .property("row_id", codecForNumber())
+ .property("type", codecForConstString("RESERVE"))
+ .build("TalerWireGatewayApi.IncomingReserveTransaction");
+
+export const codecForIncomingWadTransaction =
+ (): Codec<TalerWireGatewayApi.IncomingWadTransaction> =>
+ buildCodecForObject<TalerWireGatewayApi.IncomingWadTransaction>()
+ .property("amount", codecForAmountString())
+ .property("credit_account", codecForPaytoString())
+ .property("date", codecForTimestamp)
+ .property("debit_account", codecForPaytoString())
+ .property("origin_exchange_url", codecForURL())
+ .property("row_id", codecForNumber())
+ .property("type", codecForConstString("WAD"))
+ .property("wad_id", codecForString())
+ .build("TalerWireGatewayApi.IncomingWadTransaction");
+
+export const codecForOutgoingHistory =
+ (): Codec<TalerWireGatewayApi.OutgoingHistory> =>
+ buildCodecForObject<TalerWireGatewayApi.OutgoingHistory>()
+ .property("debit_account", codecForPaytoString())
+ .property(
+ "outgoing_transactions",
+ codecForList(codecForOutgoingBankTransaction()),
+ )
+ .build("TalerWireGatewayApi.OutgoingHistory");
+
+export const codecForOutgoingBankTransaction =
+ (): Codec<TalerWireGatewayApi.OutgoingBankTransaction> =>
+ buildCodecForObject<TalerWireGatewayApi.OutgoingBankTransaction>()
+ .property("amount", codecForAmountString())
+ .property("credit_account", codecForPaytoString())
+ .property("date", codecForTimestamp)
+ .property("exchange_base_url", codecForURL())
+ .property("row_id", codecForNumber())
+ .property("wtid", codecForString())
+ .build("TalerWireGatewayApi.OutgoingBankTransaction");
+
+export const codecForAddIncomingResponse =
+ (): Codec<TalerWireGatewayApi.AddIncomingResponse> =>
+ buildCodecForObject<TalerWireGatewayApi.AddIncomingResponse>()
+ .property("row_id", codecForNumber())
+ .property("timestamp", codecForTimestamp)
+ .build("TalerWireGatewayApi.AddIncomingResponse");
+
+export const codecForAmlRecords = (): Codec<TalerExchangeApi.AmlRecords> =>
+ buildCodecForObject<TalerExchangeApi.AmlRecords>()
+ .property("records", codecForList(codecForAmlRecord()))
+ .build("TalerExchangeApi.AmlRecords");
+
+export const codecForAmlRecord = (): Codec<TalerExchangeApi.AmlRecord> =>
+ buildCodecForObject<TalerExchangeApi.AmlRecord>()
+ .property("current_state", codecForNumber())
+ .property("h_payto", codecForString())
+ .property("rowid", codecForNumber())
+ .property("threshold", codecForAmountString())
+ .build("TalerExchangeApi.AmlRecord");
+
+export const codecForAmlDecisionDetails =
+ (): Codec<TalerExchangeApi.AmlDecisionDetails> =>
+ buildCodecForObject<TalerExchangeApi.AmlDecisionDetails>()
+ .property("aml_history", codecForList(codecForAmlDecisionDetail()))
+ .property("kyc_attributes", codecForList(codecForKycDetail()))
+ .build("TalerExchangeApi.AmlDecisionDetails");
+
+export const codecForAmlDecisionDetail =
+ (): Codec<TalerExchangeApi.AmlDecisionDetail> =>
+ buildCodecForObject<TalerExchangeApi.AmlDecisionDetail>()
+ .property("justification", codecForString())
+ .property("new_state", codecForNumber())
+ .property("decision_time", codecForTimestamp)
+ .property("new_threshold", codecForAmountString())
+ .property("decider_pub", codecForString())
+ .build("TalerExchangeApi.AmlDecisionDetail");
+
+export const codecForChallenge = (): Codec<TalerCorebankApi.Challenge> =>
+ buildCodecForObject<TalerCorebankApi.Challenge>()
+ .property("challenge_id", codecForNumber())
+ .build("TalerCorebankApi.Challenge");
+
+export const codecForTanTransmission =
+ (): Codec<TalerCorebankApi.TanTransmission> =>
+ buildCodecForObject<TalerCorebankApi.TanTransmission>()
+ .property(
+ "tan_channel",
+ codecForEither(
+ codecForConstString(TalerCorebankApi.TanChannel.SMS),
+ codecForConstString(TalerCorebankApi.TanChannel.EMAIL),
+ ),
+ )
+ .property("tan_info", codecForString())
+ .build("TalerCorebankApi.TanTransmission");
+
+interface KycDetail {
+ provider_section: string;
+ attributes?: Object;
+ collection_time: Timestamp;
+ expiration_time: Timestamp;
+}
+export const codecForKycDetail = (): Codec<TalerExchangeApi.KycDetail> =>
+ buildCodecForObject<TalerExchangeApi.KycDetail>()
+ .property("provider_section", codecForString())
+ .property("attributes", codecOptional(codecForAny()))
+ .property("collection_time", codecForTimestamp)
+ .property("expiration_time", codecForTimestamp)
+ .build("TalerExchangeApi.KycDetail");
+
+export const codecForAmlDecision = (): Codec<TalerExchangeApi.AmlDecision> =>
+ buildCodecForObject<TalerExchangeApi.AmlDecision>()
+ .property("justification", codecForString())
+ .property("new_threshold", codecForAmountString())
+ .property("h_payto", codecForString())
+ .property("new_state", codecForNumber())
+ .property("officer_sig", codecForString())
+ .property("decision_time", codecForTimestamp)
+ .property("kyc_requirements", codecOptional(codecForList(codecForString())))
+ .build("TalerExchangeApi.AmlDecision");
+
+export const codecForConversionInfo =
+ (): Codec<TalerBankConversionApi.ConversionInfo> =>
+ buildCodecForObject<TalerBankConversionApi.ConversionInfo>()
+ .property("cashin_fee", codecForAmountString())
+ .property("cashin_min_amount", codecForAmountString())
+ .property("cashin_ratio", codecForDecimalNumber())
+ .property(
+ "cashin_rounding_mode",
+ codecForEither(
+ codecForConstString("zero"),
+ codecForConstString("up"),
+ codecForConstString("nearest"),
+ ),
+ )
+ .property("cashin_tiny_amount", codecForAmountString())
+ .property("cashout_fee", codecForAmountString())
+ .property("cashout_min_amount", codecForAmountString())
+ .property("cashout_ratio", codecForDecimalNumber())
+ .property(
+ "cashout_rounding_mode",
+ codecForEither(
+ codecForConstString("zero"),
+ codecForConstString("up"),
+ codecForConstString("nearest"),
+ ),
+ )
+ .property("cashout_tiny_amount", codecForAmountString())
+ .build("ConversionBankConfig.ConversionInfo");
+
+export const codecForConversionBankConfig =
+ (): Codec<TalerBankConversionApi.IntegrationConfig> =>
+ buildCodecForObject<TalerBankConversionApi.IntegrationConfig>()
+ .property("name", codecForConstString("taler-conversion-info"))
+ .property("version", codecForString())
+ .property("regional_currency", codecForString())
+ .property(
+ "regional_currency_specification",
+ codecForCurrencySpecificiation(),
+ )
+ .property("fiat_currency", codecForString())
+ .property("fiat_currency_specification", codecForCurrencySpecificiation())
+
+ .property("conversion_rate", codecForConversionInfo())
+ .build("ConversionBankConfig.IntegrationConfig");
+
+export const codecForChallengerTermsOfServiceResponse =
+ (): Codec<ChallengerApi.ChallengerTermsOfServiceResponse> =>
+ buildCodecForObject<ChallengerApi.ChallengerTermsOfServiceResponse>()
+ .property("name", codecForConstString("challenger"))
+ .property("version", codecForString())
+ .property("implementation", codecOptional(codecForString()))
+ .build("ChallengerApi.ChallengerTermsOfServiceResponse");
+
+export const codecForChallengeSetupResponse =
+ (): Codec<ChallengerApi.ChallengeSetupResponse> =>
+ buildCodecForObject<ChallengerApi.ChallengeSetupResponse>()
+ .property("nonce", codecForString())
+ .build("ChallengerApi.ChallengeSetupResponse");
+
+export const codecForChallengeStatus =
+ (): Codec<ChallengerApi.ChallengeStatus> =>
+ buildCodecForObject<ChallengerApi.ChallengeStatus>()
+ .property("restrictions", codecOptional(codecForMap(codecForAny())))
+ .property("fix_address", codecForBoolean())
+ .property("last_address", codecOptional(codecForMap(codecForAny())))
+ .property("changes_left", codecForNumber())
+ .build("ChallengerApi.ChallengeStatus");
+export const codecForChallengeCreateResponse =
+ (): Codec<ChallengerApi.ChallengeCreateResponse> =>
+ buildCodecForObject<ChallengerApi.ChallengeCreateResponse>()
+ .property("attempts_left", codecForNumber())
+ .property("address", codecForAny())
+ .property("transmitted", codecForBoolean())
+ .property("next_tx_time", codecForString())
+ .build("ChallengerApi.ChallengeCreateResponse");
+
+export const codecForInvalidPinResponse =
+ (): Codec<ChallengerApi.InvalidPinResponse> =>
+ buildCodecForObject<ChallengerApi.InvalidPinResponse>()
+ .property("ec", codecOptional(codecForNumber()))
+ .property("hint", codecForAny())
+ .property("addresses_left", codecForNumber())
+ .property("pin_transmissions_left", codecForNumber())
+ .property("auth_attempts_left", codecForNumber())
+ .property("exhausted", codecForBoolean())
+ .property("no_challenge", codecForBoolean())
+ .build("ChallengerApi.InvalidPinResponse");
+
+export const codecForChallengerAuthResponse =
+ (): Codec<ChallengerApi.ChallengerAuthResponse> =>
+ buildCodecForObject<ChallengerApi.ChallengerAuthResponse>()
+ .property("access_token", codecForString())
+ .property("token_type", codecForAny())
+ .property("expires_in", codecForNumber())
+ .build("ChallengerApi.ChallengerAuthResponse");
+
+export const codecForChallengerInfoResponse =
+ (): Codec<ChallengerApi.ChallengerInfoResponse> =>
+ buildCodecForObject<ChallengerApi.ChallengerInfoResponse>()
+ .property("id", codecForNumber())
+ .property("address", codecForAny())
+ .property("address_type", codecForString())
+ .property("expires", codecForTimestamp)
+ .build("ChallengerApi.ChallengerInfoResponse");
+
+export const codecForTemplateEditableDetails =
+ (): Codec<TalerMerchantApi.TemplateEditableDetails> =>
+ buildCodecForObject<TalerMerchantApi.TemplateEditableDetails>()
+ .property("summary", codecOptional(codecForString()))
+ .property("currency", codecOptional(codecForString()))
+ .property("amount", codecOptional(codecForAmountString()))
+ .build("TemplateEditableDetails");
+
+export const codecForMerchantReserveCreateConfirmation =
+ (): Codec<TalerMerchantApi.MerchantReserveCreateConfirmation> =>
+ buildCodecForObject<TalerMerchantApi.MerchantReserveCreateConfirmation>()
+ .property("accounts", codecForList(codecForExchangeWireAccount()))
+ .property("reserve_pub", codecForString())
+ .build("MerchantReserveCreateConfirmation");
+
+type EmailAddress = string;
+type PhoneNumber = string;
+type EddsaSignature = string;
+// base32 encoded RSA blinded signature.
+type BlindedRsaSignature = string;
+type Base32 = string;
+
+type DecimalNumber = string;
+type RsaSignature = string;
+type Float = number;
+type LibtoolVersion = string;
+// The type of a coin's blinded envelope depends on the cipher that is used
+// for signing with a denomination key.
+type CoinEnvelope = RSACoinEnvelope | CSCoinEnvelope;
+// For denomination signatures based on RSA, the planchet is just a blinded
+// coin's public EdDSA key.
+interface RSACoinEnvelope {
+ cipher: "RSA" | "RSA+age_restricted";
+ rsa_blinded_planchet: string; // Crockford Base32 encoded
+}
+// For denomination signatures based on Blind Clause-Schnorr, the planchet
+// consists of the public nonce and two Curve25519 scalars which are two
+// blinded challenges in the Blinded Clause-Schnorr signature scheme.
+// See https://taler.net/papers/cs-thesis.pdf for details.
+interface CSCoinEnvelope {
+ cipher: "CS" | "CS+age_restricted";
+ cs_nonce: string; // Crockford Base32 encoded
+ cs_blinded_c0: string; // Crockford Base32 encoded
+ cs_blinded_c1: string; // Crockford Base32 encoded
+}
+// Secret for blinding/unblinding.
+// An RSA blinding secret, which is basically
+// a 256-bit nonce, converted to Crockford Base32.
+type DenominationBlindingKeyP = string;
+
+//FIXME: implement this codec
+const codecForURL = codecForString;
+//FIXME: implement this codec
+const codecForLibtoolVersion = codecForString;
+//FIXME: implement this codec
+const codecForCurrencyName = codecForString;
+//FIXME: implement this codec
+const codecForDecimalNumber = codecForString;
+
+export type WithdrawalOperationStatus =
+ | "pending"
+ | "selected"
+ | "aborted"
+ | "confirmed";
+
+export namespace TalerWireGatewayApi {
+ export interface TransferResponse {
+ // Timestamp that indicates when the wire transfer will be executed.
+ // In cases where the wire transfer gateway is unable to know when
+ // the wire transfer will be executed, the time at which the request
+ // has been received and stored will be returned.
+ // The purpose of this field is for debugging (humans trying to find
+ // the transaction) as well as for taxation (determining which
+ // time period a transaction belongs to).
+ timestamp: Timestamp;
+
+ // Opaque ID of the transaction that the bank has made.
+ row_id: SafeUint64;
+ }
+
+ export interface TransferRequest {
+ // Nonce to make the request idempotent. Requests with the same
+ // transaction_uid that differ in any of the other fields
+ // are rejected.
+ request_uid: HashCode;
+
+ // Amount to transfer.
+ amount: AmountString;
+
+ // Base URL of the exchange. Shall be included by the bank gateway
+ // in the appropriate section of the wire transfer details.
+ exchange_base_url: string;
+
+ // Wire transfer identifier chosen by the exchange,
+ // used by the merchant to identify the Taler order(s)
+ // associated with this wire transfer.
+ wtid: ShortHashCode;
+
+ // The recipient's account identifier as a payto URI.
+ credit_account: PaytoString;
+ }
+
+ export interface IncomingHistory {
+ // Array of incoming transactions.
+ incoming_transactions: IncomingBankTransaction[];
+
+ // Payto URI to identify the receiver of funds.
+ // This must be one of the exchange's bank accounts.
+ // Credit account is shared by all incoming transactions
+ // as per the nature of the request.
+
+ // undefined if incoming transaction is empty
+ credit_account?: PaytoString;
+ }
+
+ // Union discriminated by the "type" field.
+ export type IncomingBankTransaction =
+ | IncomingReserveTransaction
+ | IncomingWadTransaction;
+
+ export interface IncomingReserveTransaction {
+ type: "RESERVE";
+
+ // Opaque identifier of the returned record.
+ row_id: SafeUint64;
+
+ // Date of the transaction.
+ date: Timestamp;
+
+ // Amount transferred.
+ amount: AmountString;
+
+ // Payto URI to identify the sender of funds.
+ debit_account: PaytoString;
+
+ // The reserve public key extracted from the transaction details.
+ reserve_pub: EddsaPublicKey;
+ }
+
+ export interface IncomingWadTransaction {
+ type: "WAD";
+
+ // Opaque identifier of the returned record.
+ row_id: SafeUint64;
+
+ // Date of the transaction.
+ date: Timestamp;
+
+ // Amount transferred.
+ amount: AmountString;
+
+ // Payto URI to identify the receiver of funds.
+ // This must be one of the exchange's bank accounts.
+ credit_account: PaytoString;
+
+ // Payto URI to identify the sender of funds.
+ debit_account: PaytoString;
+
+ // Base URL of the exchange that originated the wad.
+ origin_exchange_url: string;
+
+ // The reserve public key extracted from the transaction details.
+ wad_id: WadId;
+ }
+
+ export interface OutgoingHistory {
+ // Array of outgoing transactions.
+ outgoing_transactions: OutgoingBankTransaction[];
+
+ // Payto URI to identify the sender of funds.
+ // This must be one of the exchange's bank accounts.
+ // Credit account is shared by all incoming transactions
+ // as per the nature of the request.
+
+ // undefined if outgoing transactions is empty
+ debit_account?: PaytoString;
+ }
+
+ export interface OutgoingBankTransaction {
+ // Opaque identifier of the returned record.
+ row_id: SafeUint64;
+
+ // Date of the transaction.
+ date: Timestamp;
+
+ // Amount transferred.
+ amount: AmountString;
+
+ // Payto URI to identify the receiver of funds.
+ credit_account: PaytoString;
+
+ // The wire transfer ID in the outgoing transaction.
+ wtid: ShortHashCode;
+
+ // Base URL of the exchange.
+ exchange_base_url: string;
+ }
+
+ export interface AddIncomingRequest {
+ // Amount to transfer.
+ amount: AmountString;
+
+ // Reserve public key that is included in the wire transfer details
+ // to identify the reserve that is being topped up.
+ reserve_pub: EddsaPublicKey;
+
+ // Account (as payto URI) that makes the wire transfer to the exchange.
+ // Usually this account must be created by the test harness before this API is
+ // used. An exception is the "exchange-fakebank", where any debit account can be
+ // specified, as it is automatically created.
+ debit_account: PaytoString;
+ }
+
+ export interface AddIncomingResponse {
+ // Timestamp that indicates when the wire transfer will be executed.
+ // In cases where the wire transfer gateway is unable to know when
+ // the wire transfer will be executed, the time at which the request
+ // has been received and stored will be returned.
+ // The purpose of this field is for debugging (humans trying to find
+ // the transaction) as well as for taxation (determining which
+ // time period a transaction belongs to).
+ timestamp: Timestamp;
+
+ // Opaque ID of the transaction that the bank has made.
+ row_id: SafeUint64;
+ }
+}
+
+export namespace TalerRevenueApi {
+ export interface RevenueConfig {
+ // Name of the API.
+ name: "taler-revenue";
+
+ // libtool-style representation of the Bank protocol version, see
+ // https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning
+ // The format is "current:revision:age".
+ version: string;
+
+ // Currency used by this gateway.
+ currency: string;
+
+ // URN of the implementation (needed to interpret 'revision' in version).
+ // @since v0, may become mandatory in the future.
+ implementation?: string;
+ }
+
+ export interface RevenueIncomingHistory {
+ // Array of incoming transactions.
+ incoming_transactions: RevenueIncomingBankTransaction[];
+
+ // Payto URI to identify the receiver of funds.
+ // Credit account is shared by all incoming transactions
+ // as per the nature of the request.
+ credit_account: string;
+ }
+
+ export interface RevenueIncomingBankTransaction {
+ // Opaque identifier of the returned record.
+ row_id: SafeUint64;
+
+ // Date of the transaction.
+ date: Timestamp;
+
+ // Amount transferred.
+ amount: AmountString;
+
+ // Payto URI to identify the sender of funds.
+ debit_account: string;
+
+ // The wire transfer subject.
+ subject: string;
+ }
+}
+
+export namespace TalerBankConversionApi {
+ export interface ConversionInfo {
+ // Exchange rate to buy regional currency from fiat
+ cashin_ratio: DecimalNumber;
+
+ // Exchange rate to sell regional currency for fiat
+ cashout_ratio: DecimalNumber;
+
+ // Fee to subtract after applying the cashin ratio.
+ cashin_fee: AmountString;
+
+ // Fee to subtract after applying the cashout ratio.
+ cashout_fee: AmountString;
+
+ // Minimum amount authorised for cashin, in fiat before conversion
+ cashin_min_amount: AmountString;
+
+ // Minimum amount authorised for cashout, in regional before conversion
+ cashout_min_amount: AmountString;
+
+ // Smallest possible regional amount, converted amount is rounded to this amount
+ cashin_tiny_amount: AmountString;
+
+ // Smallest possible fiat amount, converted amount is rounded to this amount
+ cashout_tiny_amount: AmountString;
+
+ // Rounding mode used during cashin conversion
+ cashin_rounding_mode: "zero" | "up" | "nearest";
+
+ // Rounding mode used during cashout conversion
+ cashout_rounding_mode: "zero" | "up" | "nearest";
+ }
+
+ export interface IntegrationConfig {
+ // libtool-style representation of the Bank 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 API.
+ name: "taler-conversion-info";
+
+ // Currency used by this bank.
+ regional_currency: string;
+
+ // How the bank SPA should render this currency.
+ regional_currency_specification: CurrencySpecification;
+
+ // External currency used during conversion.
+ fiat_currency: string;
+
+ // How the bank SPA should render this currency.
+ fiat_currency_specification: CurrencySpecification;
+
+ // Extra conversion rate information.
+ // Only present if server opts in to report the static conversion rate.
+ conversion_rate: ConversionInfo;
+ }
+
+ export interface CashinConversionResponse {
+ // Amount that the user will get deducted from their fiat
+ // bank account, according to the 'amount_credit' value.
+ amount_debit: AmountString;
+ // Amount that the user will receive in their regional
+ // bank account, according to 'amount_debit'.
+ amount_credit: AmountString;
+ }
+
+ export interface CashoutConversionResponse {
+ // Amount that the user will get deducted from their regional
+ // bank account, according to the 'amount_credit' value.
+ amount_debit: AmountString;
+ // Amount that the user will receive in their fiat
+ // bank account, according to 'amount_debit'.
+ amount_credit: AmountString;
+ }
+
+ export type RoundingMode = "zero" | "up" | "nearest";
+
+ export interface ConversionRate {
+ // Exchange rate to buy regional currency from fiat
+ cashin_ratio: DecimalNumber;
+
+ // Fee to subtract after applying the cashin ratio.
+ cashin_fee: AmountString;
+
+ // Minimum amount authorised for cashin, in fiat before conversion
+ cashin_min_amount: AmountString;
+
+ // Smallest possible regional amount, converted amount is rounded to this amount
+ cashin_tiny_amount: AmountString;
+
+ // Rounding mode used during cashin conversion
+ cashin_rounding_mode: RoundingMode;
+
+ // Exchange rate to sell regional currency for fiat
+ cashout_ratio: DecimalNumber;
+
+ // Fee to subtract after applying the cashout ratio.
+ cashout_fee: AmountString;
+
+ // Minimum amount authorised for cashout, in regional before conversion
+ cashout_min_amount: AmountString;
+
+ // Smallest possible fiat amount, converted amount is rounded to this amount
+ cashout_tiny_amount: AmountString;
+
+ // Rounding mode used during cashout conversion
+ cashout_rounding_mode: RoundingMode;
+ }
+}
+
+export namespace TalerBankIntegrationApi {
+ export interface BankVersion {
+ // libtool-style representation of the Bank protocol version, see
+ // https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning
+ // The format is "current:revision:age".
+ version: string;
+
+ // Currency used by this bank.
+ currency: string;
+
+ // How the bank SPA should render this currency.
+ currency_specification?: CurrencySpecification;
+
+ // Name of the API.
+ name: "taler-bank-integration";
+ }
+
+ export interface BankWithdrawalOperationStatus {
+ // Current status of the operation
+ // pending: the operation is pending parameters selection (exchange and reserve public key)
+ // selected: the operations has been selected and is pending confirmation
+ // aborted: the operation has been aborted
+ // confirmed: the transfer has been confirmed and registered by the bank
+ status: WithdrawalOperationStatus;
+
+ // Amount that will be withdrawn with this operation
+ // (raw amount without fee considerations).
+ amount: AmountString;
+
+ // Bank account of the customer that is withdrawing, as a
+ // payto URI.
+ sender_wire?: PaytoString;
+
+ // Suggestion for an exchange given by the bank.
+ suggested_exchange?: string;
+
+ // URL that the user needs to navigate to in order to
+ // complete some final confirmation (e.g. 2FA).
+ // It may contain withdrawal operation id
+ confirm_transfer_url?: string;
+
+ // Wire transfer types supported by the bank.
+ wire_types: string[];
+
+ // Reserve public key selected by the exchange,
+ // only non-null if status is selected or confirmed.
+ selected_reserve_pub?: string;
+
+ // Exchange account selected by the wallet
+ // only non-null if status is selected or confirmed.
+ selected_exchange_account?: string;
+ }
+
+ export interface BankWithdrawalOperationPostRequest {
+ // Reserve public key.
+ reserve_pub: string;
+
+ // Payto address of the exchange selected for the withdrawal.
+ selected_exchange: PaytoString;
+ }
+
+ export interface BankWithdrawalOperationPostResponse {
+ // Current status of the operation
+ // pending: the operation is pending parameters selection (exchange and reserve public key)
+ // selected: the operations has been selected and is pending confirmation
+ // aborted: the operation has been aborted
+ // confirmed: the transfer has been confirmed and registered by the bank
+ status: Omit<"pending", WithdrawalOperationStatus>;
+
+ // URL that the user needs to navigate to in order to
+ // complete some final confirmation (e.g. 2FA).
+ //
+ // Only applicable when status is selected.
+ // It may contain withdrawal operation id
+ confirm_transfer_url?: string;
+ }
+}
+
+export namespace TalerCorebankApi {
+ export interface IntegrationConfig {
+ // libtool-style representation of the Bank protocol version, see
+ // https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning
+ // The format is "current:revision:age".
+ version: string;
+
+ currency: string;
+
+ // How the bank SPA should render this currency.
+ currency_specification: CurrencySpecification;
+
+ // Name of the API.
+ name: "taler-bank-integration";
+ }
+ export interface Config {
+ // Name of this API, always "taler-corebank".
+ name: "libeufin-bank";
+ // name: "taler-corebank";
+
+ // API version in the form $n:$n:$n
+ version: string;
+
+ // Bank display name to be used in user interfaces.
+ // For consistency use "Taler Bank" if missing.
+ // @since v4, will become mandatory in the next version.
+ bank_name: string;
+
+ // Advertised base URL to use when you sharing an URL with another
+ // program.
+ // @since v4.
+ base_url?: string;
+
+ // If 'true' the server provides local currency conversion support
+ // If 'false' some parts of the API are not supported and return 501
+ allow_conversion: boolean;
+
+ // If 'true' anyone can register
+ // If 'false' only the admin can
+ allow_registrations: boolean;
+
+ // If 'true' account can delete themselves
+ // If 'false' only the admin can delete accounts
+ allow_deletions: boolean;
+
+ // If 'true' anyone can edit their name
+ // If 'false' only admin can
+ allow_edit_name: boolean;
+
+ // If 'true' anyone can edit their cashout account
+ // If 'false' only the admin
+ allow_edit_cashout_payto_uri: boolean;
+
+ // Default debt limit for newly created accounts
+ default_debit_threshold: AmountString;
+
+ // Currency used by this bank.
+ currency: string;
+
+ // How the bank SPA should render this currency.
+ currency_specification: CurrencySpecification;
+
+ // TAN channels supported by the server
+ supported_tan_channels: TanChannel[];
+
+ // Wire transfer type supported by the bank.
+ // Default to 'iban' is missing
+ // @since v4, may become mandatory in the future.
+ wire_type: string;
+ }
+
+ export interface BankAccountCreateWithdrawalRequest {
+ // Amount to withdraw.
+ amount: AmountString;
+ }
+ export interface BankAccountCreateWithdrawalResponse {
+ // ID of the withdrawal, can be used to view/modify the withdrawal operation.
+ withdrawal_id: string;
+
+ // URI that can be passed to the wallet to initiate the withdrawal.
+ taler_withdraw_uri: TalerUriString;
+ }
+ export interface WithdrawalPublicInfo {
+ // Current status of the operation
+ // pending: the operation is pending parameters selection (exchange and reserve public key)
+ // selected: the operations has been selected and is pending confirmation
+ // aborted: the operation has been aborted
+ // confirmed: the transfer has been confirmed and registered by the bank
+ status: WithdrawalOperationStatus;
+
+ // Amount that will be withdrawn with this operation
+ // (raw amount without fee considerations).
+ amount: AmountString;
+
+ // Account username
+ username: string;
+
+ // Reserve public key selected by the exchange,
+ // only non-null if status is selected or confirmed.
+ selected_reserve_pub?: string;
+
+ // Exchange account selected by the wallet
+ // only non-null if status is selected or confirmed.
+ selected_exchange_account?: PaytoString;
+ }
+
+ export interface BankAccountTransactionsResponse {
+ transactions: BankAccountTransactionInfo[];
+ }
+
+ export interface BankAccountTransactionInfo {
+ creditor_payto_uri: PaytoString;
+ debtor_payto_uri: PaytoString;
+
+ amount: AmountString;
+ direction: "debit" | "credit";
+
+ subject: string;
+
+ // Transaction unique ID. Matches
+ // $transaction_id from the URI.
+ row_id: number;
+ date: Timestamp;
+ }
+
+ export interface CreateTransactionRequest {
+ // Address in the Payto format of the wire transfer receiver.
+ // It needs at least the 'message' query string parameter.
+ payto_uri: PaytoString;
+
+ // Transaction amount (in the $currency:x.y format), optional.
+ // However, when not given, its value must occupy the 'amount'
+ // query string parameter of the 'payto' field. In case it
+ // is given in both places, the paytoUri's takes the precedence.
+ amount?: AmountString;
+
+ // Nonce to make the request idempotent. Requests with the same
+ // request_uid that differ in any of the other fields
+ // are rejected.
+ // @since v4, will become mandatory in the next version.
+ request_uid?: ShortHashCode;
+ }
+
+ export interface CreateTransactionResponse {
+ // ID identifying the transaction being created
+ row_id: Integer;
+ }
+
+ export interface RegisterAccountResponse {
+ // Internal payto URI of this bank account.
+ internal_payto_uri: PaytoString;
+ }
+
+ export interface RegisterAccountRequest {
+ // Username
+ username: string;
+
+ // Password.
+ password: string;
+
+ // Legal name of the account owner
+ name: string;
+
+ // Defaults to false.
+ is_public?: boolean;
+
+ // Is this a taler exchange account?
+ // If true:
+ // - incoming transactions to the account that do not
+ // have a valid reserve public key are automatically
+ // - the account provides the taler-wire-gateway-api endpoints
+ // Defaults to false.
+ is_taler_exchange?: boolean;
+
+ // Addresses where to send the TAN for transactions.
+ contact_data?: ChallengeContactData;
+
+ // 'payto' address of a fiat bank account.
+ // Payments will be sent to this bank account
+ // when the user wants to convert the regional currency
+ // back to fiat currency outside bank.
+ cashout_payto_uri?: PaytoString;
+
+ // Internal payto URI of this bank account.
+ // Used mostly for testing.
+ payto_uri?: PaytoString;
+
+ // If present, set the max debit allowed for this user
+ // Only admin can set this property.
+ debit_threshold?: AmountString;
+
+ // If present, set a custom minimum cashout amount for this account.
+ // Only admin can set this property
+ // @since v4
+ min_cashout?: AmountString;
+
+ // If present, enables 2FA and set the TAN channel used for challenges
+ // Only admin can set this property, other user can reconfig their account
+ // after creation.
+ tan_channel?: TanChannel;
+ }
+
+ export interface ChallengeContactData {
+ // E-Mail address
+ email?: EmailAddress;
+
+ // Phone number.
+ phone?: PhoneNumber;
+ }
+
+ export interface AccountReconfiguration {
+ // Addresses where to send the TAN for transactions.
+ // Currently only used for cashouts.
+ // If missing, cashouts will fail.
+ // In the future, might be used for other transactions
+ // as well.
+ // Only admin can change this property.
+ contact_data?: ChallengeContactData;
+
+ // 'payto' URI of a fiat bank account.
+ // Payments will be sent to this bank account
+ // when the user wants to convert the regional currency
+ // back to fiat currency outside bank.
+ // Only admin can change this property if not allowed in config
+ cashout_payto_uri?: PaytoString;
+
+ // If present, change the legal name associated with $username.
+ // Only admin can change this property if not allowed in config
+ name?: string;
+
+ // Make this account visible to anyone?
+ is_public?: boolean;
+
+ // If present, change the max debit allowed for this user
+ // Only admin can change this property.
+ debit_threshold?: AmountString;
+
+ // If present, change the custom minimum cashout amount for this account.
+ // Only admin can set this property
+ // @since v4
+ min_cashout?: AmountString;
+
+ // If present, enables 2FA and set the TAN channel used for challenges
+ tan_channel?: TanChannel | null;
+ }
+
+ export interface AccountPasswordChange {
+ // New password.
+ new_password: string;
+ // Old password. If present, check that the old password matches.
+ // Optional for admin account.
+ old_password?: string;
+ }
+
+ export interface PublicAccountsResponse {
+ public_accounts: PublicAccount[];
+ }
+ export interface PublicAccount {
+ // Username of the account
+ username: string;
+
+ // Internal payto URI of this bank account.
+ payto_uri: string;
+
+ // Current balance of the account
+ balance: Balance;
+
+ // Is this a taler exchange account?
+ is_taler_exchange: boolean;
+
+ // Opaque unique ID used for pagination.
+ // @since v4, will become mandatory in the future.
+ row_id?: Integer;
+ }
+
+ export interface ListBankAccountsResponse {
+ accounts: AccountMinimalData[];
+ }
+ export interface Balance {
+ amount: AmountString;
+ credit_debit_indicator: "credit" | "debit";
+ }
+ export interface AccountMinimalData {
+ // Username
+ username: string;
+
+ // Legal name of the account owner.
+ name: string;
+
+ // Internal payto URI of this bank account.
+ payto_uri: PaytoString;
+
+ // current balance of the account
+ balance: Balance;
+
+ // Number indicating the max debit allowed for the requesting user.
+ debit_threshold: AmountString;
+
+ // Custom minimum cashout amount for this account.
+ // If null or absent, the global conversion fee is used.
+ // @since v4
+ min_cashout?: AmountString;
+
+ // Is this account visible to anyone?
+ is_public: boolean;
+
+ // Is this a taler exchange account?
+ is_taler_exchange: boolean;
+
+ // Opaque unique ID used for pagination.
+ // @since v4, will become mandatory in the future.
+ row_id?: Integer;
+
+ // Current status of the account
+ // active: the account can be used
+ // deleted: the account has been deleted but is retained for compliance
+ // reasons, only the administrator can access it
+ // Default to 'active' is missing
+ // @since v4, will become mandatory in the next version.
+ status?: "active" | "deleted";
+ }
+
+ export interface AccountData {
+ // Legal name of the account owner.
+ name: string;
+
+ // Available balance on the account.
+ balance: Balance;
+
+ // payto://-URI of the account.
+ payto_uri: PaytoString;
+
+ // Number indicating the max debit allowed for the requesting user.
+ debit_threshold: AmountString;
+
+ // Custom minimum cashout amount for this account.
+ // If null or absent, the global conversion fee is used.
+ // @since v4
+ min_cashout?: AmountString;
+
+ contact_data?: ChallengeContactData;
+
+ // 'payto' address pointing the bank account
+ // where to send cashouts. This field is optional
+ // because not all the accounts are required to participate
+ // in the merchants' circuit. One example is the exchange:
+ // that never cashouts. Registering these accounts can
+ // be done via the access API.
+ cashout_payto_uri?: PaytoString;
+
+ // Is this account visible to anyone?
+ is_public: boolean;
+
+ // Is this a taler exchange account?
+ is_taler_exchange: boolean;
+
+ // Is 2FA enabled and what channel is used for challenges?
+ tan_channel?: TanChannel;
+
+ // Current status of the account
+ // active: the account can be used
+ // deleted: the account has been deleted but is retained for compliance
+ // reasons, only the administrator can access it
+ // Default to 'active' is missing
+ // @since v4, will become mandatory in the next version.
+ status?: "active" | "deleted";
+ }
+
+ export interface CashoutRequest {
+ // Nonce to make the request idempotent. Requests with the same
+ // request_uid that differ in any of the other fields
+ // are rejected.
+ request_uid: ShortHashCode;
+
+ // Optional subject to associate to the
+ // cashout operation. This data will appear
+ // as the incoming wire transfer subject in
+ // the user's fiat bank account.
+ subject?: string;
+
+ // That is the plain amount that the user specified
+ // to cashout. Its $currency is the (regional) currency of the
+ // bank instance.
+ amount_debit: AmountString;
+
+ // That is the amount that will effectively be
+ // transferred by the bank to the user's bank
+ // account, that is external to the regional currency.
+ // It is expressed in the fiat currency and
+ // is calculated after the cashout fee and the
+ // exchange rate. See the /cashout-rates call.
+ // The client needs to calculate this amount
+ // correctly based on the amount_debit and the cashout rate,
+ // otherwise the request will fail.
+ amount_credit: AmountString;
+ }
+
+ export interface CashoutResponse {
+ // ID identifying the operation being created
+ cashout_id: number;
+ }
+
+ /**
+ * @deprecated since 4, use 2fa
+ */
+ export interface CashoutConfirmRequest {
+ // the TAN that confirms $CASHOUT_ID.
+ tan: string;
+ }
+
+ export interface Cashouts {
+ // Every string represents a cash-out operation ID.
+ cashouts: CashoutInfo[];
+ }
+
+ export interface CashoutInfo {
+ cashout_id: number;
+ /**
+ * @deprecated since 4, use new 2fa
+ */
+ status?: "pending" | "aborted" | "confirmed";
+ }
+ export interface GlobalCashouts {
+ // Every string represents a cash-out operation ID.
+ cashouts: GlobalCashoutInfo[];
+ }
+ export interface GlobalCashoutInfo {
+ cashout_id: number;
+ username: string;
+ }
+
+ export interface CashoutStatusResponse {
+ // Amount debited to the internal
+ // regional currency bank account.
+ amount_debit: AmountString;
+
+ // Amount credited to the external bank account.
+ amount_credit: AmountString;
+
+ // Transaction subject.
+ subject: string;
+
+ // Time when the cashout was created.
+ creation_time: Timestamp;
+ }
+
+ export interface ConversionRatesResponse {
+ // Exchange rate to buy the local currency from the external one
+ buy_at_ratio: DecimalNumber;
+
+ // Exchange rate to sell the local currency for the external one
+ sell_at_ratio: DecimalNumber;
+
+ // Fee to subtract after applying the buy ratio.
+ buy_in_fee: DecimalNumber;
+
+ // Fee to subtract after applying the sell ratio.
+ sell_out_fee: DecimalNumber;
+ }
+
+ export enum MonitorTimeframeParam {
+ hour,
+ day,
+ month,
+ year,
+ decade,
+ }
+
+ export type MonitorResponse = MonitorNoConversion | MonitorWithConversion;
+
+ // Monitoring stats when conversion is not supported
+ export interface MonitorNoConversion {
+ type: "no-conversions";
+
+ // How many payments were made to a Taler exchange by another
+ // bank account.
+ talerInCount: number;
+
+ // Overall volume that has been paid to a Taler
+ // exchange by another bank account.
+ talerInVolume: AmountString;
+
+ // How many payments were made by a Taler exchange to another
+ // bank account.
+ talerOutCount: number;
+
+ // Overall volume that has been paid by a Taler
+ // exchange to another bank account.
+ talerOutVolume: AmountString;
+ }
+ // Monitoring stats when conversion is supported
+ export interface MonitorWithConversion {
+ type: "with-conversions";
+
+ // How many cashin operations were confirmed by a
+ // wallet owner. Note: wallet owners
+ // are NOT required to be customers of the libeufin-bank.
+ cashinCount: number;
+
+ // Overall regional currency that has been paid by the regional admin account
+ // to regional bank accounts to fulfill all the confirmed cashin operations.
+ cashinRegionalVolume: AmountString;
+
+ // Overall fiat currency that has been paid to the fiat admin account
+ // by fiat bank accounts to fulfill all the confirmed cashin operations.
+ cashinFiatVolume: AmountString;
+
+ // How many cashout operations were confirmed.
+ cashoutCount: number;
+
+ // Overall regional currency that has been paid to the regional admin account
+ // by fiat bank accounts to fulfill all the confirmed cashout operations.
+ cashoutRegionalVolume: AmountString;
+
+ // Overall fiat currency that has been paid by the fiat admin account
+ // to fiat bank accounts to fulfill all the confirmed cashout operations.
+ cashoutFiatVolume: AmountString;
+
+ // How many payments were made to a Taler exchange by another
+ // bank account.
+ talerInCount: number;
+
+ // Overall volume that has been paid to a Taler
+ // exchange by another bank account.
+ talerInVolume: AmountString;
+
+ // How many payments were made by a Taler exchange to another
+ // bank account.
+ talerOutCount: number;
+
+ // Overall volume that has been paid by a Taler
+ // exchange to another bank account.
+ talerOutVolume: AmountString;
+ }
+ export interface TanTransmission {
+ // Channel of the last successful transmission of the TAN challenge.
+ tan_channel: TanChannel;
+
+ // Info of the last successful transmission of the TAN challenge.
+ tan_info: string;
+ }
+
+ export interface Challenge {
+ // Unique identifier of the challenge to solve to run this protected
+ // operation.
+ challenge_id: number;
+ }
+
+ export interface ChallengeSolve {
+ // The TAN code that solves $CHALLENGE_ID
+ tan: string;
+ }
+
+ export enum TanChannel {
+ SMS = "sms",
+ EMAIL = "email",
+ }
+}
+
+export namespace TalerExchangeApi {
+ export enum AmlState {
+ normal = 0,
+ pending = 1,
+ frozen = 2,
+ }
+
+ export interface AmlRecords {
+ // Array of AML records matching the query.
+ records: AmlRecord[];
+ }
+ export interface AmlRecord {
+ // Which payto-address is this record about.
+ // Identifies a GNU Taler wallet or an affected bank account.
+ h_payto: PaytoHash;
+
+ // What is the current AML state.
+ current_state: AmlState;
+
+ // Monthly transaction threshold before a review will be triggered
+ threshold: AmountString;
+
+ // RowID of the record.
+ rowid: Integer;
+ }
+
+ export interface AmlDecisionDetails {
+ // Array of AML decisions made for this account. Possibly
+ // contains only the most recent decision if "history" was
+ // not set to 'true'.
+ aml_history: AmlDecisionDetail[];
+
+ // Array of KYC attributes obtained for this account.
+ kyc_attributes: KycDetail[];
+ }
+ export interface AmlDecisionDetail {
+ // What was the justification given?
+ justification: string;
+
+ // What is the new AML state.
+ new_state: Integer;
+
+ // When was this decision made?
+ decision_time: Timestamp;
+
+ // What is the new AML decision threshold (in monthly transaction volume)?
+ new_threshold: AmountString;
+
+ // Who made the decision?
+ decider_pub: AmlOfficerPublicKeyP;
+ }
+ export interface KycDetail {
+ // Name of the configuration section that specifies the provider
+ // which was used to collect the KYC details
+ provider_section: string;
+
+ // The collected KYC data. NULL if the attribute data could not
+ // be decrypted (internal error of the exchange, likely the
+ // attribute key was changed).
+ attributes?: Object;
+
+ // Time when the KYC data was collected
+ collection_time: Timestamp;
+
+ // Time when the validity of the KYC data will expire
+ expiration_time: Timestamp;
+ }
+
+ export interface AmlDecision {
+ // Human-readable justification for the decision.
+ justification: string;
+
+ // At what monthly transaction volume should the
+ // decision be automatically reviewed?
+ new_threshold: AmountString;
+
+ // Which payto-address is the decision about?
+ // Identifies a GNU Taler wallet or an affected bank account.
+ h_payto: PaytoHash;
+
+ // What is the new AML state (e.g. frozen, unfrozen, etc.)
+ // Numerical values are defined in AmlDecisionState.
+ new_state: Integer;
+
+ // Signature by the AML officer over a
+ // TALER_MasterAmlOfficerStatusPS.
+ // Must have purpose TALER_SIGNATURE_MASTER_AML_KEY.
+ officer_sig: EddsaSignature;
+
+ // When was the decision made?
+ decision_time: Timestamp;
+
+ // Optional argument to impose new KYC requirements
+ // that the customer has to satisfy to unblock transactions.
+ kyc_requirements?: string[];
+ }
+
+ export interface ExchangeVersionResponse {
+ // libtool-style representation of the Exchange 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: "taler-exchange";
+
+ // URN of the implementation (needed to interpret 'revision' in version).
+ // @since v18, may become mandatory in the future.
+ implementation?: string;
+
+ // Currency supported by this exchange, given
+ // as a currency code ("USD" or "EUR").
+ currency: string;
+
+ // How wallets should render this currency.
+ currency_specification: CurrencySpecification;
+
+ // Names of supported KYC requirements.
+ supported_kyc_requirements: string[];
+ }
+
+ export type AccountRestriction =
+ | RegexAccountRestriction
+ | DenyAllAccountRestriction;
+ // Account restriction that disables this type of
+ // account for the indicated operation categorically.
+ export interface DenyAllAccountRestriction {
+ type: "deny";
+ }
+ // Accounts interacting with this type of account
+ // restriction must have a payto://-URI matching
+ // the given regex.
+ export interface RegexAccountRestriction {
+ type: "regex";
+
+ // Regular expression that the payto://-URI of the
+ // partner account must follow. The regular expression
+ // should follow posix-egrep, but without support for character
+ // classes, GNU extensions, back-references or intervals. See
+ // https://www.gnu.org/software/findutils/manual/html_node/find_html/posix_002degrep-regular-expression-syntax.html
+ // for a description of the posix-egrep syntax. Applications
+ // may support regexes with additional features, but exchanges
+ // must not use such regexes.
+ payto_regex: string;
+
+ // Hint for a human to understand the restriction
+ // (that is hopefully easier to comprehend than the regex itself).
+ human_hint: string;
+
+ // Map from IETF BCP 47 language tags to localized
+ // human hints.
+ human_hint_i18n?: { [lang_tag: string]: string };
+ }
+
+ export interface WireAccount {
+ // payto:// URI identifying the account and wire method
+ payto_uri: PaytoString;
+
+ // URI to convert amounts from or to the currency used by
+ // this wire account of the exchange. Missing if no
+ // conversion is applicable.
+ conversion_url?: string;
+
+ // Restrictions that apply to bank accounts that would send
+ // funds to the exchange (crediting this exchange bank account).
+ // Optional, empty array for unrestricted.
+ credit_restrictions: AccountRestriction[];
+
+ // Restrictions that apply to bank accounts that would receive
+ // funds from the exchange (debiting this exchange bank account).
+ // Optional, empty array for unrestricted.
+ debit_restrictions: AccountRestriction[];
+
+ // Signature using the exchange's offline key over
+ // a TALER_MasterWireDetailsPS
+ // with purpose TALER_SIGNATURE_MASTER_WIRE_DETAILS.
+ master_sig: EddsaSignature;
+ }
+
+ export interface ExchangeKeysResponse {
+ // libtool-style representation of the Exchange protocol version, see
+ // https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning
+ // The format is "current:revision:age".
+ version: string;
+
+ // The exchange's base URL.
+ base_url: string;
+
+ // The exchange's currency or asset unit.
+ currency: string;
+
+ /**
+ * FIXME: PARTIALLY IMPLEMENTED!!
+ */
+
+ // How wallets should render this currency.
+ // currency_specification: CurrencySpecification;
+
+ // // Absolute cost offset for the STEFAN curve used
+ // // to (over) approximate fees payable by amount.
+ // stefan_abs: AmountString;
+
+ // // Factor to multiply the logarithm of the amount
+ // // with to (over) approximate fees payable by amount.
+ // // Note that the total to be paid is first to be
+ // // divided by the smallest denomination to obtain
+ // // the value that the logarithm is to be taken of.
+ // stefan_log: AmountString;
+
+ // // Linear cost factor for the STEFAN curve used
+ // // to (over) approximate fees payable by amount.
+ // //
+ // // Note that this is a scalar, as it is multiplied
+ // // with the actual amount.
+ // stefan_lin: Float;
+
+ // // Type of the asset. "fiat", "crypto", "regional"
+ // // or "stock". Wallets should adjust their UI/UX
+ // // based on this value.
+ // asset_type: string;
+
+ // // Array of wire accounts operated by the exchange for
+ // // incoming wire transfers.
+ // accounts: WireAccount[];
+
+ // // Object mapping names of wire methods (i.e. "iban" or "x-taler-bank")
+ // // to wire fees.
+ // wire_fees: { method: AggregateTransferFee[] };
+
+ // // List of exchanges that this exchange is partnering
+ // // with to enable wallet-to-wallet transfers.
+ // wads: ExchangePartner[];
+
+ // // Set to true if this exchange allows the use
+ // // of reserves for rewards.
+ // // @deprecated in protocol v18.
+ // rewards_allowed: false;
+
+ // // EdDSA master public key of the exchange, used to sign entries
+ // // in denoms and signkeys.
+ // master_public_key: EddsaPublicKey;
+
+ // // Relative duration until inactive reserves are closed;
+ // // not signed (!), can change without notice.
+ // reserve_closing_delay: RelativeTime;
+
+ // // Threshold amounts beyond which wallet should
+ // // trigger the KYC process of the issuing
+ // // exchange. Optional option, if not given there is no limit.
+ // // Currency must match currency.
+ // wallet_balance_limit_without_kyc?: AmountString[];
+
+ // // Denominations offered by this exchange
+ // denominations: DenomGroup[];
+
+ // // Compact EdDSA signature (binary-only) over the
+ // // contatentation of all of the master_sigs (in reverse
+ // // chronological order by group) in the arrays under
+ // // "denominations". Signature of TALER_ExchangeKeySetPS
+ // exchange_sig: EddsaSignature;
+
+ // // Public EdDSA key of the exchange that was used to generate the signature.
+ // // Should match one of the exchange's signing keys from signkeys. It is given
+ // // explicitly as the client might otherwise be confused by clock skew as to
+ // // which signing key was used for the exchange_sig.
+ // exchange_pub: EddsaPublicKey;
+
+ // // Denominations for which the exchange currently offers/requests recoup.
+ // recoup: Recoup[];
+
+ // // Array of globally applicable fees by time range.
+ // global_fees: GlobalFees[];
+
+ // // The date when the denomination keys were last updated.
+ // list_issue_date: Timestamp;
+
+ // // Auditors of the exchange.
+ // auditors: AuditorKeys[];
+
+ // // The exchange's signing keys.
+ // signkeys: SignKey[];
+
+ // // Optional field with a dictionary of (name, object) pairs defining the
+ // // supported and enabled extensions, such as age_restriction.
+ // extensions?: { name: ExtensionManifest };
+
+ // // Signature by the exchange master key of the SHA-256 hash of the
+ // // normalized JSON-object of field extensions, if it was set.
+ // // The signature has purpose TALER_SIGNATURE_MASTER_EXTENSIONS.
+ // extensions_sig?: EddsaSignature;
+ }
+
+ interface ExtensionManifest {
+ // The criticality of the extension MUST be provided. It has the same
+ // semantics as "critical" has for extensions in X.509:
+ // - if "true", the client must "understand" the extension before
+ // proceeding,
+ // - if "false", clients can safely skip extensions they do not
+ // understand.
+ // (see https://datatracker.ietf.org/doc/html/rfc5280#section-4.2)
+ critical: boolean;
+
+ // The version information MUST be provided in Taler's protocol version
+ // ranges notation, see
+ // https://docs.taler.net/core/api-common.html#protocol-version-ranges
+ version: LibtoolVersion;
+
+ // Optional configuration object, defined by the feature itself
+ config?: object;
+ }
+
+ interface SignKey {
+ // The actual exchange's EdDSA signing public key.
+ key: EddsaPublicKey;
+
+ // 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: EddsaSignature;
+ }
+
+ interface AuditorKeys {
+ // The auditor's EdDSA signing public key.
+ auditor_pub: EddsaPublicKey;
+
+ // 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: HashCode;
+
+ // Signature of TALER_ExchangeKeyValidityPS.
+ auditor_sig: EddsaSignature;
+ }
+
+ interface GlobalFees {
+ // What date (inclusive) does these fees go into effect?
+ start_date: Timestamp;
+
+ // What date (exclusive) does this fees stop going into effect?
+ end_date: Timestamp;
+
+ // Account history fee, charged when a user wants to
+ // obtain a reserve/account history.
+ history_fee: AmountString;
+
+ // Annual fee charged for having an open account at the
+ // exchange. Charged to the account. If the account
+ // balance is insufficient to cover this fee, the account
+ // is automatically deleted/closed. (Note that the exchange
+ // will keep the account history around for longer for
+ // regulatory reasons.)
+ account_fee: AmountString;
+
+ // Purse fee, charged only if a purse is abandoned
+ // and was not covered by the account limit.
+ purse_fee: AmountString;
+
+ // How long will the exchange preserve the account history?
+ // After an account was deleted/closed, the exchange will
+ // retain the account history for legal reasons until this time.
+ history_expiration: RelativeTime;
+
+ // Non-negative number of concurrent purses that any
+ // account holder is allowed to create without having
+ // to pay the purse_fee.
+ purse_account_limit: Integer;
+
+ // How long does an exchange keep a purse around after a purse
+ // has expired (or been successfully merged)? A 'GET' request
+ // for a purse will succeed until the purse expiration time
+ // plus this value.
+ purse_timeout: RelativeTime;
+
+ // Signature of TALER_GlobalFeesPS.
+ master_sig: EddsaSignature;
+ }
+
+ interface Recoup {
+ // Hash of the public key of the denomination that is being revoked under
+ // emergency protocol (see /recoup).
+ h_denom_pub: HashCode;
+
+ // We do not include any signature here, as the primary use-case for
+ // this emergency involves the exchange having lost its signing keys,
+ // so such a signature here would be pretty worthless. However, the
+ // exchange will not honor /recoup requests unless they are for
+ // denomination keys listed here.
+ }
+
+ interface AggregateTransferFee {
+ // Per transfer wire transfer fee.
+ wire_fee: AmountString;
+
+ // Per transfer closing fee.
+ closing_fee: AmountString;
+
+ // What date (inclusive) does this fee go into effect?
+ // The different fees must cover the full time period in which
+ // any of the denomination keys are valid without overlap.
+ start_date: Timestamp;
+
+ // What date (exclusive) does this fee stop going into effect?
+ // The different fees must cover the full time period in which
+ // any of the denomination keys are valid without overlap.
+ end_date: Timestamp;
+
+ // Signature of TALER_MasterWireFeePS with
+ // purpose TALER_SIGNATURE_MASTER_WIRE_FEES.
+ sig: EddsaSignature;
+ }
+
+ interface ExchangePartner {
+ // Base URL of the partner exchange.
+ partner_base_url: string;
+
+ // Public master key of the partner exchange.
+ partner_master_pub: EddsaPublicKey;
+
+ // Per exchange-to-exchange transfer (wad) fee.
+ wad_fee: AmountString;
+
+ // Exchange-to-exchange wad (wire) transfer frequency.
+ wad_frequency: RelativeTime;
+
+ // When did this partnership begin (under these conditions)?
+ start_date: Timestamp;
+
+ // How long is this partnership expected to last?
+ end_date: Timestamp;
+
+ // Signature using the exchange's offline key over
+ // TALER_WadPartnerSignaturePS
+ // with purpose TALER_SIGNATURE_MASTER_PARTNER_DETAILS.
+ master_sig: EddsaSignature;
+ }
+
+ type DenomGroup =
+ | DenomGroupRsa
+ | DenomGroupCs
+ | DenomGroupRsaAgeRestricted
+ | DenomGroupCsAgeRestricted;
+ interface DenomGroupRsa extends DenomGroupCommon {
+ cipher: "RSA";
+
+ denoms: ({
+ rsa_pub: RsaPublicKey;
+ } & DenomCommon)[];
+ }
+ interface DenomGroupCs extends DenomGroupCommon {
+ cipher: "CS";
+
+ denoms: ({
+ cs_pub: Cs25519Point;
+ } & DenomCommon)[];
+ }
+
+ // Binary representation of the age groups.
+ // The bits set in the mask mark the edges at the beginning of a next age
+ // group. F.e. for the age groups
+ // 0-7, 8-9, 10-11, 12-13, 14-15, 16-17, 18-21, 21-*
+ // the following bits are set:
+ //
+ // 31 24 16 8 0
+ // | | | | |
+ // oooooooo oo1oo1o1 o1o1o1o1 ooooooo1
+ //
+ // A value of 0 means that the exchange does not support the extension for
+ // age-restriction.
+ type AgeMask = Integer;
+
+ interface DenomGroupRsaAgeRestricted extends DenomGroupCommon {
+ cipher: "RSA+age_restricted";
+ age_mask: AgeMask;
+
+ denoms: ({
+ rsa_pub: RsaPublicKey;
+ } & DenomCommon)[];
+ }
+ interface DenomGroupCsAgeRestricted extends DenomGroupCommon {
+ cipher: "CS+age_restricted";
+ age_mask: AgeMask;
+
+ denoms: ({
+ cs_pub: Cs25519Point;
+ } & DenomCommon)[];
+ }
+ // Common attributes for all denomination groups
+ interface DenomGroupCommon {
+ // 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;
+ }
+ interface DenomCommon {
+ // Signature of TALER_DenominationKeyValidityPS.
+ master_sig: EddsaSignature;
+
+ // When does the denomination key become valid?
+ stamp_start: Timestamp;
+
+ // When is it no longer possible to withdraw coins
+ // of this denomination?
+ stamp_expire_withdraw: Timestamp;
+
+ // When is it no longer possible to deposit coins
+ // of this denomination?
+ stamp_expire_deposit: Timestamp;
+
+ // 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: Timestamp;
+
+ // 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;
+ }
+ type DenominationKey = RsaDenominationKey | CSDenominationKey;
+ interface RsaDenominationKey {
+ cipher: "RSA";
+
+ // 32-bit age mask.
+ age_mask: Integer;
+
+ // RSA public key
+ rsa_public_key: RsaPublicKey;
+ }
+ interface CSDenominationKey {
+ cipher: "CS";
+
+ // 32-bit age mask.
+ age_mask: Integer;
+
+ // Public key of the denomination.
+ cs_public_key: Cs25519Point;
+ }
+}
+
+export namespace TalerMerchantApi {
+ export interface VersionResponse {
+ // libtool-style representation of the Merchant 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: "taler-merchant";
+
+ // URN of the implementation (needed to interpret 'revision' in version).
+ // @since **v8**, may become mandatory in the future.
+ implementation?: string;
+
+ // Default (!) currency supported by this backend.
+ // This is the currency that the backend should
+ // suggest by default to the user when entering
+ // amounts. See currencies for a list of
+ // supported currencies and how to render them.
+ currency: string;
+
+ // How services should render currencies supported
+ // by this backend. Maps
+ // currency codes (e.g. "EUR" or "KUDOS") to
+ // the respective currency specification.
+ // All currencies in this map are supported by
+ // the backend. Note that the actual currency
+ // specifications are a *hint* for applications
+ // that would like *advice* on how to render amounts.
+ // Applications *may* ignore the currency specification
+ // if they know how to render currencies that they are
+ // used with.
+ currencies: { [currency: string]: CurrencySpecification };
+
+ // Array of exchanges trusted by the merchant.
+ // Since protocol **v6**.
+ exchanges: ExchangeConfigInfo[];
+ }
+
+ export interface ExchangeConfigInfo {
+ // Base URL of the exchange REST API.
+ base_url: string;
+
+ // Currency for which the merchant is configured
+ // to trust the exchange.
+ // May not be the one the exchange actually uses,
+ // but is the only one we would trust this exchange for.
+ currency: string;
+
+ // Offline master public key of the exchange. The
+ // /keys data must be signed with this public
+ // key for us to trust it.
+ master_pub: EddsaPublicKey;
+ }
+ export interface ClaimRequest {
+ // Nonce to identify the wallet that claimed the order.
+ nonce: string;
+
+ // Token that authorizes the wallet to claim the order.
+ // *Optional* as the merchant may not have required it
+ // (create_token set to false in PostOrderRequest).
+ token?: ClaimToken;
+ }
+
+ export interface ClaimResponse {
+ // Contract terms of the claimed order
+ contract_terms: ContractTerms;
+
+ // Signature by the merchant over the contract terms.
+ sig: EddsaSignature;
+ }
+
+ export interface PaymentResponse {
+ // Signature on TALER_PaymentResponsePS with the public
+ // key of the merchant instance.
+ sig: EddsaSignature;
+
+ // Text to be shown to the point-of-sale staff as a proof of
+ // payment.
+ pos_confirmation?: string;
+ }
+
+ export interface PaymentStatusRequestParams {
+ // Hash of the order’s contract terms (this is used to
+ // authenticate the wallet/customer in case
+ // $ORDER_ID is guessable).
+ // Required once an order was claimed.
+ contractTermHash?: string;
+ // Authorizes the request via the claim token that
+ // was returned in the PostOrderResponse. Used with
+ // unclaimed orders only. Whether token authorization is
+ // required is determined by the merchant when the
+ // frontend creates the order.
+ claimToken?: string;
+ // Session ID that the payment must be bound to.
+ // If not specified, the payment is not session-bound.
+ sessionId?: string;
+ // If specified, the merchant backend will wait up to
+ // timeout_ms milliseconds for completion of the payment
+ // before sending the HTTP response. A client must never
+ // rely on this behavior, as the merchant backend may return
+ // a response immediately.
+ timeout?: number;
+ // If set to “yes”, poll for the order’s pending refunds
+ // to be picked up. timeout_ms specifies how long we
+ // will wait for the refund.
+ awaitRefundObtained?: boolean;
+ // Indicates that we are polling for a refund above the
+ // given AMOUNT. timeout_ms will specify how long we
+ // will wait for the refund.
+ refund?: AmountString;
+ // Since protocol v9 refunded orders are only returned
+ // under “already_paid_order_id” if this flag is set
+ // explicitly to “YES”.
+ allowRefundedForRepurchase?: boolean;
+ }
+ export interface GetKycStatusRequestParams {
+ // If specified, the KYC check should return
+ // the KYC status only for this wire account.
+ // Otherwise, for all wire accounts.
+ wireHash?: string;
+ // If specified, the KYC check should return
+ // the KYC status only for the given exchange.
+ // Otherwise, for all exchanges we interacted with.
+ exchangeURL?: string;
+ // If specified, the merchant will wait up to
+ // timeout_ms milliseconds for the exchanges to
+ // confirm completion of the KYC process(es).
+ timeout?: number;
+ }
+ export interface GetOtpDeviceRequestParams {
+ // Timestamp in seconds to use when calculating
+ // the current OTP code of the device. Since protocol v10.
+ faketime?: number;
+ // Price to use when calculating the current OTP
+ // code of the device. Since protocol v10.
+ price?: AmountString;
+ }
+ export interface GetOrderRequestParams {
+ // Session ID that the payment must be bound to.
+ // If not specified, the payment is not session-bound.
+ sessionId?: string;
+ // Timeout in milliseconds to wait for a payment if
+ // the answer would otherwise be negative (long polling).
+ timeout?: number;
+ // Since protocol v9 refunded orders are only returned
+ // under “already_paid_order_id” if this flag is set
+ // explicitly to “YES”.
+ allowRefundedForRepurchase?: boolean;
+ }
+ export interface ListWireTransferRequestParams {
+ // Filter for transfers to the given bank account
+ // (subject and amount MUST NOT be given in the payto URI).
+ paytoURI?: string;
+ // Filter for transfers executed before the given timestamp.
+ before?: number;
+ // Filter for transfers executed after the given timestamp.
+ after?: number;
+ // At most return the given number of results. Negative for
+ // descending in execution time, positive for ascending in
+ // execution time. Default is -20.
+ limit?: number;
+ // Starting transfer_serial_id for an iteration.
+ offset?: string;
+ // Filter transfers by verification status.
+ verified?: boolean;
+ order?: "asc" | "dec";
+ }
+ export interface ListOrdersRequestParams {
+ // If set to yes, only return paid orders, if no only
+ // unpaid orders. Do not give (or use “all”) to see all
+ // orders regardless of payment status.
+ paid?: boolean;
+ // If set to yes, only return refunded orders, if no only
+ // unrefunded orders. Do not give (or use “all”) to see
+ // all orders regardless of refund status.
+ refunded?: boolean;
+ // If set to yes, only return wired orders, if no only
+ // orders with missing wire transfers. Do not give (or
+ // use “all”) to see all orders regardless of wire transfer
+ // status.
+ wired?: boolean;
+ // At most return the given number of results. Negative
+ // for descending by row ID, positive for ascending by
+ // row ID. Default is 20. Since protocol v12.
+ limit?: number;
+ // Non-negative date in seconds after the UNIX Epoc, see delta
+ // for its interpretation. If not specified, we default to the
+ // oldest or most recent entry, depending on delta.
+ date?: AbsoluteTime;
+ // Starting product_serial_id for an iteration.
+ // Since protocol v12.
+ offset?: string;
+ // Timeout in milliseconds to wait for additional orders if the
+ // answer would otherwise be negative (long polling). Only useful
+ // if delta is positive. Note that the merchant MAY still return
+ // a response that contains fewer than delta orders.
+ timeout?: number;
+ // Since protocol v6. Filters by session ID.
+ sessionId?: string;
+ // Since protocol v6. Filters by fulfillment URL.
+ fulfillmentUrl?: string;
+
+ order?: "asc" | "dec";
+ }
+
+ export interface PayRequest {
+ // The coins used to make the payment.
+ coins: CoinPaySig[];
+
+ // Custom inputs from the wallet for the contract.
+ wallet_data?: Object;
+
+ // The session for which the payment is made (or replayed).
+ // Only set for session-based payments.
+ session_id?: string;
+ }
+ export interface CoinPaySig {
+ // Signature by the coin.
+ coin_sig: EddsaSignature;
+
+ // Public key of the coin being spent.
+ coin_pub: EddsaPublicKey;
+
+ // Signature made by the denomination public key.
+ ub_sig: RsaSignature;
+
+ // The hash of the denomination public key associated with this coin.
+ h_denom: HashCode;
+
+ // The amount that is subtracted from this coin with this payment.
+ contribution: AmountString;
+
+ // URL of the exchange this coin was withdrawn from.
+ exchange_url: string;
+ }
+
+ export interface StatusPaid {
+ type: "paid";
+
+ // Was the payment refunded (even partially, via refund or abort)?
+ refunded: boolean;
+
+ // Is any amount of the refund still waiting to be picked up (even partially)?
+ refund_pending: boolean;
+
+ // Amount that was refunded in total.
+ refund_amount: AmountString;
+
+ // Amount that already taken by the wallet.
+ refund_taken: AmountString;
+ }
+ export interface StatusGotoResponse {
+ type: "goto";
+ // The client should go to the reorder URL, there a fresh
+ // order might be created as this one is taken by another
+ // customer or wallet (or repurchase detection logic may
+ // apply).
+ public_reorder_url: string;
+ }
+ export interface StatusUnpaidResponse {
+ type: "unpaid";
+ // URI that the wallet must process to complete the payment.
+ taler_pay_uri: string;
+
+ // Status URL, can be used as a redirect target for the browser
+ // to show the order QR code / trigger the wallet.
+ fulfillment_url?: string;
+
+ // Alternative order ID which was paid for already in the same session.
+ // Only given if the same product was purchased before in the same session.
+ already_paid_order_id?: string;
+ }
+
+ export interface PaidRefundStatusResponse {
+ // Text to be shown to the point-of-sale staff as a proof of
+ // payment (present only if reusable OTP algorithm is used).
+ pos_confirmation?: string;
+
+ // True if the order has been subjected to
+ // refunds. False if it was simply paid.
+ refunded: boolean;
+ }
+ export interface PaidRequest {
+ // Signature on TALER_PaymentResponsePS with the public
+ // key of the merchant instance.
+ sig: EddsaSignature;
+
+ // Hash of the order's contract terms (this is used to authenticate the
+ // wallet/customer and to enable signature verification without
+ // database access).
+ h_contract: HashCode;
+
+ // Hash over custom inputs from the wallet for the contract.
+ wallet_data_hash?: HashCode;
+
+ // Session id for which the payment is proven.
+ session_id: string;
+ }
+
+ export interface AbortRequest {
+ // Hash of the order's contract terms (this is used to authenticate the
+ // wallet/customer in case $ORDER_ID is guessable).
+ h_contract: HashCode;
+
+ // List of coins the wallet would like to see refunds for.
+ // (Should be limited to the coins for which the original
+ // payment succeeded, as far as the wallet knows.)
+ coins: AbortingCoin[];
+ }
+ interface AbortingCoin {
+ // Public key of a coin for which the wallet is requesting an abort-related refund.
+ coin_pub: EddsaPublicKey;
+
+ // The amount to be refunded (matches the original contribution)
+ contribution: AmountString;
+
+ // URL of the exchange this coin was withdrawn from.
+ exchange_url: string;
+ }
+ export interface AbortResponse {
+ // List of refund responses about the coins that the wallet
+ // requested an abort for. In the same order as the coins
+ // from the original request.
+ // The rtransaction_id is implied to be 0.
+ refunds: MerchantAbortPayRefundStatus[];
+ }
+ export type MerchantAbortPayRefundStatus =
+ | MerchantAbortPayRefundSuccessStatus
+ | MerchantAbortPayRefundFailureStatus;
+ // Details about why a refund failed.
+ export interface MerchantAbortPayRefundFailureStatus {
+ // Used as tag for the sum type RefundStatus sum type.
+ type: "failure";
+
+ // HTTP status of the exchange request, must NOT be 200.
+ exchange_status: Integer;
+
+ // Taler error code from the exchange reply, if available.
+ exchange_code?: Integer;
+
+ // If available, HTTP reply from the exchange.
+ exchange_reply?: Object;
+ }
+ // Additional details needed to verify the refund confirmation signature
+ // (h_contract_terms and merchant_pub) are already known
+ // to the wallet and thus not included.
+ export interface MerchantAbortPayRefundSuccessStatus {
+ // Used as tag for the sum type MerchantCoinRefundStatus sum type.
+ type: "success";
+
+ // HTTP status of the exchange request, 200 (integer) required for refund confirmations.
+ exchange_status: 200;
+
+ // The EdDSA :ref:signature (binary-only) with purpose
+ // TALER_SIGNATURE_EXCHANGE_CONFIRM_REFUND using a current signing key of the
+ // exchange affirming the successful refund.
+ exchange_sig: EddsaSignature;
+
+ // Public EdDSA key of the exchange that was used to generate the signature.
+ // Should match one of the exchange's signing keys from /keys. It is given
+ // explicitly as the client might otherwise be confused by clock skew as to
+ // which signing key was used.
+ exchange_pub: EddsaPublicKey;
+ }
+
+ export interface WalletRefundRequest {
+ // Hash of the order's contract terms (this is used to authenticate the
+ // wallet/customer).
+ h_contract: HashCode;
+ }
+ export interface WalletRefundResponse {
+ // Amount that was refunded in total.
+ refund_amount: AmountString;
+
+ // Successful refunds for this payment, empty array for none.
+ refunds: MerchantCoinRefundStatus[];
+
+ // Public key of the merchant.
+ merchant_pub: EddsaPublicKey;
+ }
+ export type MerchantCoinRefundStatus =
+ | MerchantCoinRefundSuccessStatus
+ | MerchantCoinRefundFailureStatus;
+ // Details about why a refund failed.
+ export interface MerchantCoinRefundFailureStatus {
+ // Used as tag for the sum type RefundStatus sum type.
+ type: "failure";
+
+ // HTTP status of the exchange request, must NOT be 200.
+ exchange_status: Integer;
+
+ // Taler error code from the exchange reply, if available.
+ exchange_code?: Integer;
+
+ // If available, HTTP reply from the exchange.
+ exchange_reply?: Object;
+
+ // Refund transaction ID.
+ rtransaction_id: Integer;
+
+ // Public key of a coin that was refunded.
+ coin_pub: EddsaPublicKey;
+
+ // Amount that was refunded, including refund fee charged by the exchange
+ // to the customer.
+ refund_amount: AmountString;
+
+ // Timestamp when the merchant approved the refund.
+ // Useful for grouping refunds.
+ execution_time: Timestamp;
+ }
+ // Additional details needed to verify the refund confirmation signature
+ // (h_contract_terms and merchant_pub) are already known
+ // to the wallet and thus not included.
+ export interface MerchantCoinRefundSuccessStatus {
+ // Used as tag for the sum type MerchantCoinRefundStatus sum type.
+ type: "success";
+
+ // HTTP status of the exchange request, 200 (integer) required for refund confirmations.
+ exchange_status: 200;
+
+ // The EdDSA :ref:signature (binary-only) with purpose
+ // TALER_SIGNATURE_EXCHANGE_CONFIRM_REFUND using a current signing key of the
+ // exchange affirming the successful refund.
+ exchange_sig: EddsaSignature;
+
+ // Public EdDSA key of the exchange that was used to generate the signature.
+ // Should match one of the exchange's signing keys from /keys. It is given
+ // explicitly as the client might otherwise be confused by clock skew as to
+ // which signing key was used.
+ exchange_pub: EddsaPublicKey;
+
+ // Refund transaction ID.
+ rtransaction_id: Integer;
+
+ // Public key of a coin that was refunded.
+ coin_pub: EddsaPublicKey;
+
+ // Amount that was refunded, including refund fee charged by the exchange
+ // to the customer.
+ refund_amount: AmountString;
+
+ // Timestamp when the merchant approved the refund.
+ // Useful for grouping refunds.
+ execution_time: Timestamp;
+ }
+
+ interface RewardInformation {
+ // Exchange from which the reward will be withdrawn. Needed by the
+ // wallet to determine denominations, fees, etc.
+ exchange_url: string;
+
+ // URL where to go after obtaining the reward.
+ next_url: string;
+
+ // (Remaining) amount of the reward (including fees).
+ reward_amount: AmountString;
+
+ // Timestamp indicating when the reward is set to expire (may be in the past).
+ // Note that rewards that have expired MAY also result in a 404 response.
+ expiration: Timestamp;
+ }
+
+ interface RewardPickupRequest {
+ // List of planchets the wallet wants to use for the reward.
+ planchets: PlanchetDetail[];
+ }
+ interface PlanchetDetail {
+ // Hash of the denomination's public key (hashed to reduce
+ // bandwidth consumption).
+ denom_pub_hash: HashCode;
+
+ // Coin's blinded public key.
+ coin_ev: CoinEnvelope;
+ }
+ interface RewardResponse {
+ // Blind RSA signatures over the planchets.
+ // The order of the signatures matches the planchets list.
+ blind_sigs: BlindSignature[];
+ }
+ interface BlindSignature {
+ // The (blind) RSA signature. Still needs to be unblinded.
+ blind_sig: BlindedRsaSignature;
+ }
+
+ export interface InstanceConfigurationMessage {
+ // Name of the merchant instance to create (will become $INSTANCE).
+ // Must match the regex ^[A-Za-z0-9][A-Za-z0-9_.@-]+$.
+ id: string;
+
+ // Merchant name corresponding to this instance.
+ name: string;
+
+ // Type of the user (business or individual).
+ // Defaults to 'business'. Should become mandatory field
+ // in the future, left as optional for API compatibility for now.
+ user_type?: string;
+
+ // Merchant email for customer contact.
+ email?: string;
+
+ // Merchant public website.
+ website?: string;
+
+ // Merchant logo.
+ logo?: ImageDataUrl;
+
+ // Authentication settings for this instance
+ auth: InstanceAuthConfigurationMessage;
+
+ // The merchant's physical address (to be put into contracts).
+ address: Location;
+
+ // The jurisdiction under which the merchant conducts its business
+ // (to be put into contracts).
+ jurisdiction: Location;
+
+ // Use STEFAN curves to determine default fees?
+ // If false, no fees are allowed by default.
+ // Can always be overridden by the frontend on a per-order basis.
+ use_stefan: boolean;
+
+ // If the frontend does NOT specify an execution date, how long should
+ // we tell the exchange to wait to aggregate transactions before
+ // executing the wire transfer? This delay is added to the current
+ // time when we generate the advisory execution time for the exchange.
+ default_wire_transfer_delay: RelativeTime;
+
+ // If the frontend does NOT specify a payment deadline, how long should
+ // offers we make be valid by default?
+ default_pay_delay: RelativeTime;
+ }
+
+ export interface InstanceAuthConfigurationMessage {
+ // Type of authentication.
+ // "external": The mechant backend does not do
+ // any authentication checks. Instead an API
+ // gateway must do the authentication.
+ // "token": The merchant checks an auth token.
+ // See "token" for details.
+ method: "external" | "token";
+
+ // For method "token", this field is mandatory.
+ // The token MUST begin with the string "secret-token:".
+ // After the auth token has been set (with method "token"),
+ // the value must be provided in a "Authorization: Bearer $token"
+ // header.
+ token?: AccessToken;
+ }
+
+ export interface InstanceReconfigurationMessage {
+ // Merchant name corresponding to this instance.
+ name: string;
+
+ // Type of the user (business or individual).
+ // Defaults to 'business'. Should become mandatory field
+ // in the future, left as optional for API compatibility for now.
+ user_type?: string;
+
+ // Merchant email for customer contact.
+ email?: string;
+
+ // Merchant public website.
+ website?: string;
+
+ // Merchant logo.
+ logo?: ImageDataUrl;
+
+ // The merchant's physical address (to be put into contracts).
+ address: Location;
+
+ // The jurisdiction under which the merchant conducts its business
+ // (to be put into contracts).
+ jurisdiction: Location;
+
+ // Use STEFAN curves to determine default fees?
+ // If false, no fees are allowed by default.
+ // Can always be overridden by the frontend on a per-order basis.
+ use_stefan: boolean;
+
+ // If the frontend does NOT specify an execution date, how long should
+ // we tell the exchange to wait to aggregate transactions before
+ // executing the wire transfer? This delay is added to the current
+ // time when we generate the advisory execution time for the exchange.
+ default_wire_transfer_delay: RelativeTime;
+
+ // If the frontend does NOT specify a payment deadline, how long should
+ // offers we make be valid by default?
+ default_pay_delay: RelativeTime;
+ }
+
+ export interface InstancesResponse {
+ // List of instances that are present in the backend (see Instance).
+ instances: Instance[];
+ }
+
+ export interface Instance {
+ // Merchant name corresponding to this instance.
+ name: string;
+
+ // Type of the user ("business" or "individual").
+ user_type: string;
+
+ // Merchant public website.
+ website?: string;
+
+ // Merchant logo.
+ logo?: ImageDataUrl;
+
+ // Merchant instance this response is about ($INSTANCE).
+ id: string;
+
+ // Public key of the merchant/instance, in Crockford Base32 encoding.
+ merchant_pub: EddsaPublicKey;
+
+ // List of the payment targets supported by this instance. Clients can
+ // specify the desired payment target in /order requests. Note that
+ // front-ends do not have to support wallets selecting payment targets.
+ payment_targets: string[];
+
+ // Has this instance been deleted (but not purged)?
+ deleted: boolean;
+ }
+
+ export interface QueryInstancesResponse {
+ // Merchant name corresponding to this instance.
+ name: string;
+
+ // Type of the user ("business" or "individual").
+ user_type: string;
+
+ // Merchant email for customer contact.
+ email?: string;
+
+ // Merchant public website.
+ website?: string;
+
+ // Merchant logo.
+ logo?: ImageDataUrl;
+
+ // Public key of the merchant/instance, in Crockford Base32 encoding.
+ merchant_pub: EddsaPublicKey;
+
+ // The merchant's physical address (to be put into contracts).
+ address: Location;
+
+ // The jurisdiction under which the merchant conducts its business
+ // (to be put into contracts).
+ jurisdiction: Location;
+
+ // Use STEFAN curves to determine default fees?
+ // If false, no fees are allowed by default.
+ // Can always be overridden by the frontend on a per-order basis.
+ use_stefan: boolean;
+
+ // If the frontend does NOT specify an execution date, how long should
+ // we tell the exchange to wait to aggregate transactions before
+ // executing the wire transfer? This delay is added to the current
+ // time when we generate the advisory execution time for the exchange.
+ default_wire_transfer_delay: RelativeTime;
+
+ // If the frontend does NOT specify a payment deadline, how long should
+ // offers we make be valid by default?
+ default_pay_delay: RelativeTime;
+
+ // Authentication configuration.
+ // Does not contain the token when token auth is configured.
+ auth: {
+ method: "external" | "token";
+ };
+ }
+
+ export interface AccountKycRedirects {
+ // Array of pending KYCs.
+ pending_kycs: MerchantAccountKycRedirect[];
+
+ // Array of exchanges with no reply.
+ timeout_kycs: ExchangeKycTimeout[];
+ }
+
+ export interface MerchantAccountKycRedirect {
+ // URL that the user should open in a browser to
+ // proceed with the KYC process (as returned
+ // by the exchange's /kyc-check/ endpoint).
+ // Optional, missing if the account is blocked
+ // due to AML and not due to KYC.
+ kyc_url?: string;
+
+ // AML status of the account.
+ aml_status: Integer;
+
+ // Base URL of the exchange this is about.
+ exchange_url: string;
+
+ // Our bank wire account this is about.
+ payto_uri: PaytoString;
+ }
+
+ export interface ExchangeKycTimeout {
+ // Base URL of the exchange this is about.
+ exchange_url: string;
+
+ // Numeric error code indicating errors the exchange
+ // returned, or TALER_EC_INVALID for none.
+ exchange_code: number;
+
+ // HTTP status code returned by the exchange when we asked for
+ // information about the KYC status.
+ // 0 if there was no response at all.
+ exchange_http_status: number;
+ }
+
+ export interface AccountAddDetails {
+ // payto:// URI of the account.
+ payto_uri: PaytoString;
+
+ // URL from where the merchant can download information
+ // about incoming wire transfers to this account.
+ credit_facade_url?: string;
+
+ // Credentials to use when accessing the credit facade.
+ // Never returned on a GET (as this may be somewhat
+ // sensitive data). Can be set in POST
+ // or PATCH requests to update (or delete) credentials.
+ // To really delete credentials, set them to the type: "none".
+ credit_facade_credentials?: FacadeCredentials;
+ }
+
+ export type FacadeCredentials =
+ | NoFacadeCredentials
+ | BasicAuthFacadeCredentials;
+ export interface NoFacadeCredentials {
+ type: "none";
+ }
+ export interface BasicAuthFacadeCredentials {
+ type: "basic";
+
+ // Username to use to authenticate
+ username: string;
+
+ // Password to use to authenticate
+ password: string;
+ }
+ export interface AccountAddResponse {
+ // Hash over the wire details (including over the salt).
+ h_wire: HashCode;
+
+ // Salt used to compute h_wire.
+ salt: HashCode;
+ }
+
+ export interface AccountPatchDetails {
+ // URL from where the merchant can download information
+ // about incoming wire transfers to this account.
+ credit_facade_url?: string;
+
+ // Credentials to use when accessing the credit facade.
+ // Never returned on a GET (as this may be somewhat
+ // sensitive data). Can be set in POST
+ // or PATCH requests to update (or delete) credentials.
+ // To really delete credentials, set them to the type: "none".
+ // If the argument is omitted, the old credentials
+ // are simply preserved.
+ credit_facade_credentials?: FacadeCredentials;
+ }
+
+ export interface AccountsSummaryResponse {
+ // List of accounts that are known for the instance.
+ accounts: BankAccountSummaryEntry[];
+ }
+
+ // TODO: missing in docs
+ export interface BankAccountSummaryEntry {
+ // payto:// URI of the account.
+ payto_uri: PaytoString;
+
+ // Hash over the wire details (including over the salt).
+ h_wire: HashCode;
+ }
+ export interface BankAccountEntry {
+ // payto:// URI of the account.
+ payto_uri: PaytoString;
+
+ // Hash over the wire details (including over the salt).
+ h_wire: HashCode;
+
+ // Salt used to compute h_wire.
+ salt: HashCode;
+
+ // URL from where the merchant can download information
+ // about incoming wire transfers to this account.
+ credit_facade_url?: string;
+
+ // true if this account is active,
+ // false if it is historic.
+ active?: boolean;
+ }
+
+ export interface ProductAddDetail {
+ // Product ID to use.
+ product_id: string;
+
+ // Human-readable product description.
+ description: string;
+
+ // Map from IETF BCP 47 language tags to localized descriptions.
+ description_i18n?: { [lang_tag: string]: string };
+
+ // Unit in which the product is measured (liters, kilograms, packages, etc.).
+ unit: string;
+
+ // The price for one unit of the product. Zero is used
+ // to imply that this product is not sold separately, or
+ // that the price is not fixed, and must be supplied by the
+ // front-end. If non-zero, this price MUST include applicable
+ // taxes.
+ price: AmountString;
+
+ // An optional base64-encoded product image.
+ image?: ImageDataUrl;
+
+ // A list of taxes paid by the merchant for one unit of this product.
+ taxes?: Tax[];
+
+ // Number of units of the product in stock in sum in total,
+ // including all existing sales ever. Given in product-specific
+ // units.
+ // A value of -1 indicates "infinite" (i.e. for "electronic" books).
+ total_stock: Integer;
+
+ // Identifies where the product is in stock.
+ address?: Location;
+
+ // Identifies when we expect the next restocking to happen.
+ next_restock?: Timestamp;
+
+ // Minimum age buyer must have (in years). Default is 0.
+ minimum_age?: Integer;
+ }
+
+ export interface ProductPatchDetail {
+ // Human-readable product description.
+ description: string;
+
+ // Map from IETF BCP 47 language tags to localized descriptions.
+ description_i18n?: { [lang_tag: string]: string };
+
+ // Unit in which the product is measured (liters, kilograms, packages, etc.).
+ unit: string;
+
+ // The price for one unit of the product. Zero is used
+ // to imply that this product is not sold separately, or
+ // that the price is not fixed, and must be supplied by the
+ // front-end. If non-zero, this price MUST include applicable
+ // taxes.
+ price: AmountString;
+
+ // An optional base64-encoded product image.
+ image?: ImageDataUrl;
+
+ // A list of taxes paid by the merchant for one unit of this product.
+ taxes?: Tax[];
+
+ // Number of units of the product in stock in sum in total,
+ // including all existing sales ever. Given in product-specific
+ // units.
+ // A value of -1 indicates "infinite" (i.e. for "electronic" books).
+ total_stock: Integer;
+
+ // Number of units of the product that were lost (spoiled, stolen, etc.).
+ total_lost?: Integer;
+
+ // Identifies where the product is in stock.
+ address?: Location;
+
+ // Identifies when we expect the next restocking to happen.
+ next_restock?: Timestamp;
+
+ // Minimum age buyer must have (in years). Default is 0.
+ minimum_age?: Integer;
+ }
+
+ export interface InventorySummaryResponse {
+ // List of products that are present in the inventory.
+ products: InventoryEntry[];
+ }
+
+ export interface InventoryEntry {
+ // Product identifier, as found in the product.
+ product_id: string;
+ // product_serial_id of the product in the database.
+ product_serial: Integer;
+ }
+
+ export interface FullInventoryDetailsResponse {
+ // List of products that are present in the inventory.
+ products: MerchantPosProductDetail[];
+
+ // List of categories in the inventory.
+ categories: MerchantCategory[];
+ }
+
+ export interface MerchantPosProductDetail {
+ // A unique numeric ID of the product
+ product_serial: number;
+
+ // A merchant-internal unique identifier for the product
+ product_id?: string;
+
+ // A list of category IDs this product belongs to.
+ // Typically, a product only belongs to one category, but more than one is supported.
+ categories: number[];
+
+ // Human-readable product description.
+ description: string;
+
+ // Map from IETF BCP 47 language tags to localized descriptions.
+ description_i18n: { [lang_tag: string]: string };
+
+ // Unit in which the product is measured (liters, kilograms, packages, etc.).
+ unit: string;
+
+ // The price for one unit of the product. Zero is used
+ // to imply that this product is not sold separately, or
+ // that the price is not fixed, and must be supplied by the
+ // front-end. If non-zero, this price MUST include applicable
+ // taxes.
+ price: AmountString;
+
+ // An optional base64-encoded product image.
+ image?: ImageDataUrl;
+
+ // A list of taxes paid by the merchant for one unit of this product.
+ taxes?: Tax[];
+
+ // Number of units of the product in stock in sum in total,
+ // including all existing sales ever. Given in product-specific
+ // units.
+ // Optional, if missing treat as "infinite".
+ total_stock?: Integer;
+
+ // Minimum age buyer must have (in years).
+ minimum_age?: Integer;
+ }
+
+ export interface MerchantCategory {
+ // A unique numeric ID of the category
+ id: number;
+
+ // The name of the category. This will be shown to users and used in the order summary.
+ name: string;
+
+ // Map from IETF BCP 47 language tags to localized names
+ name_i18n?: { [lang_tag: string]: string };
+ }
+
+ export interface ProductDetail {
+ // Human-readable product description.
+ description: string;
+
+ // Map from IETF BCP 47 language tags to localized descriptions.
+ description_i18n: { [lang_tag: string]: string };
+
+ // Unit in which the product is measured (liters, kilograms, packages, etc.).
+ unit: string;
+
+ // The price for one unit of the product. Zero is used
+ // to imply that this product is not sold separately, or
+ // that the price is not fixed, and must be supplied by the
+ // front-end. If non-zero, this price MUST include applicable
+ // taxes.
+ price: AmountString;
+
+ // An optional base64-encoded product image.
+ image: ImageDataUrl;
+
+ // A list of taxes paid by the merchant for one unit of this product.
+ taxes?: Tax[];
+
+ // Number of units of the product in stock in sum in total,
+ // including all existing sales ever. Given in product-specific
+ // units.
+ // A value of -1 indicates "infinite" (i.e. for "electronic" books).
+ total_stock: Integer;
+
+ // Number of units of the product that have already been sold.
+ total_sold: Integer;
+
+ // Number of units of the product that were lost (spoiled, stolen, etc.).
+ total_lost: Integer;
+
+ // Identifies where the product is in stock.
+ address?: Location;
+
+ // Identifies when we expect the next restocking to happen.
+ next_restock?: Timestamp;
+
+ // Minimum age buyer must have (in years).
+ minimum_age?: Integer;
+ }
+ export interface LockRequest {
+ // UUID that identifies the frontend performing the lock
+ // Must be unique for the lifetime of the lock.
+ lock_uuid: string;
+
+ // How long does the frontend intend to hold the lock?
+ duration: RelativeTime;
+
+ // How many units should be locked?
+ quantity: Integer;
+ }
+
+ export interface PostOrderRequest {
+ // The order must at least contain the minimal
+ // order detail, but can override all.
+ order: Order;
+
+ // If set, the backend will then set the refund deadline to the current
+ // time plus the specified delay. If it's not set, refunds will not be
+ // possible.
+ refund_delay?: RelativeTime;
+
+ // Specifies the payment target preferred by the client. Can be used
+ // to select among the various (active) wire methods supported by the instance.
+ payment_target?: string;
+
+ // Specifies that some products are to be included in the
+ // order from the inventory. For these inventory management
+ // is performed (so the products must be in stock) and
+ // details are completed from the product data of the backend.
+ inventory_products?: MinimalInventoryProduct[];
+
+ // Specifies a lock identifier that was used to
+ // lock a product in the inventory. Only useful if
+ // inventory_products is set. Used in case a frontend
+ // reserved quantities of the individual products while
+ // the shopping cart was being built. Multiple UUIDs can
+ // be used in case different UUIDs were used for different
+ // products (i.e. in case the user started with multiple
+ // shopping sessions that were combined during checkout).
+ lock_uuids?: string[];
+
+ // Should a token for claiming the order be generated?
+ // False can make sense if the ORDER_ID is sufficiently
+ // high entropy to prevent adversarial claims (like it is
+ // if the backend auto-generates one). Default is 'true'.
+ create_token?: boolean;
+
+ // OTP device ID to associate with the order.
+ // This parameter is optional.
+ otp_id?: string;
+ }
+
+ export type Order = MinimalOrderDetail & Partial<ContractTerms>;
+
+ export interface MinimalOrderDetail {
+ // Amount to be paid by the customer.
+ amount: AmountString;
+
+ // Short summary of the order.
+ summary: string;
+
+ // See documentation of fulfillment_url in ContractTerms.
+ // Either fulfillment_url or fulfillment_message must be specified.
+ // When creating an order, the fulfillment URL can
+ // contain ${ORDER_ID} which will be substituted with the
+ // order ID of the newly created order.
+ fulfillment_url?: string;
+
+ // See documentation of fulfillment_message in ContractTerms.
+ // Either fulfillment_url or fulfillment_message must be specified.
+ fulfillment_message?: string;
+ }
+
+ export interface MinimalInventoryProduct {
+ // Which product is requested (here mandatory!).
+ product_id: string;
+
+ // How many units of the product are requested.
+ quantity: Integer;
+ }
+
+ export interface PostOrderResponse {
+ // Order ID of the response that was just created.
+ order_id: string;
+
+ // Token that authorizes the wallet to claim the order.
+ // Provided only if "create_token" was set to 'true'
+ // in the request.
+ token?: ClaimToken;
+ }
+ export interface OutOfStockResponse {
+ // Product ID of an out-of-stock item.
+ product_id: string;
+
+ // Requested quantity.
+ requested_quantity: Integer;
+
+ // Available quantity (must be below requested_quantity).
+ available_quantity: Integer;
+
+ // When do we expect the product to be again in stock?
+ // Optional, not given if unknown.
+ restock_expected?: Timestamp;
+ }
+
+ export interface OrderHistory {
+ // Timestamp-sorted array of all orders matching the query.
+ // The order of the sorting depends on the sign of delta.
+ orders: OrderHistoryEntry[];
+ }
+ export interface OrderHistoryEntry {
+ // Order ID of the transaction related to this entry.
+ order_id: string;
+
+ // Row ID of the order in the database.
+ row_id: number;
+
+ // When the order was created.
+ timestamp: Timestamp;
+
+ // The amount of money the order is for.
+ amount: AmountString;
+
+ // The summary of the order.
+ summary: string;
+
+ // Whether some part of the order is refundable,
+ // that is the refund deadline has not yet expired
+ // and the total amount refunded so far is below
+ // the value of the original transaction.
+ refundable: boolean;
+
+ // Whether the order has been paid or not.
+ paid: boolean;
+ }
+
+ export type MerchantOrderStatusResponse =
+ | CheckPaymentPaidResponse
+ | CheckPaymentClaimedResponse
+ | CheckPaymentUnpaidResponse;
+ export interface CheckPaymentPaidResponse {
+ // The customer paid for this contract.
+ order_status: "paid";
+
+ // Was the payment refunded (even partially)?
+ refunded: boolean;
+
+ // True if there are any approved refunds that the wallet has
+ // not yet obtained.
+ refund_pending: boolean;
+
+ // Did the exchange wire us the funds?
+ wired: boolean;
+
+ // Total amount the exchange deposited into our bank account
+ // for this contract, excluding fees.
+ deposit_total: AmountString;
+
+ // Numeric error code indicating errors the exchange
+ // encountered tracking the wire transfer for this purchase (before
+ // we even got to specific coin issues).
+ // 0 if there were no issues.
+ exchange_code: number;
+
+ // HTTP status code returned by the exchange when we asked for
+ // information to track the wire transfer for this purchase.
+ // 0 if there were no issues.
+ exchange_http_status: number;
+
+ // Total amount that was refunded, 0 if refunded is false.
+ refund_amount: AmountString;
+
+ // Contract terms.
+ contract_terms: ContractTerms;
+
+ // The wire transfer status from the exchange for this order if
+ // available, otherwise empty array.
+ wire_details: TransactionWireTransfer[];
+
+ // Reports about trouble obtaining wire transfer details,
+ // empty array if no trouble were encountered.
+ wire_reports: TransactionWireReport[];
+
+ // The refund details for this order. One entry per
+ // refunded coin; empty array if there are no refunds.
+ refund_details: RefundDetails[];
+
+ // Status URL, can be used as a redirect target for the browser
+ // to show the order QR code / trigger the wallet.
+ order_status_url: string;
+ }
+ export interface CheckPaymentClaimedResponse {
+ // A wallet claimed the order, but did not yet pay for the contract.
+ order_status: "claimed";
+
+ // Contract terms.
+ contract_terms: ContractTerms;
+ }
+ export interface CheckPaymentUnpaidResponse {
+ // The order was neither claimed nor paid.
+ order_status: "unpaid";
+
+ // URI that the wallet must process to complete the payment.
+ taler_pay_uri: string;
+
+ // when was the order created
+ creation_time: Timestamp;
+
+ // Order summary text.
+ summary: string;
+
+ // Total amount of the order (to be paid by the customer).
+ total_amount: AmountString;
+
+ // Alternative order ID which was paid for already in the same session.
+ // Only given if the same product was purchased before in the same session.
+ already_paid_order_id?: string;
+
+ // Fulfillment URL of an already paid order. Only given if under this
+ // session an already paid order with a fulfillment URL exists.
+ already_paid_fulfillment_url?: string;
+
+ // Status URL, can be used as a redirect target for the browser
+ // to show the order QR code / trigger the wallet.
+ order_status_url: string;
+
+ // We do we NOT return the contract terms here because they may not
+ // exist in case the wallet did not yet claim them.
+ }
+ export interface RefundDetails {
+ // Reason given for the refund.
+ reason: string;
+
+ // Set to true if a refund is still available for the wallet for this payment.
+ pending: boolean;
+
+ // When was the refund approved.
+ timestamp: Timestamp;
+
+ // Total amount that was refunded (minus a refund fee).
+ amount: AmountString;
+ }
+ export interface TransactionWireTransfer {
+ // Responsible exchange.
+ exchange_url: string;
+
+ // 32-byte wire transfer identifier.
+ wtid: Base32;
+
+ // Execution time of the wire transfer.
+ execution_time: Timestamp;
+
+ // Total amount that has been wire transferred
+ // to the merchant.
+ amount: AmountString;
+
+ // Was this transfer confirmed by the merchant via the
+ // POST /transfers API, or is it merely claimed by the exchange?
+ confirmed: boolean;
+ }
+ export interface TransactionWireReport {
+ // Numerical error code.
+ code: number;
+
+ // Human-readable error description.
+ hint: string;
+
+ // Numerical error code from the exchange.
+ exchange_code: number;
+
+ // HTTP status code received from the exchange.
+ exchange_http_status: number;
+
+ // Public key of the coin for which we got the exchange error.
+ coin_pub: CoinPublicKey;
+ }
+
+ export interface ForgetRequest {
+ // Array of valid JSON paths to forgettable fields in the order's
+ // contract terms.
+ fields: string[];
+ }
+
+ export interface RefundRequest {
+ // Amount to be refunded.
+ refund: AmountString;
+
+ // Human-readable refund justification.
+ reason: string;
+ }
+ export interface MerchantRefundResponse {
+ // URL (handled by the backend) that the wallet should access to
+ // trigger refund processing.
+ // taler://refund/...
+ taler_refund_uri: string;
+
+ // Contract hash that a client may need to authenticate an
+ // HTTP request to obtain the above URI in a wallet-friendly way.
+ h_contract: HashCode;
+ }
+
+ export interface TransferInformation {
+ // How much was wired to the merchant (minus fees).
+ credit_amount: AmountString;
+
+ // Raw wire transfer identifier identifying the wire transfer (a base32-encoded value).
+ wtid: WireTransferIdentifierRawP;
+
+ // Target account that received the wire transfer.
+ payto_uri: PaytoString;
+
+ // Base URL of the exchange that made the wire transfer.
+ exchange_url: string;
+ }
+
+ export interface TransferList {
+ // List of all the transfers that fit the filter that we know.
+ transfers: TransferDetails[];
+ }
+ export interface TransferDetails {
+ // How much was wired to the merchant (minus fees).
+ credit_amount: AmountString;
+
+ // Raw wire transfer identifier identifying the wire transfer (a base32-encoded value).
+ wtid: WireTransferIdentifierRawP;
+
+ // Target account that received the wire transfer.
+ payto_uri: PaytoString;
+
+ // Base URL of the exchange that made the wire transfer.
+ exchange_url: string;
+
+ // Serial number identifying the transfer in the merchant backend.
+ // Used for filtering via offset.
+ transfer_serial_id: number;
+
+ // Time of the execution of the wire transfer by the exchange, according to the exchange
+ // Only provided if we did get an answer from the exchange.
+ execution_time?: Timestamp;
+
+ // True if we checked the exchange's answer and are happy with it.
+ // False if we have an answer and are unhappy, missing if we
+ // do not have an answer from the exchange.
+ verified?: boolean;
+
+ // True if the merchant uses the POST /transfers API to confirm
+ // that this wire transfer took place (and it is thus not
+ // something merely claimed by the exchange).
+ confirmed?: boolean;
+ }
+
+ export interface OtpDeviceAddDetails {
+ // Device ID to use.
+ otp_device_id: string;
+
+ // Human-readable description for the device.
+ otp_device_description: string;
+
+ // A key encoded with RFC 3548 Base32.
+ // IMPORTANT: This is not using the typical
+ // Taler base32-crockford encoding.
+ // Instead it uses the RFC 3548 encoding to
+ // be compatible with the TOTP standard.
+ otp_key: string;
+
+ // Algorithm for computing the POS confirmation.
+ // "NONE" or 0: No algorithm (no pos confirmation will be generated)
+ // "TOTP_WITHOUT_PRICE" or 1: Without amounts (typical OTP device)
+ // "TOTP_WITH_PRICE" or 2: With amounts (special-purpose OTP device)
+ // The "String" variants are supported @since protocol **v7**.
+ otp_algorithm: Integer | string;
+
+ // Counter for counter-based OTP devices.
+ otp_ctr?: Integer;
+ }
+
+ export interface OtpDevicePatchDetails {
+ // Human-readable description for the device.
+ otp_device_description: string;
+
+ // A key encoded with RFC 3548 Base32.
+ // IMPORTANT: This is not using the typical
+ // Taler base32-crockford encoding.
+ // Instead it uses the RFC 3548 encoding to
+ // be compatible with the TOTP standard.
+ otp_key: string;
+
+ // Algorithm for computing the POS confirmation.
+ otp_algorithm: Integer;
+
+ // Counter for counter-based OTP devices.
+ otp_ctr?: Integer;
+ }
+
+ export interface OtpDeviceSummaryResponse {
+ // Array of devices that are present in our backend.
+ otp_devices: OtpDeviceEntry[];
+ }
+ export interface OtpDeviceEntry {
+ // Device identifier.
+ otp_device_id: string;
+
+ // Human-readable description for the device.
+ device_description: string;
+ }
+
+ export interface OtpDeviceDetails {
+ // Human-readable description for the device.
+ device_description: string;
+
+ // Algorithm for computing the POS confirmation.
+ //
+ // Currently, the following numbers are defined:
+ // 0: None
+ // 1: TOTP without price
+ // 2: TOTP with price
+ otp_algorithm: Integer;
+
+ // Counter for counter-based OTP devices.
+ otp_ctr?: Integer;
+
+ // Current time for time-based OTP devices.
+ // Will match the faketime argument of the
+ // query if one was present, otherwise the current
+ // time at the backend.
+ //
+ // Available since protocol **v10**.
+ otp_timestamp: Integer;
+
+ // Current OTP confirmation string of the device.
+ // Matches exactly the string that would be returned
+ // as part of a payment confirmation for the given
+ // amount and time (so may contain multiple OTP codes).
+ //
+ // If the otp_algorithm is time-based, the code is
+ // returned for the current time, or for the faketime
+ // if a TIMESTAMP query argument was provided by the client.
+ //
+ // When using OTP with counters, the counter is **NOT**
+ // increased merely because this endpoint created
+ // an OTP code (this is a GET request, after all!).
+ //
+ // If the otp_algorithm requires an amount, the
+ // amount argument must be specified in the
+ // query, otherwise the otp_code is not
+ // generated.
+ //
+ // This field is *optional* in the response, as it is
+ // only provided if we could compute it based on the
+ // otp_algorithm and matching client query arguments.
+ //
+ // Available since protocol **v10**.
+ otp_code?: string;
+ }
+ export interface TemplateAddDetails {
+ // Template ID to use.
+ template_id: string;
+
+ // Human-readable description for the template.
+ template_description: string;
+
+ // OTP device ID.
+ // This parameter is optional.
+ otp_id?: string;
+
+ // Additional information in a separate template.
+ template_contract: TemplateContractDetails;
+
+ // Key-value pairs matching a subset of the
+ // fields from template_contract that are
+ // user-editable defaults for this template.
+ // Since protocol **v13**.
+ editable_defaults?: TemplateContractDetailsDefaults;
+
+ // Required currency for payments. Useful if no
+ // amount is specified in the template_contract
+ // but the user should be required to pay in a
+ // particular currency anyway. Merchant backends
+ // may reject requests if the template_contract
+ // or editable_defaults do
+ // specify an amount in a different currency.
+ // This parameter is optional.
+ // Since protocol **v13**.
+ required_currency?: string;
+ }
+ export interface TemplateContractDetails {
+ // Human-readable summary for the template.
+ summary?: string;
+
+ // Required currency for payments to the template.
+ // The user may specify any amount, but it must be
+ // in this currency.
+ // This parameter is optional and should not be present
+ // if "amount" is given.
+ currency?: string;
+
+ // The price is imposed by the merchant and cannot be changed by the customer.
+ // This parameter is optional.
+ amount?: AmountString;
+
+ // Minimum age buyer must have (in years). Default is 0.
+ minimum_age: Integer;
+
+ // The time the customer need to pay before his order will be deleted.
+ // It is deleted if the customer did not pay and if the duration is over.
+ pay_duration: RelativeTime;
+ }
+
+ export interface TemplateContractDetailsDefaults {
+ summary?: string;
+
+ currency?: string;
+
+ /**
+ * Amount *or* a plain currency string.
+ */
+ amount?: string;
+
+ minimum_age?: Integer;
+ }
+
+ export interface TemplatePatchDetails {
+ // Human-readable description for the template.
+ template_description: string;
+
+ // OTP device ID.
+ // This parameter is optional.
+ otp_id?: string;
+
+ // Additional information in a separate template.
+ template_contract: TemplateContractDetails;
+
+ // Key-value pairs matching a subset of the
+ // fields from template_contract that are
+ // user-editable defaults for this template.
+ // Since protocol **v13**.
+ editable_defaults?: TemplateContractDetailsDefaults;
+
+ // Required currency for payments. Useful if no
+ // amount is specified in the template_contract
+ // but the user should be required to pay in a
+ // particular currency anyway. Merchant backends
+ // may reject requests if the template_contract
+ // or editable_defaults do
+ // specify an amount in a different currency.
+ // This parameter is optional.
+ // Since protocol **v13**.
+ required_currency?: string;
+ }
+
+ export interface TemplateSummaryResponse {
+ // List of templates that are present in our backend.
+ templates: TemplateEntry[];
+ }
+
+ export interface TemplateEntry {
+ // Template identifier, as found in the template.
+ template_id: string;
+
+ // Human-readable description for the template.
+ template_description: string;
+ }
+
+ export interface WalletTemplateDetails {
+ // Hard-coded information about the contrac terms
+ // for this template.
+ template_contract: TemplateContractDetails;
+
+ // Key-value pairs matching a subset of the
+ // fields from template_contract that are
+ // user-editable defaults for this template.
+ // Since protocol **v13**.
+ editable_defaults?: TemplateContractDetailsDefaults;
+
+ // Required currency for payments. Useful if no
+ // amount is specified in the template_contract
+ // but the user should be required to pay in a
+ // particular currency anyway. Merchant backends
+ // may reject requests if the template_contract
+ // or editable_defaults do
+ // specify an amount in a different currency.
+ // This parameter is optional.
+ // Since protocol **v13**.
+ required_currency?: string;
+ }
+
+ export interface TemplateDetails {
+ // Human-readable description for the template.
+ template_description: string;
+
+ // OTP device ID.
+ // This parameter is optional.
+ otp_id?: string;
+
+ // Additional information in a separate template.
+ template_contract: TemplateContractDetails;
+
+ // Key-value pairs matching a subset of the
+ // fields from template_contract that are
+ // user-editable defaults for this template.
+ // Since protocol **v13**.
+ editable_defaults?: TemplateContractDetailsDefaults;
+
+ // Required currency for payments. Useful if no
+ // amount is specified in the template_contract
+ // but the user should be required to pay in a
+ // particular currency anyway. Merchant backends
+ // may reject requests if the template_contract
+ // or editable_defaults do
+ // specify an amount in a different currency.
+ // This parameter is optional.
+ // Since protocol **v13**.
+ required_currency?: string;
+ }
+ export interface UsingTemplateDetails {
+ // Summary of the template
+ summary?: string;
+
+ // The amount entered by the customer.
+ amount?: AmountString;
+ }
+
+ export interface WebhookAddDetails {
+ // Webhook ID to use.
+ webhook_id: string;
+
+ // The event of the webhook: why the webhook is used.
+ event_type: string;
+
+ // URL of the webhook where the customer will be redirected.
+ url: string;
+
+ // Method used by the webhook
+ http_method: string;
+
+ // Header template of the webhook
+ header_template?: string;
+
+ // Body template by the webhook
+ body_template?: string;
+ }
+
+ export interface WebhookPatchDetails {
+ // The event of the webhook: why the webhook is used.
+ event_type: string;
+
+ // URL of the webhook where the customer will be redirected.
+ url: string;
+
+ // Method used by the webhook
+ http_method: string;
+
+ // Header template of the webhook
+ header_template?: string;
+
+ // Body template by the webhook
+ body_template?: string;
+ }
+
+ export interface WebhookSummaryResponse {
+ // Return webhooks that are present in our backend.
+ webhooks: WebhookEntry[];
+ }
+
+ export interface WebhookEntry {
+ // Webhook identifier, as found in the webhook.
+ webhook_id: string;
+
+ // The event of the webhook: why the webhook is used.
+ event_type: string;
+ }
+
+ export interface WebhookDetails {
+ // The event of the webhook: why the webhook is used.
+ event_type: string;
+
+ // URL of the webhook where the customer will be redirected.
+ url: string;
+
+ // Method used by the webhook
+ http_method: string;
+
+ // Header template of the webhook
+ header_template?: string;
+
+ // Body template by the webhook
+ body_template?: string;
+ }
+
+ export interface TokenFamilyCreateRequest {
+ // Identifier for the token family consisting of unreserved characters
+ // according to RFC 3986.
+ slug: string;
+
+ // Human-readable name for the token family.
+ name: string;
+
+ // Human-readable description for the token family.
+ description: string;
+
+ // Optional map from IETF BCP 47 language tags to localized descriptions.
+ description_i18n?: { [lang_tag: string]: string };
+
+ // Start time of the token family's validity period.
+ // If not specified, merchant backend will use the current time.
+ valid_after?: Timestamp;
+
+ // End time of the token family's validity period.
+ valid_before: Timestamp;
+
+ // Validity duration of an issued token.
+ duration: RelativeTime;
+
+ // Kind of the token family.
+ kind: TokenFamilyKind;
+ }
+
+ export enum TokenFamilyKind {
+ Discount = "discount",
+ Subscription = "subscription",
+ }
+
+ export interface TokenFamilyUpdateRequest {
+ // Human-readable name for the token family.
+ name: string;
+
+ // Human-readable description for the token family.
+ description: string;
+
+ // Optional map from IETF BCP 47 language tags to localized descriptions.
+ description_i18n: { [lang_tag: string]: string };
+
+ // Start time of the token family's validity period.
+ valid_after: Timestamp;
+
+ // End time of the token family's validity period.
+ valid_before: Timestamp;
+
+ // Validity duration of an issued token.
+ duration: RelativeTime;
+ }
+
+ export interface TokenFamiliesList {
+ // All configured token families of this instance.
+ token_families: TokenFamilySummary[];
+ }
+
+ export interface TokenFamilySummary {
+ // Identifier for the token family consisting of unreserved characters
+ // according to RFC 3986.
+ slug: string;
+
+ // Human-readable name for the token family.
+ name: string;
+
+ // Start time of the token family's validity period.
+ valid_after: Timestamp;
+
+ // End time of the token family's validity period.
+ valid_before: Timestamp;
+
+ // Kind of the token family.
+ kind: TokenFamilyKind;
+ }
+
+ export interface TokenFamilyDetails {
+ // Identifier for the token family consisting of unreserved characters
+ // according to RFC 3986.
+ slug: string;
+
+ // Human-readable name for the token family.
+ name: string;
+
+ // Human-readable description for the token family.
+ description: string;
+
+ // Optional map from IETF BCP 47 language tags to localized descriptions.
+ description_i18n?: { [lang_tag: string]: string };
+
+ // Start time of the token family's validity period.
+ valid_after: Timestamp;
+
+ // End time of the token family's validity period.
+ valid_before: Timestamp;
+
+ // Validity duration of an issued token.
+ duration: RelativeTime;
+
+ // Kind of the token family.
+ kind: TokenFamilyKind;
+
+ // How many tokens have been issued for this family.
+ issued: Integer;
+
+ // How many tokens have been redeemed for this family.
+ redeemed: Integer;
+ }
+ export interface ContractTerms {
+ // Human-readable description of the whole purchase.
+ summary: string;
+
+ // Map from IETF BCP 47 language tags to localized summaries.
+ summary_i18n?: { [lang_tag: string]: string };
+
+ // Unique, free-form identifier for the proposal.
+ // Must be unique within a merchant instance.
+ // For merchants that do not store proposals in their DB
+ // before the customer paid for them, the order_id can be used
+ // by the frontend to restore a proposal from the information
+ // encoded in it (such as a short product identifier and timestamp).
+ order_id: string;
+
+ // Total price for the transaction.
+ // The exchange will subtract deposit fees from that amount
+ // before transferring it to the merchant.
+ amount: AmountString;
+
+ // URL where the same contract could be ordered again (if
+ // available). Returned also at the public order endpoint
+ // for people other than the actual buyer (hence public,
+ // in case order IDs are guessable).
+ public_reorder_url?: string;
+
+ // URL that will show that the order was successful after
+ // it has been paid for. Optional. When POSTing to the
+ // merchant, the placeholder "${ORDER_ID}" will be
+ // replaced with the actual order ID (useful if the
+ // order ID is generated server-side and needs to be
+ // in the URL).
+ // Note that this placeholder can only be used once.
+ // Either fulfillment_url or fulfillment_message must be specified.
+ fulfillment_url?: string;
+
+ // Message shown to the customer after paying for the order.
+ // Either fulfillment_url or fulfillment_message must be specified.
+ fulfillment_message?: string;
+
+ // Map from IETF BCP 47 language tags to localized fulfillment
+ // messages.
+ fulfillment_message_i18n?: { [lang_tag: string]: string };
+
+ // Maximum total deposit fee accepted by the merchant for this contract.
+ // Overrides defaults of the merchant instance.
+ max_fee: AmountString;
+
+ // List of products that are part of the purchase (see Product).
+ products: Product[];
+
+ // Time when this contract was generated.
+ timestamp: Timestamp;
+
+ // After this deadline has passed, no refunds will be accepted.
+ refund_deadline: Timestamp;
+
+ // After this deadline, the merchant won't accept payments for the contract.
+ pay_deadline: Timestamp;
+
+ // Transfer deadline for the exchange. Must be in the
+ // deposit permissions of coins used to pay for this order.
+ wire_transfer_deadline: Timestamp;
+
+ // Merchant's public key used to sign this proposal; this information
+ // is typically added by the backend. Note that this can be an ephemeral key.
+ merchant_pub: EddsaPublicKey;
+
+ // Base URL of the (public!) merchant backend API.
+ // Must be an absolute URL that ends with a slash.
+ merchant_base_url: string;
+
+ // More info about the merchant, see below.
+ merchant: Merchant;
+
+ // The hash of the merchant instance's wire details.
+ h_wire: HashCode;
+
+ // Wire transfer method identifier for the wire method associated with h_wire.
+ // The wallet may only select exchanges via a matching auditor if the
+ // exchange also supports this wire method.
+ // The wire transfer fees must be added based on this wire transfer method.
+ wire_method: string;
+
+ // Exchanges that the merchant accepts even if it does not accept any auditors that audit them.
+ exchanges: Exchange[];
+
+ // Delivery location for (all!) products.
+ delivery_location?: Location;
+
+ // Time indicating when the order should be delivered.
+ // May be overwritten by individual products.
+ delivery_date?: Timestamp;
+
+ // Nonce generated by the wallet and echoed by the merchant
+ // in this field when the proposal is generated.
+ nonce: string;
+
+ // Specifies for how long the wallet should try to get an
+ // automatic refund for the purchase. If this field is
+ // present, the wallet should wait for a few seconds after
+ // the purchase and then automatically attempt to obtain
+ // a refund. The wallet should probe until "delay"
+ // after the payment was successful (i.e. via long polling
+ // or via explicit requests with exponential back-off).
+ //
+ // In particular, if the wallet is offline
+ // at that time, it MUST repeat the request until it gets
+ // one response from the merchant after the delay has expired.
+ // If the refund is granted, the wallet MUST automatically
+ // recover the payment. This is used in case a merchant
+ // knows that it might be unable to satisfy the contract and
+ // desires for the wallet to attempt to get the refund without any
+ // customer interaction. Note that it is NOT an error if the
+ // merchant does not grant a refund.
+ auto_refund?: RelativeTime;
+
+ // Extra data that is only interpreted by the merchant frontend.
+ // Useful when the merchant needs to store extra information on a
+ // contract without storing it separately in their database.
+ extra?: any;
+
+ // Minimum age the buyer must have (in years). Default is 0.
+ // This value is at least as large as the maximum over all
+ // minimum age requirements of the products in this contract.
+ // It might also be set independent of any product, due to
+ // legal requirements.
+ minimum_age?: Integer;
+ }
+
+ export interface Product {
+ // Merchant-internal identifier for the product.
+ product_id?: string;
+
+ // Human-readable product description.
+ description: string;
+
+ // Map from IETF BCP 47 language tags to localized descriptions.
+ description_i18n?: { [lang_tag: string]: string };
+
+ // The number of units of the product to deliver to the customer.
+ quantity?: Integer;
+
+ // Unit in which the product is measured (liters, kilograms, packages, etc.).
+ unit?: string;
+
+ // The price of the product; this is the total price for quantity times unit of this product.
+ price?: AmountString;
+
+ // An optional base64-encoded product image.
+ image?: ImageDataUrl;
+
+ // A list of taxes paid by the merchant for this product. Can be empty.
+ taxes?: Tax[];
+
+ // Time indicating when this product should be delivered.
+ delivery_date?: Timestamp;
+ }
+
+ export interface Tax {
+ // The name of the tax.
+ name: string;
+
+ // Amount paid in tax.
+ tax: AmountString;
+ }
+ export interface Merchant {
+ // The merchant's legal name of business.
+ name: string;
+
+ // Label for a location with the business address of the merchant.
+ email?: string;
+
+ // Label for a location with the business address of the merchant.
+ website?: string;
+
+ // An optional base64-encoded product image.
+ logo?: ImageDataUrl;
+
+ // Label for a location with the business address of the merchant.
+ address?: Location;
+
+ // Label for a location that denotes the jurisdiction for disputes.
+ // Some of the typical fields for a location (such as a street address) may be absent.
+ jurisdiction?: Location;
+ }
+ // Delivery location, loosely modeled as a subset of
+ // ISO20022's PostalAddress25.
+ export interface Location {
+ // Nation with its own government.
+ country?: string;
+
+ // Identifies a subdivision of a country such as state, region, county.
+ country_subdivision?: string;
+
+ // Identifies a subdivision within a country sub-division.
+ district?: string;
+
+ // Name of a built-up area, with defined boundaries, and a local government.
+ town?: string;
+
+ // Specific location name within the town.
+ town_location?: string;
+
+ // Identifier consisting of a group of letters and/or numbers that
+ // is added to a postal address to assist the sorting of mail.
+ post_code?: string;
+
+ // Name of a street or thoroughfare.
+ street?: string;
+
+ // Name of the building or house.
+ building_name?: string;
+
+ // Number that identifies the position of a building on a street.
+ building_number?: string;
+
+ // Free-form address lines, should not exceed 7 elements.
+ address_lines?: string[];
+ }
+ interface Auditor {
+ // Official name.
+ name: string;
+
+ // Auditor's public key.
+ auditor_pub: EddsaPublicKey;
+
+ // Base URL of the auditor.
+ url: string;
+ }
+ export interface Exchange {
+ // The exchange's base URL.
+ url: string;
+
+ // How much would the merchant like to use this exchange.
+ // The wallet should use a suitable exchange with high
+ // priority. The following priority values are used, but
+ // it should be noted that they are NOT in any way normative.
+ //
+ // 0: likely it will not work (recently seen with account
+ // restriction that would be bad for this merchant)
+ // 512: merchant does not know, might be down (merchant
+ // did not yet get /wire response).
+ // 1024: good choice (recently confirmed working)
+ priority: Integer;
+
+ // Master public key of the exchange.
+ master_pub: EddsaPublicKey;
+ }
+
+ export interface MerchantReserveCreateConfirmation {
+ // Public key identifying the reserve.
+ reserve_pub: EddsaPublicKey;
+
+ // Wire accounts of the exchange where to transfer the funds.
+ accounts: ExchangeWireAccount[];
+ }
+
+ export interface TemplateEditableDetails {
+ // Human-readable summary for the template.
+ summary?: string;
+
+ // Required currency for payments to the template.
+ // The user may specify any amount, but it must be
+ // in this currency.
+ // This parameter is optional and should not be present
+ // if "amount" is given.
+ currency?: string;
+
+ // The price is imposed by the merchant and cannot be changed by the customer.
+ // This parameter is optional.
+ amount?: AmountString;
+ }
+
+ export interface MerchantTemplateContractDetails {
+ // Human-readable summary for the template.
+ summary?: string;
+
+ // The price is imposed by the merchant and cannot be changed by the customer.
+ // This parameter is optional.
+ amount?: string;
+
+ // Minimum age buyer must have (in years). Default is 0.
+ minimum_age: number;
+
+ // The time the customer need to pay before his order will be deleted.
+ // It is deleted if the customer did not pay and if the duration is over.
+ pay_duration: TalerProtocolDuration;
+ }
+
+ export interface MerchantTemplateAddDetails {
+ // Template ID to use.
+ template_id: string;
+
+ // Human-readable description for the template.
+ template_description: string;
+
+ // A base64-encoded image selected by the merchant.
+ // This parameter is optional.
+ // We are not sure about it.
+ image?: string;
+
+ editable_defaults?: TemplateEditableDetails;
+
+ // Additional information in a separate template.
+ template_contract: MerchantTemplateContractDetails;
+
+ // OTP device ID.
+ // This parameter is optional.
+ otp_id?: string;
+ }
+}
+
+export namespace ChallengerApi {
+ export interface ChallengerTermsOfServiceResponse {
+ // Name of the service
+ name: "challenger";
+
+ // libtool-style representation of the Challenger protocol version, see
+ // https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning
+ // The format is "current:revision:age".
+ version: string;
+
+ // URN of the implementation (needed to interpret 'revision' in version).
+ // @since v0, may become mandatory in the future.
+ implementation?: string;
+ }
+
+ export interface ChallengeSetupResponse {
+ // Nonce to use when constructing /authorize endpoint.
+ nonce: string;
+ }
+
+ export interface Restriction {
+ regex?: string;
+ hint?: string;
+ hint_i18n?: InternationalizedString;
+ }
+
+ export interface ChallengeStatus {
+ // Object; map of keys (names of the fields of the address
+ // to be entered by the user) to objects with a "regex" (string)
+ // containing an extended Posix regular expression for allowed
+ // address field values, and a "hint"/"hint_i18n" giving a
+ // human-readable explanation to display if the value entered
+ // by the user does not match the regex. Keys that are not mapped
+ // to such an object have no restriction on the value provided by
+ // the user. See "ADDRESS_RESTRICTIONS" in the challenger configuration.
+ restrictions: Record<string, Restriction> | undefined;
+
+ // indicates if the given address cannot be changed anymore, the
+ // form should be read-only if set to true.
+ fix_address: boolean;
+
+ // form values from the previous submission if available, details depend
+ // on the ADDRESS_TYPE, should be used to pre-populate the form
+ last_address: Record<string, string> | undefined;
+
+ // number of times the address can still be changed, may or may not be
+ // shown to the user
+ changes_left: Integer;
+ }
+
+ export interface ChallengeCreateResponse {
+ // how many more attempts are allowed, might be shown to the user,
+ // highlighting might be appropriate for low values such as 1 or 2 (the
+ // form will never be used if the value is zero)
+ attempts_left: Integer;
+
+ // the address that is being validated, might be shown or not
+ address: Object;
+
+ // true if we just retransmitted the challenge, false if we sent a
+ // challenge recently and thus refused to transmit it again this time;
+ // might make a useful hint to the user
+ transmitted: boolean;
+
+ // timestamp explaining when we would re-transmit the challenge the next
+ // time (at the earliest) if requested by the user
+ next_tx_time: string;
+ }
+
+ export interface InvalidPinResponse {
+ // numeric Taler error code, should be shown to indicate the error
+ // compactly for reporting to developers
+ ec?: number;
+
+ // human-readable Taler error code, should be shown for the user to
+ // understand the error
+ hint: string;
+
+ // how many times is the user still allowed to change the address;
+ // if 0, the user should not be shown a link to jump to the
+ // address entry form
+ addresses_left: Integer;
+
+ // how many times might the PIN still be retransmitted
+ pin_transmissions_left: Integer;
+
+ // how many times might the user still try entering the PIN code
+ auth_attempts_left: Integer;
+
+ // if true, the PIN was not even evaluated as the user previously
+ // exhausted the number of attempts
+ exhausted: boolean;
+
+ // if true, the PIN was not even evaluated as no challenge was ever
+ // issued (the user must have skipped the step of providing their
+ // address first!)
+ no_challenge: boolean;
+ }
+
+ export interface ChallengerAuthResponse {
+ // Token used to authenticate access in /info.
+ access_token: string;
+
+ // Type of the access token.
+ token_type: "Bearer";
+
+ // Amount of time that an access token is valid (in seconds).
+ expires_in: Integer;
+ }
+
+ export interface ChallengerInfoResponse {
+ // Unique ID of the record within Challenger
+ // (identifies the rowid of the token).
+ id: Integer;
+
+ // Address that was validated.
+ // Key-value pairs, details depend on the
+ // address_type.
+ address: Object;
+
+ // Type of the address.
+ address_type: string;
+
+ // How long do we consider the address to be
+ // valid for this user.
+ expires: Timestamp;
+ }
+}
diff --git a/packages/taler-util/src/http-client/utils.ts b/packages/taler-util/src/http-client/utils.ts
new file mode 100644
index 000000000..bf186ce46
--- /dev/null
+++ b/packages/taler-util/src/http-client/utils.ts
@@ -0,0 +1,116 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022-2024 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 { base64FromArrayBuffer } from "../base64.js";
+import { encodeCrock, getRandomBytes, stringToBytes } from "../taler-crypto.js";
+import { AccessToken, LongPollParams, PaginationParams } from "./types.js";
+
+/**
+ * Helper function to generate the "Authorization" HTTP header.
+ */
+export function makeBasicAuthHeader(
+ username: string,
+ password: string,
+): string {
+ const auth = `${username}:${password}`;
+ const authEncoded: string = base64FromArrayBuffer(stringToBytes(auth));
+ return `Basic ${authEncoded}`;
+}
+
+/**
+ * rfc8959
+ * @param token
+ * @returns
+ */
+export function makeBearerTokenAuthHeader(token: AccessToken): string {
+ return `Bearer ${token}`;
+}
+
+/**
+ * https://bugs.gnunet.org/view.php?id=7949
+ */
+export function addPaginationParams(url: URL, pagination?: PaginationParams) {
+ if (!pagination) return;
+ if (pagination.offset) {
+ url.searchParams.set("start", pagination.offset);
+ }
+ const order = !pagination || pagination.order === "asc" ? 1 : -1;
+ const limit =
+ !pagination || !pagination.limit || pagination.limit === 0
+ ? 5
+ : Math.abs(pagination.limit);
+ //always send delta
+ url.searchParams.set("delta", String(order * limit));
+}
+
+export function addMerchantPaginationParams(
+ url: URL,
+ pagination?: PaginationParams,
+) {
+ if (!pagination) return;
+ if (pagination.offset) {
+ url.searchParams.set("offset", pagination.offset);
+ }
+ const order = !pagination || pagination.order === "asc" ? 1 : -1;
+ const limit =
+ !pagination || !pagination.limit || pagination.limit === 0
+ ? 5
+ : Math.abs(pagination.limit);
+ //always send delta
+ url.searchParams.set("limit", String(order * limit));
+}
+
+export function addLongPollingParam(url: URL, param?: LongPollParams) {
+ if (!param) return;
+ if (param.timeoutMs) {
+ url.searchParams.set("long_poll_ms", String(param.timeoutMs));
+ }
+}
+
+export interface CacheEvictor<T> {
+ notifySuccess: (op: T) => Promise<void>;
+}
+
+export const nullEvictor: CacheEvictor<unknown> = {
+ notifySuccess: () => Promise.resolve(),
+};
+
+export class IdempotencyRetry {
+ public readonly uid: string;
+ public readonly timesLeft: number;
+ public readonly maxTries: number;
+
+ private constructor(timesLeft: number, maxTimesLeft: number) {
+ this.timesLeft = timesLeft;
+ this.maxTries = maxTimesLeft;
+ this.uid = encodeCrock(getRandomBytes(32))
+ }
+
+ static tryFiveTimes() {
+ return new IdempotencyRetry(5, 5)
+ }
+
+ next(): IdempotencyRetry | undefined {
+ const left = this.timesLeft -1
+ if (left <= 0) {
+ return undefined
+ }
+ return new IdempotencyRetry(left, this.maxTries);
+ }
+}
diff --git a/packages/taler-util/src/http-common.ts b/packages/taler-util/src/http-common.ts
new file mode 100644
index 000000000..d8cd36287
--- /dev/null
+++ b/packages/taler-util/src/http-common.ts
@@ -0,0 +1,526 @@
+/*
+ This file is part of GNU Taler
+ (C) 2023 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/>
+
+ SPDX-License-Identifier: AGPL3.0-or-later
+*/
+
+import type { CancellationToken } from "./CancellationToken.js";
+import { Codec } from "./codec.js";
+import { j2s } from "./helpers.js";
+import {
+ TalerError,
+ base64FromArrayBuffer,
+ makeErrorDetail,
+ stringToBytes,
+} from "./index.js";
+import { Logger } from "./logging.js";
+import { TalerErrorCode } from "./taler-error-codes.js";
+import { AbsoluteTime, Duration } from "./time.js";
+import { TalerErrorDetail } from "./wallet-types.js";
+
+const textEncoder = new TextEncoder();
+
+const logger = new Logger("http.ts");
+
+/**
+ * An HTTP response that is returned by all request methods of this library.
+ */
+export interface HttpResponse {
+ requestUrl: string;
+ requestMethod: string;
+ status: number;
+ headers: Headers;
+ json(): Promise<any>;
+ text(): Promise<string>;
+ bytes(): Promise<ArrayBuffer>;
+}
+
+export const DEFAULT_REQUEST_TIMEOUT_MS = 60000;
+
+export interface HttpRequestOptions {
+ method?: "POST" | "PATCH" | "PUT" | "GET" | "DELETE";
+ headers?: { [name: string]: string | undefined };
+
+ /**
+ * Timeout after which the request should be aborted.
+ */
+ timeout?: Duration;
+
+ /**
+ * Cancellation token that should abort the request when
+ * cancelled.
+ */
+ cancellationToken?: CancellationToken;
+
+ body?: string | ArrayBuffer | object;
+
+ /**
+ * How to handle redirects.
+ * Same semantics as WHATWG fetch.
+ */
+ redirect?: "follow" | "error" | "manual";
+}
+
+/**
+ * Headers, roughly modeled after the fetch API's headers object.
+ */
+export class Headers {
+ private headerMap = new Map<string, string>();
+
+ get(name: string): string | null {
+ const r = this.headerMap.get(name.toLowerCase());
+ if (r) {
+ return r;
+ }
+ return null;
+ }
+
+ set(name: string, value: string): void {
+ const normalizedName = name.toLowerCase();
+ const existing = this.headerMap.get(normalizedName);
+ if (existing !== undefined) {
+ this.headerMap.set(normalizedName, existing + "," + value);
+ } else {
+ this.headerMap.set(normalizedName, value);
+ }
+ }
+
+ toJSON(): any {
+ const m: Record<string, string> = {};
+ this.headerMap.forEach((v, k) => (m[k] = v));
+ return m;
+ }
+}
+
+/**
+ * Interface for the HTTP request library used by the wallet.
+ *
+ * The request library is bundled into an interface to make mocking and
+ * request tunneling easy.
+ */
+export interface HttpRequestLibrary {
+ /**
+ * Make an HTTP POST request with a JSON body.
+ */
+ fetch(url: string, opt?: HttpRequestOptions): Promise<HttpResponse>;
+}
+
+type TalerErrorResponse = {
+ code: number;
+} & unknown;
+
+type ResponseOrError<T> =
+ | { isError: false; response: T }
+ | { isError: true; talerErrorResponse: TalerErrorResponse };
+
+/**
+ * Read Taler error details from an HTTP response.
+ */
+export async function readTalerErrorResponse(
+ httpResponse: HttpResponse,
+): Promise<TalerErrorDetail> {
+ const contentType = httpResponse.headers.get("content-type");
+ if (contentType !== "application/json") {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ {
+ requestUrl: httpResponse.requestUrl,
+ requestMethod: httpResponse.requestMethod,
+ httpStatusCode: httpResponse.status,
+ contentType: contentType || "<null>",
+ },
+ "Error response did not even contain JSON. The request URL might be wrong or the service might be unavailable.",
+ );
+ }
+ let errJson;
+ try {
+ errJson = await httpResponse.json();
+ } catch (e: any) {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ {
+ requestUrl: httpResponse.requestUrl,
+ requestMethod: httpResponse.requestMethod,
+ httpStatusCode: httpResponse.status,
+ validationError: e.toString(),
+ },
+ "Couldn't parse JSON format from error response",
+ );
+ }
+
+ const talerErrorCode = errJson.code;
+ if (typeof talerErrorCode !== "number") {
+ logger.warn(
+ `malformed error response (status ${httpResponse.status}): ${j2s(
+ errJson,
+ )}`,
+ );
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ {
+ requestUrl: httpResponse.requestUrl,
+ requestMethod: httpResponse.requestMethod,
+ httpStatusCode: httpResponse.status,
+ },
+ "Error response did not contain error code",
+ );
+ }
+ return errJson;
+}
+
+export async function readUnexpectedResponseDetails(
+ httpResponse: HttpResponse,
+): Promise<TalerErrorDetail> {
+ let errJson;
+ try {
+ errJson = await httpResponse.json();
+ } catch (e: any) {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ {
+ requestUrl: httpResponse.requestUrl,
+ requestMethod: httpResponse.requestMethod,
+ httpStatusCode: httpResponse.status,
+ validationError: e.toString(),
+ },
+ "Couldn't parse JSON format from error response",
+ );
+ }
+ const talerErrorCode = errJson.code;
+ if (typeof talerErrorCode !== "number") {
+ return makeErrorDetail(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ {
+ requestUrl: httpResponse.requestUrl,
+ requestMethod: httpResponse.requestMethod,
+ httpStatusCode: httpResponse.status,
+ },
+ "Error response did not contain error code",
+ );
+ }
+ return makeErrorDetail(
+ TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
+ {
+ requestUrl: httpResponse.requestUrl,
+ requestMethod: httpResponse.requestMethod,
+ httpStatusCode: httpResponse.status,
+ errorResponse: errJson,
+ },
+ `Unexpected HTTP status (${httpResponse.status}) in response`,
+ );
+}
+
+export async function readSuccessResponseJsonOrErrorCode<T>(
+ httpResponse: HttpResponse,
+ codec: Codec<T>,
+): Promise<ResponseOrError<T>> {
+ if (!(httpResponse.status >= 200 && httpResponse.status < 300)) {
+ return {
+ isError: true,
+ talerErrorResponse: await readTalerErrorResponse(httpResponse),
+ };
+ }
+ let respJson;
+ try {
+ respJson = await httpResponse.json();
+ } catch (e: any) {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ {
+ requestUrl: httpResponse.requestUrl,
+ requestMethod: httpResponse.requestMethod,
+ httpStatusCode: httpResponse.status,
+ validationError: e.toString(),
+ },
+ "Couldn't parse JSON format from response",
+ );
+ }
+ let parsedResponse: T;
+ try {
+ parsedResponse = codec.decode(respJson);
+ } catch (e: any) {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ {
+ requestUrl: httpResponse.requestUrl,
+ requestMethod: httpResponse.requestMethod,
+ httpStatusCode: httpResponse.status,
+ validationError: e.toString(),
+ },
+ "Response invalid",
+ );
+ }
+ return {
+ isError: false,
+ response: parsedResponse,
+ };
+}
+
+export async function readResponseJsonOrErrorCode<T>(
+ httpResponse: HttpResponse,
+ codec: Codec<T>,
+): Promise<{ isError: boolean; response: T }> {
+ let respJson;
+ try {
+ respJson = await httpResponse.json();
+ } catch (e: any) {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ {
+ requestUrl: httpResponse.requestUrl,
+ requestMethod: httpResponse.requestMethod,
+ httpStatusCode: httpResponse.status,
+ validationError: e.toString(),
+ },
+ "Couldn't parse JSON format from response",
+ );
+ }
+ let parsedResponse: T;
+ try {
+ parsedResponse = codec.decode(respJson);
+ } catch (e: any) {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ {
+ requestUrl: httpResponse.requestUrl,
+ requestMethod: httpResponse.requestMethod,
+ httpStatusCode: httpResponse.status,
+ validationError: e.toString(),
+ },
+ "Response invalid",
+ );
+ }
+ return {
+ isError: !(httpResponse.status >= 200 && httpResponse.status < 300),
+ response: parsedResponse,
+ };
+}
+
+
+type HttpErrorDetails = {
+ requestUrl: string;
+ requestMethod: string;
+ httpStatusCode: number;
+};
+
+export function getHttpResponseErrorDetails(
+ httpResponse: HttpResponse,
+): HttpErrorDetails {
+ return {
+ requestUrl: httpResponse.requestUrl,
+ requestMethod: httpResponse.requestMethod,
+ httpStatusCode: httpResponse.status,
+ };
+}
+
+export function throwUnexpectedRequestError(
+ httpResponse: HttpResponse,
+ talerErrorResponse: TalerErrorResponse,
+): never {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
+ {
+ requestUrl: httpResponse.requestUrl,
+ requestMethod: httpResponse.requestMethod,
+ httpStatusCode: httpResponse.status,
+ errorResponse: talerErrorResponse,
+ },
+ `Unexpected HTTP status ${httpResponse.status} in response`,
+ );
+}
+
+export async function readSuccessResponseJsonOrThrow<T>(
+ httpResponse: HttpResponse,
+ codec: Codec<T>,
+): Promise<T> {
+ const r = await readSuccessResponseJsonOrErrorCode(httpResponse, codec);
+ if (!r.isError) {
+ return r.response;
+ }
+ throwUnexpectedRequestError(httpResponse, r.talerErrorResponse);
+}
+
+export async function expectSuccessResponseOrThrow<T>(
+ httpResponse: HttpResponse,
+): Promise<void> {
+ if (httpResponse.status >= 200 && httpResponse.status <= 299) {
+ return;
+ }
+ const errResp = await readTalerErrorResponse(httpResponse);
+ throwUnexpectedRequestError(httpResponse, errResp);
+}
+
+export async function readSuccessResponseTextOrErrorCode<T>(
+ httpResponse: HttpResponse,
+): Promise<ResponseOrError<string>> {
+ if (!(httpResponse.status >= 200 && httpResponse.status < 300)) {
+ let errJson;
+ try {
+ errJson = await httpResponse.json();
+ } catch (e: any) {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ {
+ requestUrl: httpResponse.requestUrl,
+ requestMethod: httpResponse.requestMethod,
+ httpStatusCode: httpResponse.status,
+ validationError: e.toString(),
+ },
+ "Couldn't parse JSON format from error response",
+ );
+ }
+
+ const talerErrorCode = errJson.code;
+ if (typeof talerErrorCode !== "number") {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ {
+ httpStatusCode: httpResponse.status,
+ requestUrl: httpResponse.requestUrl,
+ requestMethod: httpResponse.requestMethod,
+ },
+ "Error response did not contain error code",
+ );
+ }
+ return {
+ isError: true,
+ talerErrorResponse: errJson,
+ };
+ }
+ const respJson = await httpResponse.text();
+ return {
+ isError: false,
+ response: respJson,
+ };
+}
+
+export async function checkSuccessResponseOrThrow(
+ httpResponse: HttpResponse,
+): Promise<void> {
+ if (!(httpResponse.status >= 200 && httpResponse.status < 300)) {
+ let errJson;
+ try {
+ errJson = await httpResponse.json();
+ } catch (e: any) {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ {
+ requestUrl: httpResponse.requestUrl,
+ requestMethod: httpResponse.requestMethod,
+ httpStatusCode: httpResponse.status,
+ validationError: e.toString(),
+ },
+ "Couldn't parse JSON format from error response",
+ );
+ }
+
+ const talerErrorCode = errJson.code;
+ if (typeof talerErrorCode !== "number") {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ {
+ httpStatusCode: httpResponse.status,
+ requestUrl: httpResponse.requestUrl,
+ requestMethod: httpResponse.requestMethod,
+ },
+ "Error response did not contain error code",
+ );
+ }
+ throwUnexpectedRequestError(httpResponse, errJson);
+ }
+}
+
+export async function readSuccessResponseTextOrThrow<T>(
+ httpResponse: HttpResponse,
+): Promise<string> {
+ const r = await readSuccessResponseTextOrErrorCode(httpResponse);
+ if (!r.isError) {
+ return r.response;
+ }
+ throwUnexpectedRequestError(httpResponse, r.talerErrorResponse);
+}
+
+/**
+ * Get the timestamp at which the response's content is considered expired.
+ */
+export function getExpiry(
+ httpResponse: HttpResponse,
+ opt: { minDuration?: Duration },
+): AbsoluteTime {
+ const expiryDateMs = new Date(
+ httpResponse.headers.get("expiry") ?? "",
+ ).getTime();
+ let t: AbsoluteTime;
+ if (Number.isNaN(expiryDateMs)) {
+ t = AbsoluteTime.now();
+ } else {
+ t = AbsoluteTime.fromMilliseconds(expiryDateMs);
+ }
+ if (opt.minDuration) {
+ const t2 = AbsoluteTime.addDuration(AbsoluteTime.now(), opt.minDuration);
+ return AbsoluteTime.max(t, t2);
+ }
+ return t;
+}
+
+export interface HttpLibArgs {
+ enableThrottling?: boolean;
+ /**
+ * Only allow HTTPS connections, not plain http.
+ */
+ requireTls?: boolean;
+ printAsCurl?: boolean;
+}
+
+export function encodeBody(body: any): ArrayBuffer {
+ if (body == null) {
+ return new ArrayBuffer(0);
+ }
+ if (typeof body === "string") {
+ return textEncoder.encode(body).buffer;
+ } else if (ArrayBuffer.isView(body)) {
+ return body.buffer;
+ } else if (body instanceof ArrayBuffer) {
+ return body;
+ } else if (typeof body === "object") {
+ return textEncoder.encode(JSON.stringify(body)).buffer;
+ }
+ throw new TypeError("unsupported request body type");
+}
+
+export function getDefaultHeaders(method: string): Record<string, string> {
+ const headers: Record<string, string> = {};
+
+ if (method === "POST" || method === "PUT" || method === "PATCH") {
+ // Default to JSON if we have a body
+ headers["Content-Type"] = "application/json";
+ }
+
+ headers["Accept"] = "application/json";
+
+ return headers;
+}
+
+/**
+ * Helper function to generate the "Authorization" HTTP header.
+ */
+export function makeBasicAuthHeader(
+ username: string,
+ password: string,
+): string {
+ const auth = `${username}:${password}`;
+ const authEncoded: string = base64FromArrayBuffer(stringToBytes(auth));
+ return `Basic ${authEncoded}`;
+}
diff --git a/packages/taler-util/src/http-impl.missing.ts b/packages/taler-util/src/http-impl.missing.ts
new file mode 100644
index 000000000..6ae6b93ec
--- /dev/null
+++ b/packages/taler-util/src/http-impl.missing.ts
@@ -0,0 +1,38 @@
+/*
+ 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 <http://www.gnu.org/licenses/>
+
+ SPDX-License-Identifier: AGPL3.0-or-later
+*/
+
+/**
+ * Imports.
+ */
+import {
+ HttpRequestLibrary,
+ HttpRequestOptions,
+ HttpResponse,
+} from "./http.js";
+
+/**
+ * Implementation of the HTTP request library interface for node.
+ */
+export class HttpLibImpl implements HttpRequestLibrary {
+ fetch(
+ url: string,
+ opt?: HttpRequestOptions | undefined,
+ ): Promise<HttpResponse> {
+ throw new Error("Method not implemented.");
+ }
+}
diff --git a/packages/taler-util/src/http-impl.node.d.ts b/packages/taler-util/src/http-impl.node.d.ts
new file mode 100644
index 000000000..771dd991c
--- /dev/null
+++ b/packages/taler-util/src/http-impl.node.d.ts
@@ -0,0 +1,25 @@
+import { HttpLibArgs } from "./http-common.js";
+import {
+ HttpRequestLibrary,
+ HttpRequestOptions,
+ HttpResponse,
+} from "./http.js";
+/**
+ * Implementation of the HTTP request library interface for node.
+ */
+export declare class HttpLibImpl implements HttpRequestLibrary {
+ private throttle;
+ private throttlingEnabled;
+ constructor(args?: HttpLibArgs);
+ /**
+ * Set whether requests should be throttled.
+ */
+ setThrottling(enabled: boolean): void;
+ fetch(url: string, opt?: HttpRequestOptions): Promise<HttpResponse>;
+ get(url: string, opt?: HttpRequestOptions): Promise<HttpResponse>;
+ postJson(
+ url: string,
+ body: any,
+ opt?: HttpRequestOptions,
+ ): Promise<HttpResponse>;
+}
diff --git a/packages/taler-util/src/http-impl.node.ts b/packages/taler-util/src/http-impl.node.ts
new file mode 100644
index 000000000..45a12c258
--- /dev/null
+++ b/packages/taler-util/src/http-impl.node.ts
@@ -0,0 +1,324 @@
+/*
+ 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 <http://www.gnu.org/licenses/>
+
+ SPDX-License-Identifier: AGPL3.0-or-later
+*/
+
+/**
+ * Imports.
+ */
+import type { FollowOptions, RedirectableRequest } from "follow-redirects";
+import followRedirects from "follow-redirects";
+import type { ClientRequest, IncomingMessage } from "node:http";
+import { RequestOptions } from "node:http";
+import * as net from "node:net";
+import { TalerError } from "./errors.js";
+import { HttpLibArgs, encodeBody, getDefaultHeaders } from "./http-common.js";
+import {
+ DEFAULT_REQUEST_TIMEOUT_MS,
+ Headers,
+ HttpRequestLibrary,
+ HttpRequestOptions,
+ HttpResponse,
+} from "./http.js";
+import {
+ Logger,
+ RequestThrottler,
+ TalerErrorCode,
+ URL,
+ typedArrayConcat,
+} from "./index.js";
+
+const http = followRedirects.http;
+const https = followRedirects.https;
+
+// Work around a node v20.0.0, v20.1.0, and v20.2.0 bug. The issue was fixed
+// in v20.3.0.
+// https://github.com/nodejs/node/issues/47822#issuecomment-1564708870
+// Safe to remove once support for Node v20 is dropped.
+if (
+ // check for `node` in case we want to use this in "exotic" JS envs
+ process.versions.node &&
+ process.versions.node.match(/20\.[0-2]\.0/)
+) {
+ //@ts-ignore
+ net.setDefaultAutoSelectFamily(false);
+}
+
+const logger = new Logger("http-impl.node.ts");
+
+const textDecoder = new TextDecoder();
+let SHOW_CURL_HTTP_REQUEST = false;
+export function setPrintHttpRequestAsCurl(b: boolean) {
+ SHOW_CURL_HTTP_REQUEST = b;
+}
+
+/**
+ * Implementation of the HTTP request library interface for node.
+ */
+export class HttpLibImpl implements HttpRequestLibrary {
+ private throttle = new RequestThrottler();
+ private throttlingEnabled = true;
+ private requireTls = false;
+
+ constructor(args?: HttpLibArgs) {
+ this.throttlingEnabled = args?.enableThrottling ?? true;
+ this.requireTls = args?.requireTls ?? false;
+ }
+
+ /**
+ * Set whether requests should be throttled.
+ */
+ setThrottling(enabled: boolean): void {
+ this.throttlingEnabled = enabled;
+ }
+
+ async fetch(url: string, opt?: HttpRequestOptions): Promise<HttpResponse> {
+ const method = opt?.method?.toUpperCase() ?? "GET";
+
+ logger.trace(`Requesting ${method} ${url}`);
+
+ const parsedUrl = new URL(url);
+ if (this.throttlingEnabled && this.throttle.applyThrottle(url)) {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED,
+ {
+ requestMethod: method,
+ requestUrl: url,
+ throttleStats: this.throttle.getThrottleStats(url),
+ },
+ `request to origin ${parsedUrl.origin} was throttled`,
+ );
+ }
+ if (this.requireTls && parsedUrl.protocol !== "https:") {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_NETWORK_ERROR,
+ {
+ requestMethod: method,
+ requestUrl: url,
+ },
+ `request to ${parsedUrl.origin} is not possible with protocol ${parsedUrl.protocol}`,
+ );
+ }
+ let timeoutMs: number | undefined;
+ if (typeof opt?.timeout?.d_ms === "number") {
+ timeoutMs = opt.timeout.d_ms;
+ } else {
+ timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS;
+ }
+
+ const requestHeadersMap = getDefaultHeaders(method);
+ if (opt?.headers) {
+ Object.entries(opt?.headers).forEach(([key, value]) => {
+ if (value === undefined) return;
+ requestHeadersMap[key] = value;
+ });
+ }
+ logger.trace(`request timeout ${timeoutMs} ms`);
+
+ let reqBody: ArrayBuffer | undefined;
+
+ if (
+ opt?.method == "POST" ||
+ opt?.method == "PATCH" ||
+ opt?.method == "PUT"
+ ) {
+ reqBody = encodeBody(opt.body);
+ }
+
+ let path = parsedUrl.pathname;
+ if (parsedUrl.search != null) {
+ path += parsedUrl.search;
+ }
+
+ let protocol: string;
+ if (parsedUrl.protocol === "https:") {
+ protocol = "https:";
+ } else if (parsedUrl.protocol === "http:") {
+ protocol = "http:";
+ } else {
+ throw Error(`unsupported protocol (${parsedUrl.protocol})`);
+ }
+
+ const options: RequestOptions & FollowOptions<RequestOptions> = {
+ protocol,
+ port: parsedUrl.port,
+ host: parsedUrl.hostname,
+ method: method,
+ path,
+ headers: requestHeadersMap,
+ timeout: timeoutMs,
+ followRedirects: opt?.redirect !== "manual",
+ };
+
+ const chunks: Uint8Array[] = [];
+
+ if (SHOW_CURL_HTTP_REQUEST) {
+ const payload =
+ !reqBody || reqBody.byteLength === 0
+ ? undefined
+ : textDecoder.decode(reqBody);
+ const headers = Object.entries(requestHeadersMap).reduce(
+ (prev, [key, value]) => {
+ return `${prev} -H "${key}: ${value}"`;
+ },
+ "",
+ );
+ function ifUndefined<T>(arg: string, v: undefined | T): string {
+ if (v === undefined) return "";
+ return arg + " '" + String(v) + "'";
+ }
+ console.log(
+ `curl -X ${options.method} "${parsedUrl.href}" ${headers} ${ifUndefined(
+ "-d",
+ payload,
+ )}`,
+ );
+ }
+
+ let timeoutHandle: NodeJS.Timer | undefined = undefined;
+ let cancelCancelledHandler: (() => void) | undefined = undefined;
+
+ const doCleanup = () => {
+ if (timeoutHandle != null) {
+ clearTimeout(timeoutHandle);
+ }
+ if (cancelCancelledHandler) {
+ cancelCancelledHandler();
+ }
+ };
+
+ return new Promise((resolve, reject) => {
+ const handler = (res: IncomingMessage) => {
+ res.on("data", (d) => {
+ chunks.push(d);
+ });
+ res.on("end", () => {
+ const headers: Headers = new Headers();
+ for (const [k, v] of Object.entries(res.headers)) {
+ if (!v) {
+ continue;
+ }
+ if (typeof v === "string") {
+ headers.set(k, v);
+ } else {
+ headers.set(k, v.join(", "));
+ }
+ }
+ const data = typedArrayConcat(chunks);
+ const resp: HttpResponse = {
+ requestMethod: method,
+ requestUrl: parsedUrl.href,
+ status: res.statusCode || 0,
+ headers,
+ async bytes() {
+ return data;
+ },
+ json() {
+ const text = textDecoder.decode(data);
+ return JSON.parse(text);
+ },
+ async text() {
+ const text = textDecoder.decode(data);
+ return text;
+ },
+ };
+ doCleanup();
+ resolve(resp);
+ });
+ res.on("error", (e) => {
+ const code = "code" in e ? e.code : "unknown";
+ const err = TalerError.fromDetail(
+ TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
+ {
+ requestUrl: url,
+ requestMethod: method,
+ httpStatusCode: 0,
+ },
+ `Error in HTTP response handler: ${code}`,
+ );
+ doCleanup();
+ reject(err);
+ });
+ };
+
+ let req: RedirectableRequest<ClientRequest, IncomingMessage>;
+ if (options.protocol === "http:") {
+ req = http.request(options, handler);
+ } else if (options.protocol === "https:") {
+ req = https.request(options, handler);
+ } else {
+ throw new Error(`unsupported protocol ${options.protocol}`);
+ }
+
+ if (timeoutMs != null) {
+ timeoutHandle = setTimeout(() => {
+ logger.info(`request to ${url} timed out`);
+ const err = TalerError.fromDetail(
+ TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
+ {
+ requestUrl: url,
+ requestMethod: method,
+ httpStatusCode: 0,
+ },
+ `Request timed out after ${timeoutMs} ms`,
+ );
+ timeoutHandle = undefined;
+ req.destroy();
+ doCleanup();
+ reject(err);
+ req.destroy();
+ }, timeoutMs);
+ }
+
+ if (opt?.cancellationToken) {
+ cancelCancelledHandler = opt.cancellationToken.onCancelled(() => {
+ const err = TalerError.fromDetail(
+ TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
+ {
+ requestUrl: url,
+ requestMethod: method,
+ httpStatusCode: 0,
+ },
+ `Request cancelled`,
+ );
+ req.destroy();
+ doCleanup();
+ reject(err);
+ });
+ }
+
+ req.on("error", (e: Error) => {
+ const code = "code" in e ? e.code : "unknown";
+ const err = TalerError.fromDetail(
+ TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
+ {
+ requestUrl: url,
+ requestMethod: method,
+ httpStatusCode: 0,
+ },
+ `Error in HTTP request: ${code}`,
+ );
+ doCleanup();
+ reject(err);
+ });
+
+ if (reqBody) {
+ req.write(new Uint8Array(reqBody));
+ }
+ req.end();
+ });
+ }
+}
diff --git a/packages/taler-util/src/http-impl.qtart.ts b/packages/taler-util/src/http-impl.qtart.ts
new file mode 100644
index 000000000..b4e4ebbe7
--- /dev/null
+++ b/packages/taler-util/src/http-impl.qtart.ts
@@ -0,0 +1,211 @@
+/*
+ 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 <http://www.gnu.org/licenses/>
+
+ SPDX-License-Identifier: AGPL3.0-or-later
+*/
+
+/**
+ * Imports.
+ */
+import { Logger, openPromise } from "@gnu-taler/taler-util";
+import { TalerError } from "./errors.js";
+import { HttpLibArgs, encodeBody, getDefaultHeaders } from "./http-common.js";
+import {
+ Headers,
+ HttpRequestLibrary,
+ HttpRequestOptions,
+ HttpResponse,
+} from "./http.js";
+import { RequestThrottler, TalerErrorCode, URL } from "./index.js";
+import { QjsHttpResp, qjsOs } from "./qtart.js";
+
+const logger = new Logger("http-impl.qtart.ts");
+
+const textDecoder = new TextDecoder();
+
+export class RequestTimeoutError extends Error {
+ public constructor() {
+ super("Request timed out");
+ Object.setPrototypeOf(this, RequestTimeoutError.prototype);
+ }
+}
+
+export class RequestCancelledError extends Error {
+ public constructor() {
+ super("Request cancelled");
+ Object.setPrototypeOf(this, RequestCancelledError.prototype);
+ }
+}
+
+/**
+ * Implementation of the HTTP request library interface for node.
+ */
+export class HttpLibImpl implements HttpRequestLibrary {
+ private throttle = new RequestThrottler();
+ private throttlingEnabled = true;
+ private requireTls = false;
+
+ constructor(args?: HttpLibArgs) {
+ this.throttlingEnabled = args?.enableThrottling ?? true;
+ this.requireTls = args?.requireTls ?? false;
+ }
+
+ /**
+ * Set whether requests should be throttled.
+ */
+ setThrottling(enabled: boolean): void {
+ this.throttlingEnabled = enabled;
+ }
+
+ async fetch(url: string, opt?: HttpRequestOptions): Promise<HttpResponse> {
+ const method = (opt?.method ?? "GET").toUpperCase();
+
+ logger.trace(`Requesting ${method} ${url}`);
+
+ const parsedUrl = new URL(url);
+ if (this.throttlingEnabled && this.throttle.applyThrottle(url)) {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED,
+ {
+ requestMethod: method,
+ requestUrl: url,
+ throttleStats: this.throttle.getThrottleStats(url),
+ },
+ `request to origin ${parsedUrl.origin} was throttled`,
+ );
+ }
+ if (this.requireTls && parsedUrl.protocol !== "https:") {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_NETWORK_ERROR,
+ {
+ requestMethod: method,
+ requestUrl: url,
+ },
+ `request to ${parsedUrl.origin} is not possible with protocol ${parsedUrl.protocol}`,
+ );
+ }
+
+ let data: ArrayBuffer | undefined = undefined;
+ const requestHeadersMap = getDefaultHeaders(method);
+ if (opt?.headers) {
+ Object.entries(opt?.headers).forEach(([key, value]) => {
+ if (value === undefined) return;
+ requestHeadersMap[key] = value
+ })
+ }
+ let headersList: string[] = [];
+ for (let headerName of Object.keys(requestHeadersMap)) {
+ headersList.push(`${headerName}: ${requestHeadersMap[headerName]}`);
+ }
+ if (method === "POST") {
+ data = encodeBody(opt?.body);
+ }
+
+ const cancelPromCap = openPromise<QjsHttpResp>();
+
+ // Just like WHATWG fetch(), the qjs http client doesn't
+ // really support cancellation, so cancellation here just
+ // means that the result is ignored!
+ const fetchProm = qjsOs.fetchHttp(url, {
+ method,
+ data,
+ headers: headersList,
+ });
+
+ let timeoutHandle: any = undefined;
+ let cancelCancelledHandler: (() => void) | undefined = undefined;
+
+ if (opt?.timeout && opt.timeout.d_ms !== "forever") {
+ timeoutHandle = setTimeout(() => {
+ cancelPromCap.reject(new RequestTimeoutError());
+ }, opt.timeout.d_ms);
+ }
+
+ if (opt?.cancellationToken) {
+ cancelCancelledHandler = opt.cancellationToken.onCancelled(() => {
+ cancelPromCap.reject(new RequestCancelledError());
+ });
+ }
+
+ let res: QjsHttpResp;
+ try {
+ res = await Promise.race([fetchProm, cancelPromCap.promise]);
+ } catch (e) {
+ if (e instanceof RequestCancelledError) {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
+ {
+ requestUrl: url,
+ requestMethod: method,
+ httpStatusCode: 0,
+ },
+ `Request cancelled`,
+ );
+ }
+ if (e instanceof RequestTimeoutError) {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
+ {
+ requestUrl: url,
+ requestMethod: method,
+ httpStatusCode: 0,
+ },
+ `Request timed out`,
+ );
+ }
+ throw e;
+ }
+
+ if (timeoutHandle != null) {
+ clearTimeout(timeoutHandle);
+ }
+
+ if (cancelCancelledHandler != null) {
+ cancelCancelledHandler();
+ }
+
+ const headers: Headers = new Headers();
+
+ if (res.headers) {
+ for (const headerStr of res.headers) {
+ const splitPos = headerStr.indexOf(":");
+ if (splitPos < 0) {
+ continue;
+ }
+ const headerName = headerStr.slice(0, splitPos).trim().toLowerCase();
+ const headerValue = headerStr.slice(splitPos + 1).trim();
+ headers.set(headerName, headerValue);
+ }
+ }
+
+ return {
+ requestMethod: method,
+ headers,
+ async bytes() {
+ return res.data;
+ },
+ json() {
+ const text = textDecoder.decode(res.data);
+ return JSON.parse(text);
+ },
+ async text() {
+ const text = textDecoder.decode(res.data);
+ return text;
+ },
+ requestUrl: url,
+ status: res.status,
+ };
+ }
+}
diff --git a/packages/taler-util/src/http.ts b/packages/taler-util/src/http.ts
new file mode 100644
index 000000000..8bf10d0e2
--- /dev/null
+++ b/packages/taler-util/src/http.ts
@@ -0,0 +1,37 @@
+/*
+ This file is part of TALER
+ (C) 2016 GNUnet e.V.
+
+ 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.
+
+ 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
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Helpers for doing XMLHttpRequest-s that are based on ES6 promises.
+ * Allows for easy mocking for test cases.
+ *
+ * The API is inspired by the HTML5 fetch API.
+ */
+
+/**
+ * Imports
+ */
+
+import * as impl from "#http-impl";
+import * as common from "./http-common.js";
+
+export * from "./http-common.js";
+
+export function createPlatformHttpLib(
+ args?: common.HttpLibArgs,
+): common.HttpRequestLibrary {
+ return new impl.HttpLibImpl(args);
+}
diff --git a/packages/taler-util/src/i18n.ts b/packages/taler-util/src/i18n.ts
index 001735325..f43f543ea 100644
--- a/packages/taler-util/src/i18n.ts
+++ b/packages/taler-util/src/i18n.ts
@@ -10,7 +10,7 @@ export let jed: any = undefined;
* Set up jed library for internationalization,
* based on browser language settings.
*/
-export function setupI18n(lang: string, strings: { [s: string]: any }): any {
+export function setupI18n(lang: string, strings: { [s: string]: any }): void {
lang = lang.replace("_", "-");
if (!strings[lang]) {
@@ -28,10 +28,13 @@ export function internalSetStrings(langStrings: any): void {
jed = new jedLib.Jed(langStrings);
}
+declare const __translated: unique symbol;
+export type TranslatedString = string & { [__translated]: true };
+
/**
* Convert template strings to a msgid
*/
-function toI18nString(stringSeq: ReadonlyArray<string>): string {
+function toI18nString(stringSeq: ReadonlyArray<string>): TranslatedString {
let s = "";
for (let i = 0; i < stringSeq.length; i++) {
s += stringSeq[i];
@@ -39,7 +42,7 @@ function toI18nString(stringSeq: ReadonlyArray<string>): string {
s += `%${i + 1}$s`;
}
}
- return s;
+ return s as TranslatedString;
}
/**
@@ -48,7 +51,7 @@ function toI18nString(stringSeq: ReadonlyArray<string>): string {
export function singular(
stringSeq: TemplateStringsArray,
...values: any[]
-): string {
+): TranslatedString {
const s = toI18nString(stringSeq);
const tr = jed
.translate(s)
@@ -63,10 +66,10 @@ export function singular(
export function translate(
stringSeq: TemplateStringsArray,
...values: any[]
-): any[] {
+): TranslatedString[] {
const s = toI18nString(stringSeq);
if (!s) return [];
- const translation: string = jed.ngettext(s, s, 1);
+ const translation: TranslatedString = jed.ngettext(s, s, 1);
return replacePlaceholderWithValues(translation, values);
}
@@ -83,7 +86,7 @@ export function Translate({
const c = [].concat(children);
const s = stringifyArray(c);
if (!s) return [];
- const translation: string = jed.ngettext(s, s, 1);
+ const translation: TranslatedString = jed.ngettext(s, s, 1);
if (debug) {
console.log("looking for ", s, "got", translation);
}
@@ -104,12 +107,12 @@ export function getJsonI18n<K extends string>(
export function getTranslatedArray(array: Array<any>) {
const s = stringifyArray(array);
- const translation: string = jed.ngettext(s, s, 1);
+ const translation: TranslatedString = jed.ngettext(s, s, 1);
return replacePlaceholderWithValues(translation, array);
}
function replacePlaceholderWithValues(
- translation: string,
+ translation: TranslatedString,
childArray: Array<any>,
): Array<any> {
const tr = translation.split(/%(\d+)\$s/);
diff --git a/packages/taler-util/src/iban.test.ts b/packages/taler-util/src/iban.test.ts
new file mode 100644
index 000000000..a00e3b50a
--- /dev/null
+++ b/packages/taler-util/src/iban.test.ts
@@ -0,0 +1,30 @@
+/*
+ This file is part of GNU Taler
+ (C) 2023 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 test from "ava";
+import { generateIban, validateIban } from "./iban.js";
+
+test("iban validation", (t) => {
+ t.assert(validateIban("foo").type === "invalid");
+ t.assert(validateIban("NL71RABO9996666778").type === "valid");
+ t.assert(validateIban("NL71RABO9996666779").type === "invalid");
+});
+
+test("iban generation", (t) => {
+ let iban1 = generateIban("DE", 10);
+ console.log("generated IBAN", iban1);
+ t.assert(validateIban(iban1).type === "valid");
+});
diff --git a/packages/taler-util/src/iban.ts b/packages/taler-util/src/iban.ts
new file mode 100644
index 000000000..d386f90e0
--- /dev/null
+++ b/packages/taler-util/src/iban.ts
@@ -0,0 +1,296 @@
+/*
+ This file is part of GNU Taler
+ (C) 2023 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/>
+ */
+
+/**
+ * IBAN validation.
+ *
+ * Currently only validates the checksum.
+ *
+ * It does not validate:
+ * - Country-specific length
+ * - Country-specific checksums
+ *
+ * The country list is also not complete.
+ *
+ * @author Florian Dold <dold@taler.net>
+ */
+
+export type IbanValidationResult =
+ | { type: "invalid" }
+ | {
+ type: "valid";
+ normalizedIban: string;
+ };
+
+export interface IbanCountryInfo {
+ name: string;
+ isSepa?: boolean;
+ length?: number;
+}
+
+/**
+ * Incomplete list, see https://www.swift.com/resource/iban-registry-pdf
+ */
+export const ibanCountryInfoTable: Record<string, IbanCountryInfo> = {
+ AE: { name: "U.A.E." },
+ AF: { name: "Afghanistan" },
+ AL: { name: "Albania" },
+ AM: { name: "Armenia" },
+ AN: { name: "Netherlands Antilles" },
+ AR: { name: "Argentina" },
+ AT: { name: "Austria" },
+ AU: { name: "Australia" },
+ AZ: { name: "Azerbaijan" },
+ BA: { name: "Bosnia and Herzegovina" },
+ BD: { name: "Bangladesh" },
+ BE: { name: "Belgium" },
+ BG: { name: "Bulgaria" },
+ BH: { name: "Bahrain" },
+ BN: { name: "Brunei Darussalam" },
+ BO: { name: "Bolivia" },
+ BR: { name: "Brazil" },
+ BT: { name: "Bhutan" },
+ BY: { name: "Belarus" },
+ BZ: { name: "Belize" },
+ CA: { name: "Canada" },
+ CG: { name: "Congo" },
+ CH: { name: "Switzerland" },
+ CI: { name: "Cote d'Ivoire" },
+ CL: { name: "Chile" },
+ CM: { name: "Cameroon" },
+ CN: { name: "People's Republic of China" },
+ CO: { name: "Colombia" },
+ CR: { name: "Costa Rica" },
+ CS: { name: "Serbia and Montenegro" },
+ CZ: { name: "Czech Republic" },
+ DE: { name: "Germany" },
+ DK: { name: "Denmark" },
+ DO: { name: "Dominican Republic" },
+ DZ: { name: "Algeria" },
+ EC: { name: "Ecuador" },
+ EE: { name: "Estonia" },
+ EG: { name: "Egypt" },
+ ER: { name: "Eritrea" },
+ ES: { name: "Spain" },
+ ET: { name: "Ethiopia" },
+ FI: { name: "Finland" },
+ FO: { name: "Faroe Islands" },
+ FR: { name: "France" },
+ GB: { name: "United Kingdom" },
+ GD: { name: "Caribbean" },
+ GE: { name: "Georgia" },
+ GL: { name: "Greenland" },
+ GR: { name: "Greece" },
+ GT: { name: "Guatemala" },
+ HK: { name: "Hong Kong S.A.R." },
+ HN: { name: "Honduras" },
+ HR: { name: "Croatia" },
+ HT: { name: "Haiti" },
+ HU: { name: "Hungary" },
+ ID: { name: "Indonesia" },
+ IE: { name: "Ireland" },
+ IL: { name: "Israel" },
+ IN: { name: "India" },
+ IQ: { name: "Iraq" },
+ IR: { name: "Iran" },
+ IS: { name: "Iceland" },
+ IT: { name: "Italy" },
+ JM: { name: "Jamaica" },
+ JO: { name: "Jordan" },
+ JP: { name: "Japan" },
+ KE: { name: "Kenya" },
+ KG: { name: "Kyrgyzstan" },
+ KH: { name: "Cambodia" },
+ KR: { name: "South Korea" },
+ KW: { name: "Kuwait" },
+ KZ: { name: "Kazakhstan" },
+ LA: { name: "Laos" },
+ LB: { name: "Lebanon" },
+ LI: { name: "Liechtenstein" },
+ LK: { name: "Sri Lanka" },
+ LT: { name: "Lithuania" },
+ LU: { name: "Luxembourg" },
+ LV: { name: "Latvia" },
+ LY: { name: "Libya" },
+ MA: { name: "Morocco" },
+ MC: { name: "Principality of Monaco" },
+ MD: { name: "Moldava" },
+ ME: { name: "Montenegro" },
+ MK: { name: "Former Yugoslav Republic of Macedonia" },
+ ML: { name: "Mali" },
+ MM: { name: "Myanmar" },
+ MN: { name: "Mongolia" },
+ MO: { name: "Macau S.A.R." },
+ MT: { name: "Malta" },
+ MV: { name: "Maldives" },
+ MX: { name: "Mexico" },
+ MY: { name: "Malaysia" },
+ NG: { name: "Nigeria" },
+ NI: { name: "Nicaragua" },
+ NL: { name: "Netherlands" },
+ NO: { name: "Norway" },
+ NP: { name: "Nepal" },
+ NZ: { name: "New Zealand" },
+ OM: { name: "Oman" },
+ PA: { name: "Panama" },
+ PE: { name: "Peru" },
+ PH: { name: "Philippines" },
+ PK: { name: "Islamic Republic of Pakistan" },
+ PL: { name: "Poland" },
+ PR: { name: "Puerto Rico" },
+ PT: { name: "Portugal" },
+ PY: { name: "Paraguay" },
+ QA: { name: "Qatar" },
+ RE: { name: "Reunion" },
+ RO: { name: "Romania" },
+ RS: { name: "Serbia" },
+ RU: { name: "Russia" },
+ RW: { name: "Rwanda" },
+ SA: { name: "Saudi Arabia" },
+ SE: { name: "Sweden" },
+ SG: { name: "Singapore" },
+ SI: { name: "Slovenia" },
+ SK: { name: "Slovak" },
+ SN: { name: "Senegal" },
+ SO: { name: "Somalia" },
+ SR: { name: "Suriname" },
+ SV: { name: "El Salvador" },
+ SY: { name: "Syria" },
+ TH: { name: "Thailand" },
+ TJ: { name: "Tajikistan" },
+ TM: { name: "Turkmenistan" },
+ TN: { name: "Tunisia" },
+ TR: { name: "Turkey" },
+ TT: { name: "Trinidad and Tobago" },
+ TW: { name: "Taiwan" },
+ TZ: { name: "Tanzania" },
+ UA: { name: "Ukraine" },
+ US: { name: "United States" },
+ UY: { name: "Uruguay" },
+ VA: { name: "Vatican" },
+ VE: { name: "Venezuela" },
+ VN: { name: "Viet Nam" },
+ YE: { name: "Yemen" },
+ ZA: { name: "South Africa" },
+ ZW: { name: "Zimbabwe" },
+};
+
+let ccZero = "0".charCodeAt(0);
+let ccNine = "9".charCodeAt(0);
+let ccA = "A".charCodeAt(0);
+let ccZ = "Z".charCodeAt(0);
+
+/**
+ * Append a IBAN digit(s) based on a char code.
+ */
+function appendDigit(digits: number[], cc: number): boolean {
+ if (cc >= ccZero && cc <= ccNine) {
+ digits.push(cc - ccZero);
+ } else if (cc >= ccA && cc <= ccZ) {
+ const n = cc - ccA + 10;
+ digits.push(Math.floor(n / 10) % 10);
+ digits.push(n % 10);
+ } else {
+ return false;
+ }
+ return true;
+}
+
+/**
+ * Compute MOD-97-10 as per ISO/IEC 7064:2003.
+ */
+function mod97(digits: number[]): number {
+ let i = 0;
+ let modAccum = 0;
+ while (i < digits.length) {
+ let n = 0;
+ while (n < 9 && i < digits.length) {
+ modAccum = modAccum * 10 + digits[i];
+ i++;
+ n++;
+ }
+ modAccum = modAccum % 97;
+ }
+ return modAccum;
+}
+
+export function validateIban(ibanString: string): IbanValidationResult {
+ let myIban = ibanString.toLocaleUpperCase().replace(" ", "");
+ let countryCode = myIban.substring(0, 2);
+ let countryInfo = ibanCountryInfoTable[countryCode];
+
+ if (!countryInfo) {
+ return {
+ type: "invalid",
+ };
+ }
+
+ let digits: number[] = [];
+
+ for (let i = 4; i < myIban.length; i++) {
+ const cc = myIban.charCodeAt(i);
+ if (!appendDigit(digits, cc)) {
+ return {
+ type: "invalid",
+ };
+ }
+ }
+
+ for (let i = 0; i < 4; i++) {
+ if (!appendDigit(digits, ibanString.charCodeAt(i))) {
+ return {
+ type: "invalid",
+ };
+ }
+ }
+
+ const rem = mod97(digits);
+ if (rem === 1) {
+ return {
+ type: "valid",
+ normalizedIban: myIban,
+ };
+ } else {
+ return {
+ type: "invalid",
+ };
+ }
+}
+
+export function generateIban(countryCode: string, length: number): string {
+ let ibanSuffix = "";
+ let digits: number[] = [];
+
+ for (let i = 0; i < length; i++) {
+ const cc = ccZero + (Math.floor(Math.random() * 100) % 10);
+ appendDigit(digits, cc);
+ ibanSuffix += String.fromCharCode(cc);
+ }
+
+ appendDigit(digits, countryCode.charCodeAt(0));
+ appendDigit(digits, countryCode.charCodeAt(1));
+
+ // Try using "00" as check digits
+ appendDigit(digits, ccZero);
+ appendDigit(digits, ccZero);
+
+ const requiredChecksum = 98 - mod97(digits);
+
+ const checkDigit1 = Math.floor(requiredChecksum / 10) % 10;
+ const checkDigit2 = requiredChecksum % 10;
+
+ return countryCode + checkDigit1 + checkDigit2 + ibanSuffix;
+}
diff --git a/packages/taler-util/src/index.browser.ts b/packages/taler-util/src/index.browser.ts
index 3b8e194b3..ec77b10c0 100644
--- a/packages/taler-util/src/index.browser.ts
+++ b/packages/taler-util/src/index.browser.ts
@@ -19,3 +19,7 @@
import { loadBrowserPrng } from "./prng-browser.js";
loadBrowserPrng();
export * from "./index.js";
+
+// The web stuff doesn't support package.json export declarations yet,
+// so we export more stuff here than we should.
+export * from "./http-common.js";
diff --git a/packages/taler-util/src/index.node.ts b/packages/taler-util/src/index.node.ts
index bd59f320a..ba4c6cf4e 100644
--- a/packages/taler-util/src/index.node.ts
+++ b/packages/taler-util/src/index.node.ts
@@ -21,4 +21,4 @@ initNodePrng();
export * from "./index.js";
export * from "./talerconfig.js";
export * from "./globbing/minimatch.js";
-export { clk } from "./clk.js";
+export { setPrintHttpRequestAsCurl } from "./http-impl.node.js";
diff --git a/packages/taler-util/src/index.qtart.ts b/packages/taler-util/src/index.qtart.ts
new file mode 100644
index 000000000..ddb9bcfd4
--- /dev/null
+++ b/packages/taler-util/src/index.qtart.ts
@@ -0,0 +1,27 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 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 { setPRNG } from "./nacl-fast.js";
+
+setPRNG(function (x: Uint8Array, n: number) {
+ // @ts-ignore
+ const va = globalThis._tart.randomBytes(n);
+ const v = new Uint8Array(va);
+ for (let i = 0; i < n; i++) x[i] = v[i];
+ for (let i = 0; i < v.length; i++) v[i] = 0;
+});
+
+export * from "./index.js";
diff --git a/packages/taler-util/src/index.ts b/packages/taler-util/src/index.ts
index 2f674d097..9f99f2f5a 100644
--- a/packages/taler-util/src/index.ts
+++ b/packages/taler-util/src/index.ts
@@ -2,36 +2,61 @@ import { TalerErrorCode } from "./taler-error-codes.js";
export { TalerErrorCode };
+export * from "./CancellationToken.js";
+export * from "./MerchantApiClient.js";
+export { RequestThrottler } from "./RequestThrottler.js";
+export * from "./ReserveStatus.js";
+export * from "./ReserveTransaction.js";
+export { TaskThrottler } from "./TaskThrottler.js";
export * from "./amounts.js";
export * from "./backup-types.js";
+export * from "./bank-api-client.js";
+export * from "./base64.js";
+export * from "./bitcoin.js";
export * from "./codec.js";
+export * from "./contract-terms.js";
+export * from "./errors.js";
+export { fnutil } from "./fnutils.js";
export * from "./helpers.js";
-export * from "./libtool-version.js";
-export * from "./notifications.js";
-export * from "./payto.js";
-export * from "./ReserveStatus.js";
-export * from "./ReserveTransaction.js";
-export * from "./taler-types.js";
-export * from "./taleruri.js";
-export * from "./time.js";
-export * from "./transactions-types.js";
-export * from "./wallet-types.js";
+export * from "./http-client/authentication.js";
+export * from "./http-client/bank-conversion.js";
+export * from "./http-client/bank-core.js";
+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/exchange.js";
+export * from "./http-client/merchant.js";
+export * from "./http-client/officer-account.js";
+export * from "./http-client/types.js";
+export { CacheEvictor } from "./http-client/utils.js";
+export * from "./http-status-codes.js";
export * from "./i18n.js";
-export * from "./logging.js";
-export * from "./url.js";
-export { fnutil } from "./fnutils.js";
+export * from "./iban.js";
+export * from "./invariants.js";
export * from "./kdf.js";
-export * from "./taler-crypto.js";
-export * from "./http-status-codes.js";
-export * from "./bitcoin.js";
+export * from "./libeufin-api-types.js";
+export * from "./libtool-version.js";
+export * from "./logging.js";
export {
+ crypto_sign_keyPair_fromSeed,
randomBytes,
secretbox,
secretbox_open,
- crypto_sign_keyPair_fromSeed,
setPRNG,
} from "./nacl-fast.js";
-export { RequestThrottler } from "./RequestThrottler.js";
-export * from "./CancellationToken.js";
-export * from "./contract-terms.js";
-export * from "./base64.js";
+export * from "./notifications.js";
+export * from "./observability.js";
+export * from "./operation.js";
+export * from "./payto.js";
+export * from "./promises.js";
+export * from "./rfc3548.js";
+export * from "./taler-crypto.js";
+export * from "./taler-types.js";
+export * from "./taleruri.js";
+export * from "./time.js";
+export * from "./timer.js";
+export * from "./transaction-test-data.js";
+export * from "./transactions-types.js";
+export * from "./url.js";
+export * from "./wallet-types.js";
diff --git a/packages/taler-util/src/invariants.ts b/packages/taler-util/src/invariants.ts
new file mode 100644
index 000000000..c6e9b8113
--- /dev/null
+++ b/packages/taler-util/src/invariants.ts
@@ -0,0 +1,59 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019-2024 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/>
+ */
+
+/**
+ * Helpers for invariants.
+ */
+
+/**
+ * An invariant has been violated.
+ */
+export class InvariantViolatedError extends Error {
+ constructor(message?: string) {
+ super(message);
+ Object.setPrototypeOf(this, InvariantViolatedError.prototype);
+ }
+}
+
+/**
+ * Check a database invariant.
+ *
+ * A violation of this invariant means that the database is inconsistent.
+ */
+export function checkDbInvariant(b: boolean, m?: string): asserts b {
+ if (!b) {
+ if (m) {
+ throw Error(`BUG: database invariant failed (${m})`);
+ } else {
+ throw Error("BUG: database invariant failed");
+ }
+ }
+}
+
+/**
+ * Check a logic invariant.
+ *
+ * A violation of this invariant means that there is a logic bug in the program.
+ */
+export function checkLogicInvariant(b: boolean, m?: string): asserts b {
+ if (!b) {
+ if (m) {
+ throw Error(`BUG: logic invariant failed (${m})`);
+ } else {
+ throw Error("BUG: logic invariant failed");
+ }
+ }
+}
diff --git a/packages/taler-util/src/iso-4217.ts b/packages/taler-util/src/iso-4217.ts
new file mode 100644
index 000000000..b155676ff
--- /dev/null
+++ b/packages/taler-util/src/iso-4217.ts
@@ -0,0 +1,1717 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 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/>
+ */
+
+// From https://en.wikipedia.org/wiki/ISO_4217
+
+//modifications to the original data
+// * currency without decimal represented with 0
+// * removed 4 with label "No universal currency"
+// * numeric as number
+// * removed all field except:
+// - c: currency name
+// - a: alphabetic code
+// - n: numeric code
+// - d: minor unit
+type CurrencyInfo = {
+ /**
+ * name
+ */
+ c: string;
+ /**
+ * alphabetic code
+ */
+ a: string;
+ /**
+ * numeric code
+ */
+ n: number;
+ /**
+ * minor unit
+ * "0" means that there is no minor unit for that currency, whereas "1", "2"
+ * and "3" signify a ratio of 10:1, 100:1 and 1000:1 respectively.
+ */
+ d: number;
+};
+export const data: Array<CurrencyInfo> = [
+ {
+ c: "Afghani",
+ a: "AFN",
+ n: 971,
+ d: 2,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "Lek",
+ a: "ALL",
+ n: 8,
+ d: 2,
+ },
+ {
+ c: "Algerian Dinar",
+ a: "DZD",
+ n: 12,
+ d: 2,
+ },
+ {
+ c: "US Dollar",
+ a: "USD",
+ n: 840,
+ d: 2,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "Kwanza",
+ a: "AOA",
+ n: 973,
+ d: 2,
+ },
+ {
+ c: "East Caribbean Dollar",
+ a: "XCD",
+ n: 951,
+ d: 2,
+ },
+ {
+ c: "East Caribbean Dollar",
+ a: "XCD",
+ n: 951,
+ d: 2,
+ },
+ {
+ c: "Argentine Peso",
+ a: "ARS",
+ n: 32,
+ d: 2,
+ },
+ {
+ c: "Armenian Dram",
+ a: "AMD",
+ n: 51,
+ d: 2,
+ },
+ {
+ c: "Aruban Florin",
+ a: "AWG",
+ n: 533,
+ d: 2,
+ },
+ {
+ c: "Australian Dollar",
+ a: "AUD",
+ n: 36,
+ d: 2,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "Azerbaijan Manat",
+ a: "AZN",
+ n: 944,
+ d: 2,
+ },
+ {
+ c: "Bahamian Dollar",
+ a: "BSD",
+ n: 44,
+ d: 2,
+ },
+ {
+ c: "Bahraini Dinar",
+ a: "BHD",
+ n: 48,
+ d: 3,
+ },
+ {
+ c: "Taka",
+ a: "BDT",
+ n: 50,
+ d: 2,
+ },
+ {
+ c: "Barbados Dollar",
+ a: "BBD",
+ n: 52,
+ d: 2,
+ },
+ {
+ c: "Belarusian Ruble",
+ a: "BYN",
+ n: 933,
+ d: 2,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "Belize Dollar",
+ a: "BZD",
+ n: 84,
+ d: 2,
+ },
+ {
+ c: "CFA Franc BCEAO",
+ a: "XOF",
+ n: 952,
+ d: 0,
+ },
+ {
+ c: "Bermudian Dollar",
+ a: "BMD",
+ n: 60,
+ d: 2,
+ },
+ {
+ c: "Indian Rupee",
+ a: "INR",
+ n: 356,
+ d: 2,
+ },
+ {
+ c: "Ngultrum",
+ a: "BTN",
+ n: 64,
+ d: 2,
+ },
+ {
+ c: "Boliviano",
+ a: "BOB",
+ n: 68,
+ d: 2,
+ },
+ {
+ c: "Mvdol",
+ a: "BOV",
+ n: 984,
+ d: 2,
+ },
+ {
+ c: "US Dollar",
+ a: "USD",
+ n: 840,
+ d: 2,
+ },
+ {
+ c: "Convertible Mark",
+ a: "BAM",
+ n: 977,
+ d: 2,
+ },
+ {
+ c: "Pula",
+ a: "BWP",
+ n: 72,
+ d: 2,
+ },
+ {
+ c: "Norwegian Krone",
+ a: "NOK",
+ n: 578,
+ d: 2,
+ },
+ {
+ c: "Brazilian Real",
+ a: "BRL",
+ n: 986,
+ d: 2,
+ },
+ {
+ c: "US Dollar",
+ a: "USD",
+ n: 840,
+ d: 2,
+ },
+ {
+ c: "Brunei Dollar",
+ a: "BND",
+ n: 96,
+ d: 2,
+ },
+ {
+ c: "Bulgarian Lev",
+ a: "BGN",
+ n: 975,
+ d: 2,
+ },
+ {
+ c: "CFA Franc BCEAO",
+ a: "XOF",
+ n: 952,
+ d: 0,
+ },
+ {
+ c: "Burundi Franc",
+ a: "BIF",
+ n: 108,
+ d: 0,
+ },
+ {
+ c: "Cabo Verde Escudo",
+ a: "CVE",
+ n: 132,
+ d: 2,
+ },
+ {
+ c: "Riel",
+ a: "KHR",
+ n: 116,
+ d: 2,
+ },
+ {
+ c: "CFA Franc BEAC",
+ a: "XAF",
+ n: 950,
+ d: 0,
+ },
+ {
+ c: "Canadian Dollar",
+ a: "CAD",
+ n: 124,
+ d: 2,
+ },
+ {
+ c: "Cayman Islands Dollar",
+ a: "KYD",
+ n: 136,
+ d: 2,
+ },
+ {
+ c: "CFA Franc BEAC",
+ a: "XAF",
+ n: 950,
+ d: 0,
+ },
+ {
+ c: "CFA Franc BEAC",
+ a: "XAF",
+ n: 950,
+ d: 0,
+ },
+ {
+ c: "Chilean Peso",
+ a: "CLP",
+ n: 152,
+ d: 0,
+ },
+ {
+ c: "Unidad de Fomento",
+ a: "CLF",
+ n: 990,
+ d: 4,
+ },
+ {
+ c: "Yuan Renminbi",
+ a: "CNY",
+ n: 156,
+ d: 2,
+ },
+ {
+ c: "Australian Dollar",
+ a: "AUD",
+ n: 36,
+ d: 2,
+ },
+ {
+ c: "Australian Dollar",
+ a: "AUD",
+ n: 36,
+ d: 2,
+ },
+ {
+ c: "Colombian Peso",
+ a: "COP",
+ n: 170,
+ d: 2,
+ },
+ {
+ c: "Unidad de Valor Real",
+ a: "COU",
+ n: 970,
+ d: 2,
+ },
+ {
+ c: "Comorian Franc",
+ a: "KMF",
+ n: 174,
+ d: 0,
+ },
+ {
+ c: "Congolese Franc",
+ a: "CDF",
+ n: 976,
+ d: 2,
+ },
+ {
+ c: "CFA Franc BEAC",
+ a: "XAF",
+ n: 950,
+ d: 0,
+ },
+ {
+ c: "New Zealand Dollar",
+ a: "NZD",
+ n: 554,
+ d: 2,
+ },
+ {
+ c: "Costa Rican Colon",
+ a: "CRC",
+ n: 188,
+ d: 2,
+ },
+ {
+ c: "CFA Franc BCEAO",
+ a: "XOF",
+ n: 952,
+ d: 0,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "Cuban Peso",
+ a: "CUP",
+ n: 192,
+ d: 2,
+ },
+ {
+ c: "Peso Convertible",
+ a: "CUC",
+ n: 931,
+ d: 2,
+ },
+ {
+ c: "Netherlands Antillean Guilder",
+ a: "ANG",
+ n: 532,
+ d: 2,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "Czech Koruna",
+ a: "CZK",
+ n: 203,
+ d: 2,
+ },
+ {
+ c: "Danish Krone",
+ a: "DKK",
+ n: 208,
+ d: 2,
+ },
+ {
+ c: "Djibouti Franc",
+ a: "DJF",
+ n: 262,
+ d: 0,
+ },
+ {
+ c: "East Caribbean Dollar",
+ a: "XCD",
+ n: 951,
+ d: 2,
+ },
+ {
+ c: "Dominican Peso",
+ a: "DOP",
+ n: 214,
+ d: 2,
+ },
+ {
+ c: "US Dollar",
+ a: "USD",
+ n: 840,
+ d: 2,
+ },
+ {
+ c: "Egyptian Pound",
+ a: "EGP",
+ n: 818,
+ d: 2,
+ },
+ {
+ c: "El Salvador Colon",
+ a: "SVC",
+ n: 222,
+ d: 2,
+ },
+ {
+ c: "US Dollar",
+ a: "USD",
+ n: 840,
+ d: 2,
+ },
+ {
+ c: "CFA Franc BEAC",
+ a: "XAF",
+ n: 950,
+ d: 0,
+ },
+ {
+ c: "Nakfa",
+ a: "ERN",
+ n: 232,
+ d: 2,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "Lilangeni",
+ a: "SZL",
+ n: 748,
+ d: 2,
+ },
+ {
+ c: "Ethiopian Birr",
+ a: "ETB",
+ n: 230,
+ d: 2,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "Falkland Islands Pound",
+ a: "FKP",
+ n: 238,
+ d: 2,
+ },
+ {
+ c: "Danish Krone",
+ a: "DKK",
+ n: 208,
+ d: 2,
+ },
+ {
+ c: "Fiji Dollar",
+ a: "FJD",
+ n: 242,
+ d: 2,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "CFP Franc",
+ a: "XPF",
+ n: 953,
+ d: 0,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "CFA Franc BEAC",
+ a: "XAF",
+ n: 950,
+ d: 0,
+ },
+ {
+ c: "Dalasi",
+ a: "GMD",
+ n: 270,
+ d: 2,
+ },
+ {
+ c: "Lari",
+ a: "GEL",
+ n: 981,
+ d: 2,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "Ghana Cedi",
+ a: "GHS",
+ n: 936,
+ d: 2,
+ },
+ {
+ c: "Gibraltar Pound",
+ a: "GIP",
+ n: 292,
+ d: 2,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "Danish Krone",
+ a: "DKK",
+ n: 208,
+ d: 2,
+ },
+ {
+ c: "East Caribbean Dollar",
+ a: "XCD",
+ n: 951,
+ d: 2,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "US Dollar",
+ a: "USD",
+ n: 840,
+ d: 2,
+ },
+ {
+ c: "Quetzal",
+ a: "GTQ",
+ n: 320,
+ d: 2,
+ },
+ {
+ c: "Pound Sterling",
+ a: "GBP",
+ n: 826,
+ d: 2,
+ },
+ {
+ c: "Guinean Franc",
+ a: "GNF",
+ n: 324,
+ d: 0,
+ },
+ {
+ c: "CFA Franc BCEAO",
+ a: "XOF",
+ n: 952,
+ d: 0,
+ },
+ {
+ c: "Guyana Dollar",
+ a: "GYD",
+ n: 328,
+ d: 2,
+ },
+ {
+ c: "Gourde",
+ a: "HTG",
+ n: 332,
+ d: 2,
+ },
+ {
+ c: "US Dollar",
+ a: "USD",
+ n: 840,
+ d: 2,
+ },
+ {
+ c: "Australian Dollar",
+ a: "AUD",
+ n: 36,
+ d: 2,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "Lempira",
+ a: "HNL",
+ n: 340,
+ d: 2,
+ },
+ {
+ c: "Hong Kong Dollar",
+ a: "HKD",
+ n: 344,
+ d: 2,
+ },
+ {
+ c: "Forint",
+ a: "HUF",
+ n: 348,
+ d: 2,
+ },
+ {
+ c: "Iceland Krona",
+ a: "ISK",
+ n: 352,
+ d: 0,
+ },
+ {
+ c: "Indian Rupee",
+ a: "INR",
+ n: 356,
+ d: 2,
+ },
+ {
+ c: "Rupiah",
+ a: "IDR",
+ n: 360,
+ d: 2,
+ },
+ {
+ c: "SDR (Special Drawing Right)",
+ a: "XDR",
+ n: 960,
+ d: 0,
+ },
+ {
+ c: "Iranian Rial",
+ a: "IRR",
+ n: 364,
+ d: 2,
+ },
+ {
+ c: "Iraqi Dinar",
+ a: "IQD",
+ n: 368,
+ d: 3,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "Pound Sterling",
+ a: "GBP",
+ n: 826,
+ d: 2,
+ },
+ {
+ c: "New Israeli Sheqel",
+ a: "ILS",
+ n: 376,
+ d: 2,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "Jamaican Dollar",
+ a: "JMD",
+ n: 388,
+ d: 2,
+ },
+ {
+ c: "Yen",
+ a: "JPY",
+ n: 392,
+ d: 0,
+ },
+ {
+ c: "Pound Sterling",
+ a: "GBP",
+ n: 826,
+ d: 2,
+ },
+ {
+ c: "Jordanian Dinar",
+ a: "JOD",
+ n: 400,
+ d: 3,
+ },
+ {
+ c: "Tenge",
+ a: "KZT",
+ n: 398,
+ d: 2,
+ },
+ {
+ c: "Kenyan Shilling",
+ a: "KES",
+ n: 404,
+ d: 2,
+ },
+ {
+ c: "Australian Dollar",
+ a: "AUD",
+ n: 36,
+ d: 2,
+ },
+ {
+ c: "North Korean Won",
+ a: "KPW",
+ n: 408,
+ d: 2,
+ },
+ {
+ c: "Won",
+ a: "KRW",
+ n: 410,
+ d: 0,
+ },
+ {
+ c: "Kuwaiti Dinar",
+ a: "KWD",
+ n: 414,
+ d: 3,
+ },
+ {
+ c: "Som",
+ a: "KGS",
+ n: 417,
+ d: 2,
+ },
+ {
+ c: "Lao Kip",
+ a: "LAK",
+ n: 418,
+ d: 2,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "Lebanese Pound",
+ a: "LBP",
+ n: 422,
+ d: 2,
+ },
+ {
+ c: "Loti",
+ a: "LSL",
+ n: 426,
+ d: 2,
+ },
+ {
+ c: "Rand",
+ a: "ZAR",
+ n: 710,
+ d: 2,
+ },
+ {
+ c: "Liberian Dollar",
+ a: "LRD",
+ n: 430,
+ d: 2,
+ },
+ {
+ c: "Libyan Dinar",
+ a: "LYD",
+ n: 434,
+ d: 3,
+ },
+ {
+ c: "Swiss Franc",
+ a: "CHF",
+ n: 756,
+ d: 2,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "Pataca",
+ a: "MOP",
+ n: 446,
+ d: 2,
+ },
+ {
+ c: "Denar",
+ a: "MKD",
+ n: 807,
+ d: 2,
+ },
+ {
+ c: "Malagasy Ariary",
+ a: "MGA",
+ n: 969,
+ d: 2,
+ },
+ {
+ c: "Malawi Kwacha",
+ a: "MWK",
+ n: 454,
+ d: 2,
+ },
+ {
+ c: "Malaysian Ringgit",
+ a: "MYR",
+ n: 458,
+ d: 2,
+ },
+ {
+ c: "Rufiyaa",
+ a: "MVR",
+ n: 462,
+ d: 2,
+ },
+ {
+ c: "CFA Franc BCEAO",
+ a: "XOF",
+ n: 952,
+ d: 0,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "US Dollar",
+ a: "USD",
+ n: 840,
+ d: 2,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "Ouguiya",
+ a: "MRU",
+ n: 929,
+ d: 2,
+ },
+ {
+ c: "Mauritius Rupee",
+ a: "MUR",
+ n: 480,
+ d: 2,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "ADB Unit of Account",
+ a: "XUA",
+ n: 965,
+ d: 0,
+ },
+ {
+ c: "Mexican Peso",
+ a: "MXN",
+ n: 484,
+ d: 2,
+ },
+ {
+ c: "Mexican Unidad de Inversion",
+ a: "MXV",
+ n: 979,
+ d: 2,
+ },
+ {
+ c: "US Dollar",
+ a: "USD",
+ n: 840,
+ d: 2,
+ },
+ {
+ c: "Moldovan Leu",
+ a: "MDL",
+ n: 498,
+ d: 2,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "Tugrik",
+ a: "MNT",
+ n: 496,
+ d: 2,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "East Caribbean Dollar",
+ a: "XCD",
+ n: 951,
+ d: 2,
+ },
+ {
+ c: "Moroccan Dirham",
+ a: "MAD",
+ n: 504,
+ d: 2,
+ },
+ {
+ c: "Mozambique Metical",
+ a: "MZN",
+ n: 943,
+ d: 2,
+ },
+ {
+ c: "Kyat",
+ a: "MMK",
+ n: 104,
+ d: 2,
+ },
+ {
+ c: "Namibia Dollar",
+ a: "NAD",
+ n: 516,
+ d: 2,
+ },
+ {
+ c: "Rand",
+ a: "ZAR",
+ n: 710,
+ d: 2,
+ },
+ {
+ c: "Australian Dollar",
+ a: "AUD",
+ n: 36,
+ d: 2,
+ },
+ {
+ c: "Nepalese Rupee",
+ a: "NPR",
+ n: 524,
+ d: 2,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "CFP Franc",
+ a: "XPF",
+ n: 953,
+ d: 0,
+ },
+ {
+ c: "New Zealand Dollar",
+ a: "NZD",
+ n: 554,
+ d: 2,
+ },
+ {
+ c: "Cordoba Oro",
+ a: "NIO",
+ n: 558,
+ d: 2,
+ },
+ {
+ c: "CFA Franc BCEAO",
+ a: "XOF",
+ n: 952,
+ d: 0,
+ },
+ {
+ c: "Naira",
+ a: "NGN",
+ n: 566,
+ d: 2,
+ },
+ {
+ c: "New Zealand Dollar",
+ a: "NZD",
+ n: 554,
+ d: 2,
+ },
+ {
+ c: "Australian Dollar",
+ a: "AUD",
+ n: 36,
+ d: 2,
+ },
+ {
+ c: "US Dollar",
+ a: "USD",
+ n: 840,
+ d: 2,
+ },
+ {
+ c: "Norwegian Krone",
+ a: "NOK",
+ n: 578,
+ d: 2,
+ },
+ {
+ c: "Rial Omani",
+ a: "OMR",
+ n: 512,
+ d: 3,
+ },
+ {
+ c: "Pakistan Rupee",
+ a: "PKR",
+ n: 586,
+ d: 2,
+ },
+ {
+ c: "US Dollar",
+ a: "USD",
+ n: 840,
+ d: 2,
+ },
+ {
+ c: "Balboa",
+ a: "PAB",
+ n: 590,
+ d: 2,
+ },
+ {
+ c: "US Dollar",
+ a: "USD",
+ n: 840,
+ d: 2,
+ },
+ {
+ c: "Kina",
+ a: "PGK",
+ n: 598,
+ d: 2,
+ },
+ {
+ c: "Guarani",
+ a: "PYG",
+ n: 600,
+ d: 0,
+ },
+ {
+ c: "Sol",
+ a: "PEN",
+ n: 604,
+ d: 2,
+ },
+ {
+ c: "Philippine Peso",
+ a: "PHP",
+ n: 608,
+ d: 2,
+ },
+ {
+ c: "New Zealand Dollar",
+ a: "NZD",
+ n: 554,
+ d: 2,
+ },
+ {
+ c: "Zloty",
+ a: "PLN",
+ n: 985,
+ d: 2,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "US Dollar",
+ a: "USD",
+ n: 840,
+ d: 2,
+ },
+ {
+ c: "Qatari Rial",
+ a: "QAR",
+ n: 634,
+ d: 2,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "Romanian Leu",
+ a: "RON",
+ n: 946,
+ d: 2,
+ },
+ {
+ c: "Russian Ruble",
+ a: "RUB",
+ n: 643,
+ d: 2,
+ },
+ {
+ c: "Rwanda Franc",
+ a: "RWF",
+ n: 646,
+ d: 0,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "Saint Helena Pound",
+ a: "SHP",
+ n: 654,
+ d: 2,
+ },
+ {
+ c: "East Caribbean Dollar",
+ a: "XCD",
+ n: 951,
+ d: 2,
+ },
+ {
+ c: "East Caribbean Dollar",
+ a: "XCD",
+ n: 951,
+ d: 2,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "East Caribbean Dollar",
+ a: "XCD",
+ n: 951,
+ d: 2,
+ },
+ {
+ c: "Tala",
+ a: "WST",
+ n: 882,
+ d: 2,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "Dobra",
+ a: "STN",
+ n: 930,
+ d: 2,
+ },
+ {
+ c: "Saudi Riyal",
+ a: "SAR",
+ n: 682,
+ d: 2,
+ },
+ {
+ c: "CFA Franc BCEAO",
+ a: "XOF",
+ n: 952,
+ d: 0,
+ },
+ {
+ c: "Serbian Dinar",
+ a: "RSD",
+ n: 941,
+ d: 2,
+ },
+ {
+ c: "Seychelles Rupee",
+ a: "SCR",
+ n: 690,
+ d: 2,
+ },
+ {
+ c: "Leone",
+ a: "SLL",
+ n: 694,
+ d: 2,
+ },
+ {
+ c: "Leone",
+ a: "SLE",
+ n: 925,
+ d: 2,
+ },
+ {
+ c: "Singapore Dollar",
+ a: "SGD",
+ n: 702,
+ d: 2,
+ },
+ {
+ c: "Netherlands Antillean Guilder",
+ a: "ANG",
+ n: 532,
+ d: 2,
+ },
+ {
+ c: "Sucre",
+ a: "XSU",
+ n: 994,
+ d: 0,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "Solomon Islands Dollar",
+ a: "SBD",
+ n: 90,
+ d: 2,
+ },
+ {
+ c: "Somali Shilling",
+ a: "SOS",
+ n: 706,
+ d: 2,
+ },
+ {
+ c: "Rand",
+ a: "ZAR",
+ n: 710,
+ d: 2,
+ },
+ {
+ c: "South Sudanese Pound",
+ a: "SSP",
+ n: 728,
+ d: 2,
+ },
+ {
+ c: "Euro",
+ a: "EUR",
+ n: 978,
+ d: 2,
+ },
+ {
+ c: "Sri Lanka Rupee",
+ a: "LKR",
+ n: 144,
+ d: 2,
+ },
+ {
+ c: "Sudanese Pound",
+ a: "SDG",
+ n: 938,
+ d: 2,
+ },
+ {
+ c: "Surinam Dollar",
+ a: "SRD",
+ n: 968,
+ d: 2,
+ },
+ {
+ c: "Norwegian Krone",
+ a: "NOK",
+ n: 578,
+ d: 2,
+ },
+ {
+ c: "Swedish Krona",
+ a: "SEK",
+ n: 752,
+ d: 2,
+ },
+ {
+ c: "Swiss Franc",
+ a: "CHF",
+ n: 756,
+ d: 2,
+ },
+ {
+ c: "WIR Euro",
+ a: "CHE",
+ n: 947,
+ d: 2,
+ },
+ {
+ c: "WIR Franc",
+ a: "CHW",
+ n: 948,
+ d: 2,
+ },
+ {
+ c: "Syrian Pound",
+ a: "SYP",
+ n: 760,
+ d: 2,
+ },
+ {
+ c: "New Taiwan Dollar",
+ a: "TWD",
+ n: 901,
+ d: 2,
+ },
+ {
+ c: "Somoni",
+ a: "TJS",
+ n: 972,
+ d: 2,
+ },
+ {
+ c: "Tanzanian Shilling",
+ a: "TZS",
+ n: 834,
+ d: 2,
+ },
+ {
+ c: "Baht",
+ a: "THB",
+ n: 764,
+ d: 2,
+ },
+ {
+ c: "US Dollar",
+ a: "USD",
+ n: 840,
+ d: 2,
+ },
+ {
+ c: "CFA Franc BCEAO",
+ a: "XOF",
+ n: 952,
+ d: 0,
+ },
+ {
+ c: "New Zealand Dollar",
+ a: "NZD",
+ n: 554,
+ d: 2,
+ },
+ {
+ c: "Pa'anga",
+ a: "TOP",
+ n: 776,
+ d: 2,
+ },
+ {
+ c: "Trinidad and Tobago Dollar",
+ a: "TTD",
+ n: 780,
+ d: 2,
+ },
+ {
+ c: "Tunisian Dinar",
+ a: "TND",
+ n: 788,
+ d: 3,
+ },
+ {
+ c: "Turkish Lira",
+ a: "TRY",
+ n: 949,
+ d: 2,
+ },
+ {
+ c: "Turkmenistan New Manat",
+ a: "TMT",
+ n: 934,
+ d: 2,
+ },
+ {
+ c: "US Dollar",
+ a: "USD",
+ n: 840,
+ d: 2,
+ },
+ {
+ c: "Australian Dollar",
+ a: "AUD",
+ n: 36,
+ d: 2,
+ },
+ {
+ c: "Uganda Shilling",
+ a: "UGX",
+ n: 800,
+ d: 0,
+ },
+ {
+ c: "Hryvnia",
+ a: "UAH",
+ n: 980,
+ d: 2,
+ },
+ {
+ c: "UAE Dirham",
+ a: "AED",
+ n: 784,
+ d: 2,
+ },
+ {
+ c: "Pound Sterling",
+ a: "GBP",
+ n: 826,
+ d: 2,
+ },
+ {
+ c: "US Dollar",
+ a: "USD",
+ n: 840,
+ d: 2,
+ },
+ {
+ c: "US Dollar",
+ a: "USD",
+ n: 840,
+ d: 2,
+ },
+ {
+ c: "US Dollar (Next day)",
+ a: "USN",
+ n: 997,
+ d: 2,
+ },
+ {
+ c: "Peso Uruguayo",
+ a: "UYU",
+ n: 858,
+ d: 2,
+ },
+ {
+ c: "Uruguay Peso en Unidades Indexadas (UI)",
+ a: "UYI",
+ n: 940,
+ d: 0,
+ },
+ {
+ c: "Unidad Previsional",
+ a: "UYW",
+ n: 927,
+ d: 4,
+ },
+ {
+ c: "Uzbekistan Sum",
+ a: "UZS",
+ n: 860,
+ d: 2,
+ },
+ {
+ c: "Vatu",
+ a: "VUV",
+ n: 548,
+ d: 0,
+ },
+ {
+ c: "Bolívar Soberano",
+ a: "VES",
+ n: 928,
+ d: 2,
+ },
+ {
+ c: "Bolívar Soberano",
+ a: "VED",
+ n: 926,
+ d: 2,
+ },
+ {
+ c: "Dong",
+ a: "VND",
+ n: 704,
+ d: 0,
+ },
+ {
+ c: "US Dollar",
+ a: "USD",
+ n: 840,
+ d: 2,
+ },
+ {
+ c: "US Dollar",
+ a: "USD",
+ n: 840,
+ d: 2,
+ },
+ {
+ c: "CFP Franc",
+ a: "XPF",
+ n: 953,
+ d: 0,
+ },
+ {
+ c: "Moroccan Dirham",
+ a: "MAD",
+ n: 504,
+ d: 2,
+ },
+ {
+ c: "Yemeni Rial",
+ a: "YER",
+ n: 886,
+ d: 2,
+ },
+ {
+ c: "Zambian Kwacha",
+ a: "ZMW",
+ n: 967,
+ d: 2,
+ },
+ {
+ c: "Zimbabwe Dollar",
+ a: "ZWL",
+ n: 932,
+ d: 2,
+ },
+ {
+ c: "Bond Markets Unit European Composite Unit (EURCO)",
+ a: "XBA",
+ n: 955,
+ d: 0,
+ },
+ {
+ c: "Bond Markets Unit European Monetary Unit (E.M.U.-6)",
+ a: "XBB",
+ n: 956,
+ d: 0,
+ },
+ {
+ c: "Bond Markets Unit European Unit of Account 9 (E.U.A.-9)",
+ a: "XBC",
+ n: 957,
+ d: 0,
+ },
+ {
+ c: "Bond Markets Unit European Unit of Account 17 (E.U.A.-17)",
+ a: "XBD",
+ n: 958,
+ d: 0,
+ },
+ {
+ c: "Codes specifically reserved for testing purposes",
+ a: "XTS",
+ n: 963,
+ d: 0,
+ },
+ {
+ c: "The codes assigned for transactions where no currency is involved",
+ a: "XXX",
+ n: 999,
+ d: 0,
+ },
+ {
+ c: "Gold",
+ a: "XAU",
+ n: 959,
+ d: 0,
+ },
+ {
+ c: "Palladium",
+ a: "XPD",
+ n: 964,
+ d: 0,
+ },
+ {
+ c: "Platinum",
+ a: "XPT",
+ n: 962,
+ d: 0,
+ },
+ {
+ c: "Silver",
+ a: "XAG",
+ n: 961,
+ d: 0,
+ },
+];
diff --git a/packages/taler-util/src/kdf.ts b/packages/taler-util/src/kdf.ts
index 5fcaa1b4c..8f4314340 100644
--- a/packages/taler-util/src/kdf.ts
+++ b/packages/taler-util/src/kdf.ts
@@ -58,38 +58,3 @@ export function hmacSha512(key: Uint8Array, message: Uint8Array): Uint8Array {
export function hmacSha256(key: Uint8Array, message: Uint8Array): Uint8Array {
return hmac(sha256, 64, key, message);
}
-
-export function kdf(
- outputLength: number,
- ikm: Uint8Array,
- salt?: Uint8Array,
- info?: Uint8Array,
-): Uint8Array {
- salt = salt ?? new Uint8Array(64);
- // extract
- const prk = hmacSha512(salt, ikm);
-
- info = info ?? new Uint8Array(0);
-
- // expand
- const N = Math.ceil(outputLength / 32);
- const output = new Uint8Array(N * 32);
- for (let i = 0; i < N; i++) {
- let buf;
- if (i == 0) {
- buf = new Uint8Array(info.byteLength + 1);
- buf.set(info, 0);
- } else {
- buf = new Uint8Array(info.byteLength + 1 + 32);
- for (let j = 0; j < 32; j++) {
- buf[j] = output[(i - 1) * 32 + j];
- }
- buf.set(info, 32);
- }
- buf[buf.length - 1] = i + 1;
- const chunk = hmacSha256(prk, buf);
- output.set(chunk, i * 32);
- }
-
- return output.slice(0, outputLength);
-}
diff --git a/packages/taler-util/src/libeufin-api-types.ts b/packages/taler-util/src/libeufin-api-types.ts
new file mode 100644
index 000000000..aa3d0cb7a
--- /dev/null
+++ b/packages/taler-util/src/libeufin-api-types.ts
@@ -0,0 +1,31 @@
+/*
+ This file is part of GNU Taler
+ (C) 2023 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/>
+ */
+
+export type FacadeCredentials =
+ | NoFacadeCredentials
+ | BasicAuthFacadeCredentials;
+export interface NoFacadeCredentials {
+ type: "none";
+}
+export interface BasicAuthFacadeCredentials {
+ type: "basic";
+
+ // Username to use to authenticate
+ username: string;
+
+ // Password to use to authenticate
+ password: string;
+}
diff --git a/packages/taler-util/src/libtool-version.test.ts b/packages/taler-util/src/libtool-version.test.ts
index c1683f0df..addd1b418 100644
--- a/packages/taler-util/src/libtool-version.test.ts
+++ b/packages/taler-util/src/libtool-version.test.ts
@@ -45,4 +45,6 @@ test("version comparison", (t) => {
compatible: true,
currentCmp: 0,
});
+ t.true(LibtoolVersion.compare("42:0:1", "41:0:0")?.compatible);
+ t.true(LibtoolVersion.compare("41:0:0", "42:0:1")?.compatible);
});
diff --git a/packages/taler-util/src/logging.ts b/packages/taler-util/src/logging.ts
index 840402d6f..17bb184f7 100644
--- a/packages/taler-util/src/logging.ts
+++ b/packages/taler-util/src/logging.ts
@@ -32,36 +32,86 @@ export enum LogLevel {
None = "none",
}
-export let globalLogLevel = LogLevel.Info;
+let globalLogLevel = LogLevel.Info;
+const byTagLogLevel: Record<string, LogLevel> = {};
-export function setGlobalLogLevelFromString(logLevelStr: string) {
- let level: LogLevel;
+let nativeLogging: boolean = false;
+
+// from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/toString
+Error.prototype.toString = function () {
+ if (
+ this === null ||
+ (typeof this !== "object" && typeof this !== "function")
+ ) {
+ throw new TypeError();
+ }
+ let name = this.name;
+ name = name === undefined ? "Error" : `${name}`;
+ let msg = this.message;
+ msg = msg === undefined ? "" : `${msg}`;
+
+ let cause = "";
+ if ("cause" in this) {
+ cause = `\n Caused by: ${this.cause}`;
+ }
+ return `${name}: ${msg}${cause}`;
+};
+
+export function getGlobalLogLevel(): string {
+ return globalLogLevel;
+}
+
+export function setGlobalLogLevelFromString(logLevelStr: string): void {
+ globalLogLevel = getLevelForString(logLevelStr);
+}
+
+export function setLogLevelFromString(tag: string, logLevelStr: string): void {
+ byTagLogLevel[tag] = getLevelForString(logLevelStr);
+}
+
+export function enableNativeLogging() {
+ nativeLogging = true;
+}
+
+function getLevelForString(logLevelStr: string): LogLevel {
switch (logLevelStr.toLowerCase()) {
case "trace":
- level = LogLevel.Trace;
- break;
+ return LogLevel.Trace;
case "info":
- level = LogLevel.Info;
- break;
+ return LogLevel.Info;
case "warn":
case "warning":
- level = LogLevel.Warn;
- break;
+ return LogLevel.Warn;
case "error":
- level = LogLevel.Error;
- break;
+ return LogLevel.Error;
case "none":
- level = LogLevel.None;
- break;
+ return LogLevel.None;
default:
if (isNode) {
process.stderr.write(`Invalid log level, defaulting to WARNING\n`);
} else {
console.warn(`Invalid log level, defaulting to WARNING`);
}
- level = LogLevel.Warn;
+ return LogLevel.Warn;
+ }
+}
+
+function writeNativeLog(
+ message: any,
+ tag: string,
+ level: number,
+ args: any[],
+): void {
+ const logFn = (globalThis as any).__nativeLog;
+ if (logFn) {
+ let m: string;
+ if (args.length == 0) {
+ m = message;
+ } else {
+ m = message + " " + args.toString();
+ }
+ logFn(level, tag, message);
}
- globalLogLevel = level;
}
function writeNodeLog(
@@ -98,8 +148,9 @@ function writeNodeLog(
export class Logger {
constructor(private tag: string) {}
- shouldLogTrace() {
- switch (globalLogLevel) {
+ shouldLogTrace(): boolean {
+ const level = byTagLogLevel[this.tag] ?? globalLogLevel;
+ switch (level) {
case LogLevel.Trace:
return true;
case LogLevel.Message:
@@ -111,8 +162,9 @@ export class Logger {
}
}
- shouldLogInfo() {
- switch (globalLogLevel) {
+ shouldLogInfo(): boolean {
+ const level = byTagLogLevel[this.tag] ?? globalLogLevel;
+ switch (level) {
case LogLevel.Trace:
case LogLevel.Message:
case LogLevel.Info:
@@ -124,8 +176,9 @@ export class Logger {
}
}
- shouldLogWarn() {
- switch (globalLogLevel) {
+ shouldLogWarn(): boolean {
+ const level = byTagLogLevel[this.tag] ?? globalLogLevel;
+ switch (level) {
case LogLevel.Trace:
case LogLevel.Message:
case LogLevel.Info:
@@ -137,8 +190,9 @@ export class Logger {
}
}
- shouldLogError() {
- switch (globalLogLevel) {
+ shouldLogError(): boolean {
+ const level = byTagLogLevel[this.tag] ?? globalLogLevel;
+ switch (level) {
case LogLevel.Trace:
case LogLevel.Message:
case LogLevel.Info:
@@ -154,6 +208,10 @@ export class Logger {
if (!this.shouldLogInfo()) {
return;
}
+ if (nativeLogging) {
+ writeNativeLog(message, this.tag, 2, args);
+ return;
+ }
if (isNode) {
writeNodeLog(message, this.tag, "INFO", args);
} else {
@@ -168,6 +226,10 @@ export class Logger {
if (!this.shouldLogWarn()) {
return;
}
+ if (nativeLogging) {
+ writeNativeLog(message, this.tag, 3, args);
+ return;
+ }
if (isNode) {
writeNodeLog(message, this.tag, "WARN", args);
} else {
@@ -182,6 +244,10 @@ export class Logger {
if (!this.shouldLogError()) {
return;
}
+ if (nativeLogging) {
+ writeNativeLog(message, this.tag, 4, args);
+ return;
+ }
if (isNode) {
writeNodeLog(message, this.tag, "ERROR", args);
} else {
@@ -192,10 +258,14 @@ export class Logger {
}
}
- trace(message: any, ...args: any[]): void {
+ trace(message: string, ...args: any[]): void {
if (!this.shouldLogTrace()) {
return;
}
+ if (nativeLogging) {
+ writeNativeLog(message, this.tag, 1, args);
+ return;
+ }
if (isNode) {
writeNodeLog(message, this.tag, "TRACE", args);
} else {
diff --git a/packages/taler-util/src/notifications.ts b/packages/taler-util/src/notifications.ts
index c50cc72de..d4dfe7589 100644
--- a/packages/taler-util/src/notifications.ts
+++ b/packages/taler-util/src/notifications.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2019 GNUnet e.V.
+ (C) 2019-2023 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
@@ -22,265 +22,228 @@
/**
* Imports.
*/
-import { TalerErrorDetail } from "./wallet-types.js";
+import { AbsoluteTime } from "./time.js";
+import { TransactionState } from "./transactions-types.js";
+import { ExchangeEntryState, TalerErrorDetail } from "./wallet-types.js";
export enum NotificationType {
- CoinWithdrawn = "coin-withdrawn",
- ProposalAccepted = "proposal-accepted",
- ProposalDownloaded = "proposal-downloaded",
- RefundsSubmitted = "refunds-submitted",
- RecoupStarted = "recoup-started",
- RecoupFinished = "recoup-finished",
- RefreshRevealed = "refresh-revealed",
- RefreshMelted = "refresh-melted",
- RefreshStarted = "refresh-started",
- RefreshUnwarranted = "refresh-unwarranted",
- ReserveUpdated = "reserve-updated",
- ReserveConfirmed = "reserve-confirmed",
- ReserveCreated = "reserve-created",
- WithdrawGroupCreated = "withdraw-group-created",
- WithdrawGroupFinished = "withdraw-group-finished",
- WaitingForRetry = "waiting-for-retry",
- RefundStarted = "refund-started",
- RefundQueried = "refund-queried",
- RefundFinished = "refund-finished",
- ExchangeOperationError = "exchange-operation-error",
- ExchangeAdded = "exchange-added",
- RefreshOperationError = "refresh-operation-error",
- RecoupOperationError = "recoup-operation-error",
- RefundApplyOperationError = "refund-apply-error",
- RefundStatusOperationError = "refund-status-error",
- ProposalOperationError = "proposal-error",
+ BalanceChange = "balance-change",
BackupOperationError = "backup-error",
- TipOperationError = "tip-error",
- PayOperationError = "pay-error",
- PayOperationSuccess = "pay-operation-success",
- WithdrawOperationError = "withdraw-error",
- ReserveNotYetFound = "reserve-not-yet-found",
- ReserveOperationError = "reserve-error",
- InternalError = "internal-error",
- PendingOperationProcessed = "pending-operation-processed",
- ProposalRefused = "proposal-refused",
- ReserveRegisteredWithBank = "reserve-registered-with-bank",
- DepositOperationError = "deposit-operation-error",
-}
-
-export interface ProposalAcceptedNotification {
- type: NotificationType.ProposalAccepted;
- proposalId: string;
-}
-
-export interface InternalErrorNotification {
- type: NotificationType.InternalError;
- message: string;
- exception: any;
-}
-
-export interface ReserveNotYetFoundNotification {
- type: NotificationType.ReserveNotYetFound;
- reservePub: string;
-}
-
-export interface CoinWithdrawnNotification {
- type: NotificationType.CoinWithdrawn;
-}
-
-export interface RefundStartedNotification {
- type: NotificationType.RefundStarted;
-}
-
-export interface RefundQueriedNotification {
- type: NotificationType.RefundQueried;
-}
-
-export interface ProposalDownloadedNotification {
- type: NotificationType.ProposalDownloaded;
- proposalId: string;
-}
-
-export interface RefundsSubmittedNotification {
- type: NotificationType.RefundsSubmitted;
- proposalId: string;
-}
-
-export interface RecoupStartedNotification {
- type: NotificationType.RecoupStarted;
-}
-
-export interface RecoupFinishedNotification {
- type: NotificationType.RecoupFinished;
-}
-
-export interface RefreshMeltedNotification {
- type: NotificationType.RefreshMelted;
-}
-
-export interface RefreshRevealedNotification {
- type: NotificationType.RefreshRevealed;
-}
-
-export interface RefreshStartedNotification {
- type: NotificationType.RefreshStarted;
-}
-
-export interface RefreshRefusedNotification {
- type: NotificationType.RefreshUnwarranted;
-}
-
-export interface ReserveConfirmedNotification {
- type: NotificationType.ReserveConfirmed;
-}
-
-export interface WithdrawalGroupCreatedNotification {
- type: NotificationType.WithdrawGroupCreated;
- withdrawalGroupId: string;
-}
-
-export interface WithdrawalGroupFinishedNotification {
- type: NotificationType.WithdrawGroupFinished;
- reservePub: string;
-}
-
-export interface WaitingForRetryNotification {
- type: NotificationType.WaitingForRetry;
- numPending: number;
- numGivingLiveness: number;
- numDue: number;
-}
-
-export interface RefundFinishedNotification {
- type: NotificationType.RefundFinished;
-}
-
-export interface ExchangeAddedNotification {
- type: NotificationType.ExchangeAdded;
-}
-
-export interface ExchangeOperationErrorNotification {
- type: NotificationType.ExchangeOperationError;
- error: TalerErrorDetail;
-}
-
-export interface RefreshOperationErrorNotification {
- type: NotificationType.RefreshOperationError;
- error: TalerErrorDetail;
-}
+ TransactionStateTransition = "transaction-state-transition",
+ /**
+ * @deprecated
+ */
+ WithdrawalOperationTransition = "withdrawal-operation-transition",
+ ExchangeStateTransition = "exchange-state-transition",
+ Idle = "idle",
+ TaskObservabilityEvent = "task-observability-event",
+ RequestObservabilityEvent = "request-observability-event",
+}
+
+export interface ErrorInfoSummary {
+ code: number;
+ hint?: string;
+ message?: string;
+}
+
+export interface TransactionStateTransitionNotification {
+ type: NotificationType.TransactionStateTransition;
+ transactionId: string;
+ oldTxState: TransactionState;
+ newTxState: TransactionState;
+ errorInfo?: ErrorInfoSummary;
+
+ /**
+ * Additional "user data" that is dependent on the
+ * state transition.
+ *
+ * Usage should be avoided.
+ *
+ * Currently used to notify the iOS app about
+ * the KYC URL.
+ */
+ experimentalUserData?: any;
+}
+
+export interface ExchangeStateTransitionNotification {
+ type: NotificationType.ExchangeStateTransition;
+ /**
+ * Identification of the exchange entry that this
+ * notification is about.
+ */
+ exchangeBaseUrl: string;
+
+ /**
+ * If missing, the notification means that
+ * the exchange entry is newly created.
+ */
+ oldExchangeState?: ExchangeEntryState;
+
+ /**
+ * New state of the exchange.
+ */
+ newExchangeState: ExchangeEntryState;
+
+ /**
+ * Summary of the error that occurred when trying to update the exchange entry,
+ * if applicable.
+ */
+ errorInfo?: ErrorInfoSummary;
+}
+
+export interface BalanceChangeNotification {
+ type: NotificationType.BalanceChange;
+
+ /**
+ * Transaction ID of the transaction that caused the balance update.
+ *
+ * Only used as a hint for debugging, should not be relied upon by clients.
+ */
+ hintTransactionId: string;
+}
+
+export interface TaskProgressNotification {
+ type: NotificationType.TaskObservabilityEvent;
+ taskId: string;
+ event: ObservabilityEvent;
+}
+
+export interface RequestProgressNotification {
+ type: NotificationType.RequestObservabilityEvent;
+ requestId: string;
+ operation: string;
+ event: ObservabilityEvent;
+}
+
+export enum ObservabilityEventType {
+ HttpFetchStart = "http-fetch-start",
+ HttpFetchFinishError = "http-fetch-finish-error",
+ HttpFetchFinishSuccess = "http-fetch-finish-success",
+ DbQueryStart = "db-query-start",
+ DbQueryFinishSuccess = "db-query-finish-success",
+ DbQueryFinishError = "db-query-finish-error",
+ RequestStart = "request-start",
+ RequestFinishSuccess = "request-finish-success",
+ RequestFinishError = "request-finish-error",
+ TaskStart = "task-start",
+ TaskStop = "task-stop",
+ TaskReset = "task-reset",
+ ShepherdTaskResult = "sheperd-task-result",
+ DeclareTaskDependency = "declare-task-dependency",
+ CryptoStart = "crypto-start",
+ CryptoFinishSuccess = "crypto-finish-success",
+ CryptoFinishError = "crypto-finish-error",
+ Message = "message",
+}
+
+export type ObservabilityEvent =
+ | {
+ id: string;
+ when: AbsoluteTime;
+ type: ObservabilityEventType.HttpFetchStart;
+ url: string;
+ }
+ | {
+ id: string;
+ when: AbsoluteTime;
+ type: ObservabilityEventType.HttpFetchFinishSuccess;
+ url: string;
+ status: number;
+ }
+ | {
+ id: string;
+ when: AbsoluteTime;
+ type: ObservabilityEventType.HttpFetchFinishError;
+ url: string;
+ error: TalerErrorDetail;
+ }
+ | {
+ type: ObservabilityEventType.DbQueryStart;
+ name: string;
+ location: string;
+ }
+ | {
+ type: ObservabilityEventType.DbQueryFinishSuccess;
+ name: string;
+ location: string;
+ }
+ | {
+ type: ObservabilityEventType.DbQueryFinishError;
+ name: string;
+ location: string;
+ }
+ | {
+ type: ObservabilityEventType.RequestStart;
+ }
+ | {
+ type: ObservabilityEventType.RequestFinishSuccess;
+ durationMs: number;
+ }
+ | {
+ type: ObservabilityEventType.RequestFinishError;
+ }
+ | {
+ type: ObservabilityEventType.TaskStart;
+ taskId: string;
+ }
+ | {
+ type: ObservabilityEventType.TaskStop;
+ taskId: string;
+ }
+ | {
+ type: ObservabilityEventType.TaskReset;
+ taskId: string;
+ }
+ | {
+ type: ObservabilityEventType.DeclareTaskDependency;
+ taskId: string;
+ }
+ | {
+ type: ObservabilityEventType.CryptoStart;
+ operation: string;
+ }
+ | {
+ type: ObservabilityEventType.CryptoFinishSuccess;
+ operation: string;
+ }
+ | {
+ type: ObservabilityEventType.CryptoFinishError;
+ operation: string;
+ }
+ | {
+ type: ObservabilityEventType.ShepherdTaskResult;
+ resultType: string;
+ }
+ | {
+ type: ObservabilityEventType.Message;
+ contents: string;
+ };
export interface BackupOperationErrorNotification {
type: NotificationType.BackupOperationError;
error: TalerErrorDetail;
}
-
-export interface RefundStatusOperationErrorNotification {
- type: NotificationType.RefundStatusOperationError;
- error: TalerErrorDetail;
-}
-
-export interface RefundApplyOperationErrorNotification {
- type: NotificationType.RefundApplyOperationError;
- error: TalerErrorDetail;
-}
-
-export interface PayOperationErrorNotification {
- type: NotificationType.PayOperationError;
- error: TalerErrorDetail;
-}
-
-export interface ProposalOperationErrorNotification {
- type: NotificationType.ProposalOperationError;
- error: TalerErrorDetail;
-}
-
-export interface TipOperationErrorNotification {
- type: NotificationType.TipOperationError;
- error: TalerErrorDetail;
-}
-
-export interface WithdrawOperationErrorNotification {
- type: NotificationType.WithdrawOperationError;
- error: TalerErrorDetail;
-}
-
-export interface RecoupOperationErrorNotification {
- type: NotificationType.RecoupOperationError;
- error: TalerErrorDetail;
-}
-
-export interface DepositOperationErrorNotification {
- type: NotificationType.DepositOperationError;
- error: TalerErrorDetail;
-}
-
-export interface ReserveOperationErrorNotification {
- type: NotificationType.ReserveOperationError;
- error: TalerErrorDetail;
-}
-
-export interface ReserveCreatedNotification {
- type: NotificationType.ReserveCreated;
- reservePub: string;
-}
-
-export interface PendingOperationProcessedNotification {
- type: NotificationType.PendingOperationProcessed;
- id: string;
-}
-
-export interface ProposalRefusedNotification {
- type: NotificationType.ProposalRefused;
-}
-
-export interface ReserveRegisteredWithBankNotification {
- type: NotificationType.ReserveRegisteredWithBank;
-}
-
/**
- * Notification sent when a pay (or pay replay) operation succeeded.
+ * This notification is required to signal UI that
+ * the withdrawal operation changed the state.
*
- * We send this notification because the confirmPay request can return
- * a "confirmed" response that indicates that the payment has been confirmed
- * by the user, but we're still waiting for the payment to succeed or fail.
+ * https://bugs.gnunet.org/view.php?id=8099
*/
-export interface PayOperationSuccessNotification {
- type: NotificationType.PayOperationSuccess;
- proposalId: string;
+export interface WithdrawalOperationTransitionNotification {
+ type: NotificationType.WithdrawalOperationTransition;
+ uri: string;
+}
+
+export interface IdleNotification {
+ type: NotificationType.Idle;
}
export type WalletNotification =
+ | BalanceChangeNotification
+ | WithdrawalOperationTransitionNotification
| BackupOperationErrorNotification
- | WithdrawOperationErrorNotification
- | ReserveOperationErrorNotification
- | ExchangeAddedNotification
- | ExchangeOperationErrorNotification
- | RefreshOperationErrorNotification
- | RefundStatusOperationErrorNotification
- | RefundApplyOperationErrorNotification
- | ProposalOperationErrorNotification
- | PayOperationErrorNotification
- | TipOperationErrorNotification
- | ProposalAcceptedNotification
- | ProposalDownloadedNotification
- | RefundsSubmittedNotification
- | RecoupStartedNotification
- | RecoupFinishedNotification
- | RefreshMeltedNotification
- | RefreshRevealedNotification
- | RefreshStartedNotification
- | RefreshRefusedNotification
- | ReserveCreatedNotification
- | ReserveConfirmedNotification
- | WithdrawalGroupFinishedNotification
- | WaitingForRetryNotification
- | RefundStartedNotification
- | RefundFinishedNotification
- | RefundQueriedNotification
- | WithdrawalGroupCreatedNotification
- | CoinWithdrawnNotification
- | RecoupOperationErrorNotification
- | DepositOperationErrorNotification
- | InternalErrorNotification
- | PendingOperationProcessedNotification
- | ProposalRefusedNotification
- | ReserveRegisteredWithBankNotification
- | ReserveNotYetFoundNotification
- | PayOperationSuccessNotification;
+ | ExchangeStateTransitionNotification
+ | TransactionStateTransitionNotification
+ | TaskProgressNotification
+ | RequestProgressNotification
+ | IdleNotification;
diff --git a/packages/taler-util/src/observability.ts b/packages/taler-util/src/observability.ts
new file mode 100644
index 000000000..0171142c8
--- /dev/null
+++ b/packages/taler-util/src/observability.ts
@@ -0,0 +1,98 @@
+/*
+ This file is part of GNU Taler
+ (C) 2024 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 {
+ AbsoluteTime,
+ CancellationToken,
+ ObservabilityEvent,
+} from "./index.js";
+import {
+ HttpRequestLibrary,
+ HttpRequestOptions,
+ HttpResponse,
+} from "./http-common.js";
+import { ObservabilityEventType } from "./notifications.js";
+import { getErrorDetailFromException } from "./errors.js";
+
+/**
+ * Observability sink can be passed into various operations (HTTP requests, DB access)
+ * to do structured logging within a particular context (task, request, ...).
+ */
+export interface ObservabilityContext {
+ observe(evt: ObservabilityEvent): void;
+}
+
+let seqId = 1000;
+
+export class ObservableHttpClientLibrary implements HttpRequestLibrary {
+ private readonly cancelatorById = new Map<string, CancellationToken.Source>();
+ constructor(
+ private impl: HttpRequestLibrary,
+ private oc: ObservabilityContext,
+ ) {}
+
+ public cancelRequest(id: string): void {
+ const cancelator = this.cancelatorById.get(id);
+ if (!cancelator) return;
+ cancelator.cancel();
+ }
+
+ async fetch(
+ url: string,
+ opt?: HttpRequestOptions | undefined,
+ ): Promise<HttpResponse> {
+ const id = `req-${seqId}`;
+ seqId = seqId + 1;
+
+ const cancelator = CancellationToken.create();
+ if (opt?.cancellationToken) {
+ opt.cancellationToken.onCancelled(cancelator.cancel);
+ }
+ this.cancelatorById.set(id, cancelator);
+
+ this.oc.observe({
+ id,
+ when: AbsoluteTime.now(),
+ type: ObservabilityEventType.HttpFetchStart,
+ url: url,
+ });
+
+ const optsWithCancel = opt ?? {};
+ optsWithCancel.cancellationToken = cancelator.token;
+ try {
+ const res = await this.impl.fetch(url, optsWithCancel);
+ this.oc.observe({
+ id,
+ when: AbsoluteTime.now(),
+ type: ObservabilityEventType.HttpFetchFinishSuccess,
+ url,
+ status: res.status,
+ });
+ return res;
+ } catch (e) {
+ this.oc.observe({
+ id,
+ when: AbsoluteTime.now(),
+ type: ObservabilityEventType.HttpFetchFinishError,
+ url,
+ error: getErrorDetailFromException(e),
+ });
+ throw e;
+ } finally {
+ this.cancelatorById.delete(id);
+ }
+ }
+}
diff --git a/packages/taler-util/src/operation.ts b/packages/taler-util/src/operation.ts
new file mode 100644
index 000000000..e2ab9d4e4
--- /dev/null
+++ b/packages/taler-util/src/operation.ts
@@ -0,0 +1,198 @@
+/*
+ This file is part of GNU Taler
+ (C) 2023-2024 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 {
+ HttpResponse,
+ readResponseJsonOrErrorCode,
+ readSuccessResponseJsonOrThrow,
+ readTalerErrorResponse,
+} from "./http-common.js";
+import {
+ Codec,
+ HttpStatusCode,
+ TalerError,
+ TalerErrorCode,
+ TalerErrorDetail,
+} from "./index.js";
+
+type OperationFailWithBodyOrNever<ErrorEnum, ErrorMap> =
+ ErrorEnum extends keyof ErrorMap ? OperationFailWithBody<ErrorMap> : never;
+
+export type OperationResult<Body, ErrorEnum, K = never> =
+ | OperationOk<Body>
+ | OperationAlternative<ErrorEnum, any>
+ | OperationFail<ErrorEnum>
+ | OperationFailWithBodyOrNever<ErrorEnum, K>;
+
+export function isOperationOk<T, E>(
+ c: OperationResult<T, E>,
+): c is OperationOk<T> {
+ return c.type === "ok";
+}
+
+export function isOperationFail<T, E>(
+ c: OperationResult<T, E>,
+): c is OperationFail<E> {
+ return c.type === "fail";
+}
+
+/**
+ * successful operation
+ */
+export interface OperationOk<BodyT> {
+ type: "ok";
+
+ /**
+ * Parsed response body.
+ */
+ body: BodyT;
+}
+
+/**
+ * unsuccessful operation, see details
+ */
+export interface OperationFail<T> {
+ type: "fail";
+
+ /**
+ * Error case (either HTTP status code or TalerErrorCode)
+ */
+ case: T;
+
+ detail: TalerErrorDetail;
+}
+
+/**
+ * unsuccessful operation, see body
+ */
+export interface OperationAlternative<T, B> {
+ type: "fail";
+
+ case: T;
+ body: B;
+}
+
+export interface OperationFailWithBody<B> {
+ type: "fail";
+
+ case: keyof B;
+ body: B[OperationFailWithBody<B>["case"]];
+}
+
+export async function opSuccessFromHttp<T>(
+ resp: HttpResponse,
+ codec: Codec<T>,
+): Promise<OperationOk<T>> {
+ const body = await readSuccessResponseJsonOrThrow(resp, codec);
+ return { type: "ok" as const, body };
+}
+
+/**
+ * Success case, but instead of the body we're returning a fixed response
+ * to the client.
+ */
+export function opFixedSuccess<T>(body: T): OperationOk<T> {
+ return { type: "ok" as const, body };
+}
+
+export function opEmptySuccess(resp: HttpResponse): OperationOk<void> {
+ return { type: "ok" as const, body: void 0 };
+}
+
+export async function opKnownFailureWithBody<B>(
+ case_: keyof B,
+ body: B[typeof case_],
+): Promise<OperationFailWithBody<B>> {
+ return { type: "fail", case: case_, body };
+}
+
+export async function opKnownAlternativeFailure<T extends HttpStatusCode, B>(
+ resp: HttpResponse,
+ s: T,
+ codec: Codec<B>,
+): Promise<OperationAlternative<T, B>> {
+ const body = (await readResponseJsonOrErrorCode(resp, codec)).response;
+ return { type: "fail", case: s, body };
+}
+
+export async function opKnownHttpFailure<T extends HttpStatusCode>(
+ s: T,
+ resp: HttpResponse,
+): Promise<OperationFail<T>> {
+ const detail = await readTalerErrorResponse(resp);
+ return { type: "fail", case: s, detail };
+}
+
+export function opKnownTalerFailure<T extends TalerErrorCode>(
+ s: T,
+ detail: TalerErrorDetail,
+): OperationFail<T> {
+ return { type: "fail", case: s, detail };
+}
+
+export function opUnknownFailure(resp: HttpResponse, error: TalerErrorDetail): never {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
+ {
+ requestUrl: resp.requestUrl,
+ requestMethod: resp.requestMethod,
+ httpStatusCode: resp.status,
+ errorResponse: error,
+ },
+ `Unexpected HTTP status ${resp.status} in response`,
+ );
+}
+
+/**
+ * Convenience function to throw an error if the operation is not a success.
+ */
+export function narrowOpSuccessOrThrow<Body, ErrorEnum>(
+ opName: string,
+ opRes: OperationResult<Body, ErrorEnum>,
+): asserts opRes is OperationOk<Body> {
+ if (opRes.type !== "ok") {
+ throw TalerError.fromDetail(
+ TalerErrorCode.GENERIC_CLIENT_INTERNAL_ERROR,
+ {
+ operation: opName,
+ error: String(opRes.case),
+ detail: "detail" in opRes ? opRes.detail : undefined,
+ },
+ `Operation ${opName} failed: ${String(opRes.case)}`,
+ );
+ }
+}
+
+export type ResultByMethod<
+ TT extends object,
+ p extends keyof TT,
+> = TT[p] extends (...args: any[]) => infer Ret
+ ? Ret extends Promise<infer Result>
+ ? Result extends OperationResult<any, any>
+ ? Result
+ : never
+ : never //api always use Promises
+ : never; //error cases just for functions
+
+export type FailCasesByMethod<TT extends object, p extends keyof TT> = Exclude<
+ ResultByMethod<TT, p>,
+ OperationOk<any>
+>;
+
+export type RedirectResult = { redirectURL: URL }
diff --git a/packages/taler-util/src/payto.ts b/packages/taler-util/src/payto.ts
index 8eb0b88a8..a471d0b87 100644
--- a/packages/taler-util/src/payto.ts
+++ b/packages/taler-util/src/payto.ts
@@ -15,6 +15,7 @@
*/
import { generateFakeSegwitAddress } from "./bitcoin.js";
+import { Codec, Context, DecodingError, renderContext } from "./codec.js";
import { URLSearchParams } from "./url.js";
export type PaytoUri =
@@ -23,8 +24,29 @@ export type PaytoUri =
| PaytoUriTalerBank
| PaytoUriBitcoin;
+declare const __payto_str: unique symbol;
+export type PaytoString = string & { [__payto_str]: true };
+
+export function codecForPaytoString(): Codec<PaytoString> {
+ return {
+ decode(x: any, c?: Context): PaytoString {
+ if (typeof x !== "string") {
+ throw new DecodingError(
+ `expected string at ${renderContext(c)} but got ${typeof x}`,
+ );
+ }
+ if (!x.startsWith(paytoPfx)) {
+ throw new DecodingError(
+ `expected start with payto at ${renderContext(c)} but got "${x}"`,
+ );
+ }
+ return x as PaytoString;
+ },
+ };
+}
+
export interface PaytoUriGeneric {
- targetType: string;
+ targetType: PaytoType | string;
targetPath: string;
params: { [name: string]: string };
}
@@ -50,11 +72,77 @@ export interface PaytoUriTalerBank extends PaytoUriGeneric {
export interface PaytoUriBitcoin extends PaytoUriGeneric {
isKnown: true;
targetType: "bitcoin";
+ address: string;
segwitAddrs: Array<string>;
}
const paytoPfx = "payto://";
+export type PaytoType = "iban" | "bitcoin" | "x-taler-bank";
+
+export function buildPayto(
+ type: "iban",
+ iban: string,
+ bic: string | undefined,
+): PaytoUriIBAN;
+export function buildPayto(
+ type: "bitcoin",
+ address: string,
+ reserve: string | undefined,
+): PaytoUriBitcoin;
+export function buildPayto(
+ type: "x-taler-bank",
+ host: string,
+ account: string,
+): PaytoUriTalerBank;
+export function buildPayto(
+ type: PaytoType,
+ first: string,
+ second?: string,
+): PaytoUriGeneric {
+ switch (type) {
+ case "bitcoin": {
+ const uppercased = first.toUpperCase();
+ const result: PaytoUriBitcoin = {
+ isKnown: true,
+ targetType: "bitcoin",
+ targetPath: first,
+ address: uppercased,
+ params: {},
+ segwitAddrs: !second ? [] : generateFakeSegwitAddress(second, first),
+ };
+ return result;
+ }
+ case "iban": {
+ const uppercased = first.toUpperCase();
+ const result: PaytoUriIBAN = {
+ isKnown: true,
+ targetType: "iban",
+ iban: uppercased,
+ params: {},
+ targetPath: !second ? uppercased : `${second}/${uppercased}`,
+ };
+ return result;
+ }
+ case "x-taler-bank": {
+ if (!second) throw Error("missing account for payto://x-taler-bank");
+ const result: PaytoUriTalerBank = {
+ isKnown: true,
+ targetType: "x-taler-bank",
+ host: first,
+ account: second,
+ params: {},
+ targetPath: `${first}/${second}`,
+ };
+ return result;
+ }
+ default: {
+ const unknownType: never = type;
+ throw Error(`unknown payto:// type ${unknownType}`);
+ }
+ }
+}
+
/**
* Add query parameters to a payto URI
*/
@@ -80,14 +168,13 @@ export function addPaytoQueryParams(
* @param p
* @returns
*/
-export function stringifyPaytoUri(p: PaytoUri): string {
- const url = `${paytoPfx}${p.targetType}/${p.targetPath}`;
+export function stringifyPaytoUri(p: PaytoUri): PaytoString {
+ const url = new URL(`${paytoPfx}${p.targetType}/${p.targetPath}`);
const paramList = !p.params ? [] : Object.entries(p.params);
- if (paramList.length > 0) {
- const search = paramList.map(([key, value]) => `${key}=${value}`).join("&");
- return `${url}?${search}`;
- }
- return url;
+ paramList.forEach(([key, value]) => {
+ url.searchParams.set(key, value);
+ });
+ return url.href as PaytoString;
}
/**
@@ -139,13 +226,13 @@ export function parsePaytoUri(s: string): PaytoUri | undefined {
let iban: string | undefined = undefined;
let bic: string | undefined = undefined;
if (parts.length === 1) {
- iban = parts[0];
+ iban = parts[0].toUpperCase();
}
if (parts.length === 2) {
bic = parts[0];
- iban = parts[1];
+ iban = parts[1].toUpperCase();
} else {
- iban = targetPath;
+ iban = targetPath.toUpperCase();
}
return {
isKnown: true,
@@ -163,10 +250,12 @@ export function parsePaytoUri(s: string): PaytoUri | undefined {
? []
: generateFakeSegwitAddress(reserve, targetPath);
+ const uppercased = targetType.toUpperCase();
const result: PaytoUriBitcoin = {
isKnown: true,
targetPath,
targetType,
+ address: uppercased,
params,
segwitAddrs,
};
@@ -180,3 +269,25 @@ export function parsePaytoUri(s: string): PaytoUri | undefined {
isKnown: false,
};
}
+
+export function talerPaytoFromExchangeReserve(
+ exchangeBaseUrl: string,
+ reservePub: string,
+): string {
+ const url = new URL(exchangeBaseUrl);
+ let proto: string;
+ if (url.protocol === "http:") {
+ proto = "taler-reserve-http";
+ } else if (url.protocol === "https:") {
+ proto = "taler-reserve";
+ } else {
+ throw Error(`unsupported exchange base URL protocol (${url.protocol})`);
+ }
+
+ let path = url.pathname;
+ if (!path.endsWith("/")) {
+ path = path + "/";
+ }
+
+ return `payto://${proto}/${url.host}${url.pathname}${reservePub}`;
+}
diff --git a/packages/taler-util/src/promises.ts b/packages/taler-util/src/promises.ts
new file mode 100644
index 000000000..bc1e40260
--- /dev/null
+++ b/packages/taler-util/src/promises.ts
@@ -0,0 +1,112 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 GNUnet e.V.
+
+ 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.
+
+ 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
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * An opened promise.
+ *
+ * @see {@link openPromise}
+ */
+export interface OpenedPromise<T> {
+ promise: Promise<T>;
+ resolve: (val: T) => void;
+ reject: (err: any) => void;
+ lastError?: any;
+}
+
+/**
+ * Get an unresolved promise together with its extracted resolve / reject
+ * function.
+ *
+ * Recent ECMAScript proposals also call this a promise capability.
+ */
+export function openPromise<T>(): OpenedPromise<T> {
+ let resolve: ((x?: any) => void) | null = null;
+ let promiseReject: ((reason?: any) => void) | null = null;
+ const promise = new Promise<T>((res, rej) => {
+ resolve = res;
+ promiseReject = rej;
+ });
+ if (!(resolve && promiseReject)) {
+ // Never happens, unless JS implementation is broken
+ throw Error("JS implementation is broken");
+ }
+ const result: OpenedPromise<T> = { resolve, reject: promiseReject, promise };
+ function saveLastError(reason?: any) {
+ result.lastError = reason;
+ promiseReject!(reason);
+ }
+ result.reject = saveLastError;
+ return result;
+}
+
+export class AsyncCondition {
+ private promCap?: OpenedPromise<void> = undefined;
+ constructor() {}
+
+ wait(): Promise<void> {
+ if (!this.promCap) {
+ this.promCap = openPromise<void>();
+ }
+ return this.promCap.promise;
+ }
+
+ trigger(): void {
+ if (this.promCap) {
+ this.promCap.resolve();
+ }
+ this.promCap = undefined;
+ }
+}
+
+/**
+ * Flag that can be raised to notify asynchronous waiters.
+ *
+ * You can think of it as a promise that can
+ * be un-resolved.
+ */
+export class AsyncFlag {
+ private promCap?: OpenedPromise<void> = undefined;
+ private internalFlagRaised: boolean = false;
+
+ constructor() {}
+
+ /**
+ * Wait until the flag is raised.
+ *
+ * Reset if before returning.
+ */
+ wait(): Promise<void> {
+ if (this.internalFlagRaised) {
+ return Promise.resolve();
+ }
+ if (!this.promCap) {
+ this.promCap = openPromise<void>();
+ }
+ return this.promCap.promise;
+ }
+
+ raise(): void {
+ this.internalFlagRaised = true;
+ if (this.promCap) {
+ this.promCap.resolve();
+ }
+ }
+
+ reset(): void {
+ this.internalFlagRaised = false;
+ this.promCap = undefined;
+ }
+}
diff --git a/packages/taler-util/src/qtart.ts b/packages/taler-util/src/qtart.ts
new file mode 100644
index 000000000..e298a157c
--- /dev/null
+++ b/packages/taler-util/src/qtart.ts
@@ -0,0 +1,35 @@
+// @ts-ignore
+import * as _qjsOsImp from "os";
+// @ts-ignore
+import * as _qjsStdImp from "std";
+
+export interface QjsHttpResp {
+ status: number;
+ data: ArrayBuffer;
+ headers?: string[];
+}
+
+export interface QjsHttpOptions {
+ method: string;
+ debug?: boolean;
+ data?: ArrayBuffer;
+ headers?: string[];
+}
+
+export interface QjsOsLib {
+ fetchHttp(url: string, options?: QjsHttpOptions): Promise<QjsHttpResp>;
+ postMessageToHost(s: string): void;
+ setMessageFromHostHandler(h: (s: string) => void): void;
+ rename(oldPath: string, newPath: string): number;
+ remove(path: string): number;
+}
+
+export interface QjsStdLib {
+ writeFile(filename: string, contents: string): void;
+ loadFile(filename: string): string;
+}
+
+// This is not the nodejs "os" module, but the qjs "os" module.
+export const qjsOs: QjsOsLib = _qjsOsImp as any;
+
+export const qjsStd: QjsStdLib = _qjsStdImp as any;
diff --git a/packages/taler-util/src/rfc3548.ts b/packages/taler-util/src/rfc3548.ts
new file mode 100644
index 000000000..2dd18cdfc
--- /dev/null
+++ b/packages/taler-util/src/rfc3548.ts
@@ -0,0 +1,60 @@
+/*
+ This file is part of GNU Taler
+ (C) 2024 Taler Systems SA
+
+ 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 { getRandomBytes } from "./taler-crypto.js";
+
+const encTable = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
+
+/**
+ * base32 RFC 3548
+ */
+export function encodeRfc3548Base32(data: ArrayBuffer) {
+ const dataBytes = new Uint8Array(data);
+ let sb = "";
+ const size = data.byteLength;
+ let bitBuf = 0;
+ let numBits = 0;
+ let pos = 0;
+ while (pos < size || numBits > 0) {
+ if (pos < size && numBits < 5) {
+ const d = dataBytes[pos++];
+ bitBuf = (bitBuf << 8) | d;
+ numBits += 8;
+ }
+ if (numBits < 5) {
+ // zero-padding
+ bitBuf = bitBuf << (5 - numBits);
+ numBits = 5;
+ }
+ const v = (bitBuf >>> (numBits - 5)) & 31;
+ sb += encTable[v];
+ numBits -= 5;
+ }
+ return sb;
+}
+
+export function isRfc3548Base32Charset(s: string): boolean {
+ for (let idx = 0; idx < s.length; idx++) {
+ const c = s.charAt(idx);
+ if (encTable.indexOf(c) === -1) return false;
+ }
+ return true;
+}
+
+export function randomRfc3548Base32Key(): string {
+ const buf = getRandomBytes(20);
+ return encodeRfc3548Base32(buf);
+}
diff --git a/packages/taler-util/src/sha256.ts b/packages/taler-util/src/sha256.ts
index 321e5d827..ba8f09279 100644
--- a/packages/taler-util/src/sha256.ts
+++ b/packages/taler-util/src/sha256.ts
@@ -145,7 +145,7 @@ export class HashSha256 {
}
// Resets hash state making it possible
- // to re-use this instance to hash other data.
+ // to reuse this instance to hash other data.
reset(): this {
this.state[0] = 0x6a09e667;
this.state[1] = 0xbb67ae85;
diff --git a/packages/taler-util/src/taler-crypto.test.ts b/packages/taler-util/src/taler-crypto.test.ts
index 913bf4348..021730c7e 100644
--- a/packages/taler-util/src/taler-crypto.test.ts
+++ b/packages/taler-util/src/taler-crypto.test.ts
@@ -21,10 +21,10 @@ import test from "ava";
import {
encodeCrock,
decodeCrock,
- ecdheGetPublic,
+ ecdhGetPublic,
eddsaGetPublic,
- keyExchangeEddsaEcdhe,
- keyExchangeEcdheEddsa,
+ keyExchangeEddsaEcdh,
+ keyExchangeEcdhEddsa,
stringToBytes,
bytesToString,
deriveBSeed,
@@ -37,8 +37,9 @@ import {
getRandomBytes,
bigintToNaclArr,
bigintFromNaclArr,
+ kdf,
} from "./taler-crypto.js";
-import { sha512, kdf } from "./kdf.js";
+import { sha512 } from "./kdf.js";
import * as nacl from "./nacl-fast.js";
import { initNodePrng } from "./prng-node.js";
@@ -127,19 +128,19 @@ test("taler-exchange-tvg eddsa_ecdh", (t) => {
const key_material =
"PKZ42Z56SVK2796HG1QYBRJ6ZQM2T9QGA3JA4AAZ8G7CWK9FPX175Q9JE5P0ZAX3HWWPHAQV4DPCK10R9X3SAXHRV0WF06BHEC2ZTKR";
- const myEcdhePub = ecdheGetPublic(decodeCrock(priv_ecdhe));
+ const myEcdhePub = ecdhGetPublic(decodeCrock(priv_ecdhe));
t.deepEqual(encodeCrock(myEcdhePub), pub_ecdhe);
const myEddsaPub = eddsaGetPublic(decodeCrock(priv_eddsa));
t.deepEqual(encodeCrock(myEddsaPub), pub_eddsa);
- const myKm1 = keyExchangeEddsaEcdhe(
+ const myKm1 = keyExchangeEddsaEcdh(
decodeCrock(priv_eddsa),
decodeCrock(pub_ecdhe),
);
t.deepEqual(encodeCrock(myKm1), key_material);
- const myKm2 = keyExchangeEcdheEddsa(
+ const myKm2 = keyExchangeEcdhEddsa(
decodeCrock(priv_ecdhe),
decodeCrock(pub_eddsa),
);
@@ -193,19 +194,19 @@ test("taler-exchange-tvg eddsa_ecdh #2", (t) => {
const key_material =
"G6RA58N61K7MT3WA13Q7VRTE1FQS6H43RX9HK8Z5TGAB61601GEGX51JRHHQMNKNM2R9AVC1STSGQDRHGKWVYP584YGBCTVMMJYQF30";
- const myEcdhePub = ecdheGetPublic(decodeCrock(priv_ecdhe));
+ const myEcdhePub = ecdhGetPublic(decodeCrock(priv_ecdhe));
t.deepEqual(encodeCrock(myEcdhePub), pub_ecdhe);
const myEddsaPub = eddsaGetPublic(decodeCrock(priv_eddsa));
t.deepEqual(encodeCrock(myEddsaPub), pub_eddsa);
- const myKm1 = keyExchangeEddsaEcdhe(
+ const myKm1 = keyExchangeEddsaEcdh(
decodeCrock(priv_eddsa),
decodeCrock(pub_ecdhe),
);
t.deepEqual(encodeCrock(myKm1), key_material);
- const myKm2 = keyExchangeEcdheEddsa(
+ const myKm2 = keyExchangeEcdhEddsa(
decodeCrock(priv_ecdhe),
decodeCrock(pub_eddsa),
);
diff --git a/packages/taler-util/src/taler-crypto.ts b/packages/taler-util/src/taler-crypto.ts
index 113e4194b..950161b10 100644
--- a/packages/taler-util/src/taler-crypto.ts
+++ b/packages/taler-util/src/taler-crypto.ts
@@ -21,20 +21,23 @@
/**
* Imports.
*/
-import * as nacl from "./nacl-fast.js";
-import { kdf } from "./kdf.js";
import bigint from "big-integer";
+import * as fflate from "fflate";
+import { AmountLike, Amounts } from "./amounts.js";
+import * as argon2 from "./argon2.js";
+import { canonicalJson } from "./helpers.js";
+import { hmacSha256, hmacSha512 } from "./kdf.js";
+import { Logger } from "./logging.js";
+import * as nacl from "./nacl-fast.js";
+import { secretbox } from "./nacl-fast.js";
import {
CoinEnvelope,
CoinPublicKeyString,
- DenominationPubKey,
DenomKeyType,
+ DenominationPubKey,
HashCodeString,
} from "./taler-types.js";
-import { Logger } from "./logging.js";
-import { secretbox } from "./nacl-fast.js";
-import * as fflate from "fflate";
-import { canonicalJson } from "./helpers.js";
+import { TalerProtocolDuration, TalerProtocolTimestamp } from "./time.js";
export type Flavor<T, FlavorT extends string> = T & {
_flavor?: `taler.${FlavorT}`;
@@ -55,7 +58,56 @@ export function getRandomBytesF<T extends number, N extends string>(
return nacl.randomBytes(n);
}
-const useNative = true;
+export const useNative = true;
+
+/**
+ * Interface of the native Taler runtime library.
+ */
+interface NativeTartLib {
+ decodeUtf8(buf: Uint8Array): string;
+ decodeUtf8(str: string): Uint8Array;
+ randomBytes(n: number): Uint8Array;
+ encodeCrock(buf: Uint8Array | ArrayBuffer): string;
+ decodeCrock(str: string): Uint8Array;
+ hash(buf: Uint8Array): Uint8Array;
+ hashArgon2id(
+ password: Uint8Array,
+ salt: Uint8Array,
+ iterations: number,
+ memorySize: number,
+ hashLength: number,
+ ): Uint8Array;
+ eddsaGetPublic(buf: Uint8Array): Uint8Array;
+ ecdheGetPublic(buf: Uint8Array): Uint8Array;
+ eddsaSign(msg: Uint8Array, priv: Uint8Array): Uint8Array;
+ eddsaVerify(msg: Uint8Array, sig: Uint8Array, pub: Uint8Array): boolean;
+ kdf(
+ outLen: number,
+ ikm: Uint8Array,
+ salt?: Uint8Array,
+ info?: Uint8Array,
+ ): Uint8Array;
+ keyExchangeEcdhEddsa(ecdhPriv: Uint8Array, eddsaPub: Uint8Array): Uint8Array;
+ keyExchangeEddsaEcdh(eddsaPriv: Uint8Array, ecdhPub: Uint8Array): Uint8Array;
+ rsaBlind(hmsg: Uint8Array, bks: Uint8Array, rsaPub: Uint8Array): Uint8Array;
+ rsaUnblind(
+ blindSig: Uint8Array,
+ rsaPub: Uint8Array,
+ bks: Uint8Array,
+ ): Uint8Array;
+ rsaVerify(hmsg: Uint8Array, rsaSig: Uint8Array, rsaPub: Uint8Array): boolean;
+ hashStateInit(): any;
+ hashStateUpdate(st: any, data: Uint8Array): any;
+ hashStateFinish(st: any): Uint8Array;
+}
+
+// @ts-ignore
+let tart: NativeTartLib | undefined;
+
+if (useNative) {
+ // @ts-ignore
+ tart = globalThis._tart;
+}
const encTable = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
@@ -71,7 +123,7 @@ function getValue(chr: string): number {
switch (chr) {
case "O":
case "o":
- a = "0;";
+ a = "0";
break;
case "i":
case "I":
@@ -101,9 +153,8 @@ function getValue(chr: string): number {
}
export function encodeCrock(data: ArrayBuffer): string {
- if (useNative && "_encodeCrock" in globalThis) {
- // @ts-ignore
- return globalThis._encodeCrock(data);
+ if (tart) {
+ return tart.encodeCrock(data);
}
const dataBytes = new Uint8Array(data);
let sb = "";
@@ -129,6 +180,44 @@ export function encodeCrock(data: ArrayBuffer): string {
return sb;
}
+export function kdf(
+ outputLength: number,
+ ikm: Uint8Array,
+ salt?: Uint8Array,
+ info?: Uint8Array,
+): Uint8Array {
+ if (tart) {
+ return tart.kdf(outputLength, ikm, salt, info);
+ }
+ salt = salt ?? new Uint8Array(64);
+ // extract
+ const prk = hmacSha512(salt, ikm);
+
+ info = info ?? new Uint8Array(0);
+
+ // expand
+ const N = Math.ceil(outputLength / 32);
+ const output = new Uint8Array(N * 32);
+ for (let i = 0; i < N; i++) {
+ let buf;
+ if (i == 0) {
+ buf = new Uint8Array(info.byteLength + 1);
+ buf.set(info, 0);
+ } else {
+ buf = new Uint8Array(info.byteLength + 1 + 32);
+ for (let j = 0; j < 32; j++) {
+ buf[j] = output[(i - 1) * 32 + j];
+ }
+ buf.set(info, 32);
+ }
+ buf[buf.length - 1] = i + 1;
+ const chunk = hmacSha256(prk, buf);
+ output.set(chunk, i * 32);
+ }
+
+ return output.slice(0, outputLength);
+}
+
/**
* HMAC-SHA512-SHA256 (see RFC 5869).
*/
@@ -142,9 +231,8 @@ export function kdfKw(args: {
}
export function decodeCrock(encoded: string): Uint8Array {
- if (useNative && "_decodeCrock" in globalThis) {
- // @ts-ignore
- return globalThis._decodeCrock(encoded);
+ if (tart) {
+ return tart.decodeCrock(encoded);
}
const size = encoded.length;
let bitpos = 0;
@@ -173,38 +261,71 @@ export function decodeCrock(encoded: string): Uint8Array {
return out;
}
+export async function hashArgon2id(
+ password: Uint8Array,
+ salt: Uint8Array,
+ iterations: number,
+ memorySize: number,
+ hashLength: number,
+): Promise<Uint8Array> {
+ if (tart) {
+ return tart.hashArgon2id(
+ password,
+ salt,
+ iterations,
+ memorySize,
+ hashLength,
+ );
+ }
+ return await argon2.hashArgon2id(
+ password,
+ salt,
+ iterations,
+ memorySize,
+ hashLength,
+ );
+}
+
export function eddsaGetPublic(eddsaPriv: Uint8Array): Uint8Array {
- if (useNative && "_eddsaGetPublic" in globalThis) {
- // @ts-ignore
- return globalThis._eddsaGetPublic(eddsaPriv);
+ if (tart) {
+ return tart.eddsaGetPublic(eddsaPriv);
}
const pair = nacl.crypto_sign_keyPair_fromSeed(eddsaPriv);
return pair.publicKey;
}
-export function ecdheGetPublic(ecdhePriv: Uint8Array): Uint8Array {
+export function ecdhGetPublic(ecdhePriv: Uint8Array): Uint8Array {
+ if (tart) {
+ return tart.ecdheGetPublic(ecdhePriv);
+ }
return nacl.scalarMult_base(ecdhePriv);
}
-export function keyExchangeEddsaEcdhe(
+export function keyExchangeEddsaEcdh(
eddsaPriv: Uint8Array,
- ecdhePub: Uint8Array,
+ ecdhPub: Uint8Array,
): Uint8Array {
+ if (tart) {
+ return tart.keyExchangeEddsaEcdh(eddsaPriv, ecdhPub);
+ }
const ph = hash(eddsaPriv);
const a = new Uint8Array(32);
for (let i = 0; i < 32; i++) {
a[i] = ph[i];
}
- const x = nacl.scalarMult(a, ecdhePub);
+ const x = nacl.scalarMult(a, ecdhPub);
return hash(x);
}
-export function keyExchangeEcdheEddsa(
- ecdhePriv: Uint8Array & MaterialEcdhePriv,
+export function keyExchangeEcdhEddsa(
+ ecdhPriv: Uint8Array & MaterialEcdhePriv,
eddsaPub: Uint8Array & MaterialEddsaPub,
): Uint8Array {
+ if (tart) {
+ return tart.keyExchangeEcdhEddsa(ecdhPriv, eddsaPub);
+ }
const curve25519Pub = nacl.sign_ed25519_pk_to_curve25519(eddsaPub);
- const x = nacl.scalarMult(ecdhePriv, curve25519Pub);
+ const x = nacl.scalarMult(ecdhPriv, curve25519Pub);
return hash(x);
}
@@ -271,7 +392,7 @@ function csKdfMod(
// Newer versions of node have TextEncoder and TextDecoder as a global,
// just like modern browsers.
// In older versions of node or environments that do not have these
-// globals, they must be polyfilled (by adding them to globa/globalThis)
+// globals, they must be polyfilled (by adding them to global/globalThis)
// before stringToBytes or bytesToString is called the first time.
let encoder: any;
@@ -365,6 +486,9 @@ export function rsaBlind(
bks: Uint8Array,
rsaPubEnc: Uint8Array,
): Uint8Array {
+ if (tart) {
+ return tart.rsaBlind(hm, bks, rsaPubEnc);
+ }
const rsaPub = rsaPubDecode(rsaPubEnc);
const data = rsaFullDomainHash(hm, rsaPub);
const r = rsaBlindingKeyDerive(rsaPub, bks);
@@ -378,6 +502,9 @@ export function rsaUnblind(
rsaPubEnc: Uint8Array,
bks: Uint8Array,
): Uint8Array {
+ if (tart) {
+ return tart.rsaUnblind(sig, rsaPubEnc, bks);
+ }
const rsaPub = rsaPubDecode(rsaPubEnc);
const blinded_s = loadBigInt(sig);
const r = rsaBlindingKeyDerive(rsaPub, bks);
@@ -391,6 +518,9 @@ export function rsaVerify(
rsaSig: Uint8Array,
rsaPubEnc: Uint8Array,
): boolean {
+ if (tart) {
+ return tart.rsaVerify(hm, rsaSig, rsaPubEnc);
+ }
const rsaPub = rsaPubDecode(rsaPubEnc);
const d = rsaFullDomainHash(hm, rsaPub);
const sig = loadBigInt(rsaSig);
@@ -563,7 +693,7 @@ export async function csBlind(
* Unblind operation to unblind the signature
* @param bseed seed to derive secrets
* @param rPub public R received from /csr
- * @param csPub denomination publick key
+ * @param csPub denomination public key
* @param b returned from exchange to select c
* @param csSig blinded signature
* @returns unblinded signature
@@ -591,7 +721,7 @@ export async function csUnblind(
* Verification algorithm for CS signatures
* @param hm message signed
* @param csSig unblinded signature
- * @param csPub denomination publick key
+ * @param csPub denomination public key
* @returns true if valid, false if invalid
*/
export async function csVerify(
@@ -629,14 +759,13 @@ export function createEddsaKeyPair(): EddsaKeyPair {
export function createEcdheKeyPair(): EcdheKeyPair {
const ecdhePriv = nacl.randomBytes(32);
- const ecdhePub = ecdheGetPublic(ecdhePriv);
+ const ecdhePub = ecdhGetPublic(ecdhePriv);
return { ecdhePriv, ecdhePub };
}
export function hash(d: Uint8Array): Uint8Array {
- if (useNative && "_hash" in globalThis) {
- // @ts-ignore
- return globalThis._hash(d);
+ if (tart) {
+ return tart.hash(d);
}
return nacl.hash(d);
}
@@ -664,7 +793,7 @@ const logger = new Logger("talerCrypto.ts");
export function hashCoinEvInner(
coinEv: CoinEnvelope,
- hashState: nacl.HashState,
+ hashState: TalerHashState,
): void {
const hashInputBuf = new ArrayBuffer(4);
const uint8ArrayBuf = new Uint8Array(hashInputBuf);
@@ -723,9 +852,8 @@ export function hashDenomPub(pub: DenominationPubKey): Uint8Array {
}
export function eddsaSign(msg: Uint8Array, eddsaPriv: Uint8Array): Uint8Array {
- if (useNative && "_eddsaSign" in globalThis) {
- // @ts-ignore
- return globalThis._eddsaSign(msg, eddsaPriv);
+ if (tart) {
+ return tart.eddsaSign(msg, eddsaPriv);
}
const pair = nacl.crypto_sign_keyPair_fromSeed(eddsaPriv);
return nacl.sign_detached(msg, pair.secretKey);
@@ -736,14 +864,26 @@ export function eddsaVerify(
sig: Uint8Array,
eddsaPub: Uint8Array,
): boolean {
- if (useNative && "_eddsaVerify" in globalThis) {
- // @ts-ignore
- return globalThis._eddsaVerify(msg, sig, eddsaPub);
+ if (tart) {
+ return tart.eddsaVerify(msg, sig, eddsaPub);
}
return nacl.sign_detached_verify(msg, sig, eddsaPub);
}
-export function createHashContext(): nacl.HashState {
+export interface TalerHashState {
+ update(data: Uint8Array): void;
+ finish(): Uint8Array;
+}
+
+export function createHashContext(): TalerHashState {
+ if (tart) {
+ const t = tart;
+ const st = tart.hashStateInit();
+ return {
+ finish: () => t.hashStateFinish(st),
+ update: (d) => t.hashStateUpdate(st, d),
+ };
+ }
return new nacl.HashState();
}
@@ -763,6 +903,21 @@ export function bufferForUint32(n: number): Uint8Array {
return buf;
}
+/**
+ * This makes the assumption that the uint64 fits a float,
+ * which should be true for all Taler protocol messages.
+ */
+export function bufferForUint64(n: number): Uint8Array {
+ const arrBuf = new ArrayBuffer(8);
+ const buf = new Uint8Array(arrBuf);
+ const dv = new DataView(arrBuf);
+ if (n < 0 || !Number.isInteger(n)) {
+ throw Error("non-negative integer expected");
+ }
+ dv.setBigUint64(0, BigInt(n));
+ return buf;
+}
+
export function bufferForUint8(n: number): Uint8Array {
const arrBuf = new ArrayBuffer(1);
const buf = new Uint8Array(arrBuf);
@@ -819,6 +974,7 @@ export function hashWire(paytoUri: string, salt: string): string {
export enum TalerSignaturePurpose {
MERCHANT_TRACK_TRANSACTION = 1103,
WALLET_RESERVE_WITHDRAW = 1200,
+ WALLET_RESERVE_HISTORY = 1208,
WALLET_COIN_DEPOSIT = 1201,
GLOBAL_FEES = 1022,
MASTER_DENOMINATION_KEY_VALIDITY = 1025,
@@ -828,6 +984,7 @@ export enum TalerSignaturePurpose {
TEST = 4242,
MERCHANT_PAYMENT_OK = 1104,
MERCHANT_CONTRACT = 1101,
+ MERCHANT_REFUND = 1102,
WALLET_COIN_RECOUP = 1203,
WALLET_COIN_LINK = 1204,
WALLET_COIN_RECOUP_REFRESH = 1206,
@@ -837,14 +994,19 @@ export enum TalerSignaturePurpose {
WALLET_PURSE_MERGE = 1213,
WALLET_ACCOUNT_MERGE = 1214,
WALLET_PURSE_ECONTRACT = 1216,
+ WALLET_PURSE_DELETE = 1220,
+ WALLET_COIN_HISTORY = 1209,
EXCHANGE_CONFIRM_RECOUP = 1039,
EXCHANGE_CONFIRM_RECOUP_REFRESH = 1041,
+ TALER_SIGNATURE_AML_DECISION = 1350,
+ TALER_SIGNATURE_AML_QUERY = 1351,
+ TALER_SIGNATURE_MASTER_AML_KEY = 1017,
ANASTASIS_POLICY_UPLOAD = 1400,
ANASTASIS_POLICY_DOWNLOAD = 1401,
SYNC_BACKUP_UPLOAD = 1450,
}
-export const enum WalletAccountMergeFlags {
+export enum WalletAccountMergeFlags {
/**
* Not a legal mode!
*/
@@ -1120,6 +1282,10 @@ export namespace AgeRestriction {
};
}
+ const PublishedAgeRestrictionBaseKey: Edx25519PublicKey = decodeCrock(
+ "CH0VKFDZ2GWRWHQBBGEK9MWV5YDQVJ0RXEE0KYT3NMB69F0R96TG",
+ );
+
export async function restrictionCommitSeeded(
ageMask: number,
age: number,
@@ -1132,19 +1298,32 @@ export namespace AgeRestriction {
const pubs: Edx25519PublicKey[] = [];
const privs: Edx25519PrivateKey[] = [];
- for (let i = 0; i < numPubs; i++) {
+ for (let i = 0; i < numPrivs; i++) {
const privSeed = await kdfKw({
outputLength: 32,
ikm: seed,
- info: stringToBytes("age-restriction-commit"),
+ info: stringToBytes("age-commitment"),
salt: bufferForUint32(i),
});
+
const priv = await Edx25519.keyCreateFromSeed(privSeed);
const pub = await Edx25519.getPublic(priv);
pubs.push(pub);
- if (i < numPrivs) {
- privs.push(priv);
- }
+ privs.push(priv);
+ }
+
+ for (let i = numPrivs; i < numPubs; i++) {
+ const deriveSeed = await kdfKw({
+ outputLength: 32,
+ ikm: seed,
+ info: stringToBytes("age-factor"),
+ salt: bufferForUint32(i),
+ });
+ const pub = await Edx25519.publicKeyDerive(
+ PublishedAgeRestrictionBaseKey,
+ deriveSeed,
+ );
+ pubs.push(pub);
}
return {
@@ -1257,7 +1436,7 @@ export namespace AgeRestriction {
}
// FIXME: make it a branded type!
-type EncryptionNonce = FlavorP<Uint8Array, "EncryptionNonce", 24>;
+export type EncryptionNonce = FlavorP<Uint8Array, "EncryptionNonce", 24>;
async function deriveKey(
keySeed: OpaqueData,
@@ -1272,7 +1451,7 @@ async function deriveKey(
});
}
-async function encryptWithDerivedKey(
+export async function encryptWithDerivedKey(
nonce: EncryptionNonce,
keySeed: OpaqueData,
plaintext: OpaqueData,
@@ -1285,7 +1464,7 @@ async function encryptWithDerivedKey(
const nonceSize = 24;
-async function decryptWithDerivedKey(
+export async function decryptWithDerivedKey(
ciphertext: OpaqueData,
keySeed: OpaqueData,
salt: string,
@@ -1343,6 +1522,7 @@ export function encryptContractForMerge(
contractPriv: ContractPrivateKey,
mergePriv: MergePrivateKey,
contractTerms: any,
+ nonce: EncryptionNonce,
): Promise<OpaqueData> {
const contractTermsCanon = canonicalJson(contractTerms) + "\0";
const contractTermsBytes = stringToBytes(contractTermsCanon);
@@ -1353,14 +1533,15 @@ export function encryptContractForMerge(
mergePriv,
contractTermsCompressed,
]);
- const key = keyExchangeEcdheEddsa(contractPriv, pursePub);
- return encryptWithDerivedKey(getRandomBytesF(24), key, data, mergeSalt);
+ const key = keyExchangeEcdhEddsa(contractPriv, pursePub);
+ return encryptWithDerivedKey(nonce, key, data, mergeSalt);
}
export function encryptContractForDeposit(
pursePub: PursePublicKey,
contractPriv: ContractPrivateKey,
contractTerms: any,
+ nonce: EncryptionNonce,
): Promise<OpaqueData> {
const contractTermsCanon = canonicalJson(contractTerms) + "\0";
const contractTermsBytes = stringToBytes(contractTermsCanon);
@@ -1370,8 +1551,8 @@ export function encryptContractForDeposit(
bufferForUint32(contractTermsBytes.length),
contractTermsCompressed,
]);
- const key = keyExchangeEcdheEddsa(contractPriv, pursePub);
- return encryptWithDerivedKey(getRandomBytesF(24), key, data, depositSalt);
+ const key = keyExchangeEcdhEddsa(contractPriv, pursePub);
+ return encryptWithDerivedKey(nonce, key, data, depositSalt);
}
export interface DecryptForMergeResult {
@@ -1388,7 +1569,7 @@ export async function decryptContractForMerge(
pursePub: PursePublicKey,
contractPriv: ContractPrivateKey,
): Promise<DecryptForMergeResult> {
- const key = keyExchangeEcdheEddsa(contractPriv, pursePub);
+ const key = keyExchangeEcdhEddsa(contractPriv, pursePub);
const dec = await decryptWithDerivedKey(enc, key, mergeSalt);
const mergePriv = dec.slice(8, 8 + 32);
const contractTermsCompressed = dec.slice(8 + 32);
@@ -1408,7 +1589,7 @@ export async function decryptContractForDeposit(
pursePub: PursePublicKey,
contractPriv: ContractPrivateKey,
): Promise<DecryptForDepositResult> {
- const key = keyExchangeEcdheEddsa(contractPriv, pursePub);
+ const key = keyExchangeEcdhEddsa(contractPriv, pursePub);
const dec = await decryptWithDerivedKey(enc, key, depositSalt);
const contractTermsCompressed = dec.slice(8);
const contractTermsBuf = fflate.unzlibSync(contractTermsCompressed);
@@ -1420,3 +1601,63 @@ export async function decryptContractForDeposit(
contractTerms: JSON.parse(contractTermsString),
};
}
+
+export function amountToBuffer(amount: AmountLike): Uint8Array {
+ const amountJ = Amounts.jsonifyAmount(amount);
+ const buffer = new ArrayBuffer(8 + 4 + 12);
+ const dvbuf = new DataView(buffer);
+ const u8buf = new Uint8Array(buffer);
+ const curr = stringToBytes(amountJ.currency);
+ if (typeof dvbuf.setBigUint64 !== "undefined") {
+ dvbuf.setBigUint64(0, BigInt(amountJ.value));
+ } else {
+ const arr = bigint(amountJ.value).toArray(2 ** 8).value;
+ let offset = 8 - arr.length;
+ for (let i = 0; i < arr.length; i++) {
+ dvbuf.setUint8(offset++, arr[i]);
+ }
+ }
+ dvbuf.setUint32(8, amountJ.fraction);
+ u8buf.set(curr, 8 + 4);
+
+ return u8buf;
+}
+
+export function timestampRoundedToBuffer(
+ ts: TalerProtocolTimestamp,
+): Uint8Array {
+ const b = new ArrayBuffer(8);
+ const v = new DataView(b);
+ // The buffer we sign over represents the timestamp in microseconds.
+ if (typeof v.setBigUint64 !== "undefined") {
+ const s = BigInt(ts.t_s) * BigInt(1000 * 1000);
+ v.setBigUint64(0, s);
+ } else {
+ const s =
+ ts.t_s === "never" ? bigint.zero : bigint(ts.t_s).multiply(1000 * 1000);
+ const arr = s.toArray(2 ** 8).value;
+ let offset = 8 - arr.length;
+ for (let i = 0; i < arr.length; i++) {
+ v.setUint8(offset++, arr[i]);
+ }
+ }
+ return new Uint8Array(b);
+}
+
+export function durationRoundedToBuffer(ts: TalerProtocolDuration): Uint8Array {
+ const b = new ArrayBuffer(8);
+ const v = new DataView(b);
+ // The buffer we sign over represents the timestamp in microseconds.
+ if (typeof v.setBigUint64 !== "undefined") {
+ const s = BigInt(ts.d_us);
+ v.setBigUint64(0, s);
+ } else {
+ const s = ts.d_us === "forever" ? bigint.zero : bigint(ts.d_us);
+ const arr = s.toArray(2 ** 8).value;
+ let offset = 8 - arr.length;
+ for (let i = 0; i < arr.length; i++) {
+ v.setUint8(offset++, arr[i]);
+ }
+ }
+ return new Uint8Array(b);
+}
diff --git a/packages/taler-util/src/taler-error-codes.ts b/packages/taler-util/src/taler-error-codes.ts
index ef629954a..9985e74b3 100644
--- a/packages/taler-util/src/taler-error-codes.ts
+++ b/packages/taler-util/src/taler-error-codes.ts
@@ -22,6 +22,8 @@
*/
export enum TalerErrorCode {
+
+
/**
* Special code to indicate success (no error).
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -29,251 +31,343 @@ export enum TalerErrorCode {
*/
NONE = 0,
+
/**
- * A non-integer error code was returned in the JSON response.
+ * An error response did not include an error code in the format expected by the client. Most likely, the server does not speak the GNU Taler protocol. Check the URL and/or the network connection to the server.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
* (A value of 0 indicates that the error is generated client-side).
*/
INVALID = 1,
+
/**
- * An internal failure happened on the client side.
+ * An internal failure happened on the client side. Details should be in the local logs. Check if you are using the latest available version or file a report with the developers.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_CLIENT_INTERNAL_ERROR = 2,
+
/**
- * The response we got from the server was not even in JSON format.
+ * The response we got from the server was not in the expected format. Most likely, the server does not speak the GNU Taler protocol. Check the URL and/or the network connection to the server.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_INVALID_RESPONSE = 10,
+
/**
- * An operation timed out.
+ * The operation timed out. Trying again might help. Check the network connection.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_TIMEOUT = 11,
+
/**
- * The version string given does not follow the expected CURRENT:REVISION:AGE Format.
+ * The protocol version given by the server does not follow the required format. Most likely, the server does not speak the GNU Taler protocol. Check the URL and/or the network connection to the server.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_VERSION_MALFORMED = 12,
+
/**
- * The service responded with a reply that was in JSON but did not satsify the protocol. Note that invalid cryptographic signatures should have signature-specific error codes.
+ * The service responded with a reply that was in the right data format, but the content did not satisfy the protocol. Please file a bug report.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_REPLY_MALFORMED = 13,
+
/**
- * There is an error in the client-side configuration, for example the base URL specified is malformed.
+ * There is an error in the client-side configuration, for example an option is set to an invalid value. Check the logs and fix the local configuration.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_CONFIGURATION_INVALID = 14,
+
/**
- * The client made a request to a service, but received an error response it does not know how to handle.
+ * The client made a request to a service, but received an error response it does not know how to handle. Please file a bug report.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_UNEXPECTED_REQUEST_ERROR = 15,
+
+ /**
+ * The token used by the client to authorize the request does not grant the required permissions for the request. Check the requirements and obtain a suitable authorization token to proceed.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GENERIC_TOKEN_PERMISSION_INSUFFICIENT = 16,
+
+
/**
- * The HTTP method used is invalid for this endpoint.
+ * The HTTP method used is invalid for this endpoint. This is likely a bug in the client implementation. Check if you are using the latest available version and/or file a report with the developers.
* Returned with an HTTP status code of #MHD_HTTP_METHOD_NOT_ALLOWED (405).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_METHOD_INVALID = 20,
+
/**
- * There is no endpoint defined for the URL provided by the client.
+ * There is no endpoint defined for the URL provided by the client. Check if you used the correct URL and/or file a report with the developers of the client software.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_ENDPOINT_UNKNOWN = 21,
+
/**
- * The JSON in the client's request was malformed (generic parse error).
+ * The JSON in the client's request was malformed. This is likely a bug in the client implementation. Check if you are using the latest available version and/or file a report with the developers.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_JSON_INVALID = 22,
+
/**
- * Some of the HTTP headers provided by the client caused the server to not be able to handle the request.
+ * Some of the HTTP headers provided by the client were malformed and caused the server to not be able to handle the request. This is likely a bug in the client implementation. Check if you are using the latest available version and/or file a report with the developers.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_HTTP_HEADERS_MALFORMED = 23,
+
/**
- * The payto:// URI provided by the client is malformed.
+ * The payto:// URI provided by the client is malformed. Check that you are using the correct syntax as of RFC 8905 and/or that you entered the bank account number correctly.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_PAYTO_URI_MALFORMED = 24,
+
/**
- * A required parameter in the request was missing.
+ * A required parameter in the request was missing. This is likely a bug in the client implementation. Check if you are using the latest available version and/or file a report with the developers.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_PARAMETER_MISSING = 25,
+
/**
- * A parameter in the request was malformed.
+ * A parameter in the request was malformed. This is likely a bug in the client implementation. Check if you are using the latest available version and/or file a report with the developers.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_PARAMETER_MALFORMED = 26,
+
/**
- * The reserve public key given as part of a /reserves/ endpoint was malformed.
+ * The reserve public key was malformed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_RESERVE_PUB_MALFORMED = 27,
+
+ /**
+ * The body in the request could not be decompressed by the server. This is likely a bug in the client implementation. Check if you are using the latest available version and/or file a report with the developers.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GENERIC_COMPRESSION_INVALID = 28,
+
+
/**
- * The currencies involved in the operation do not match.
+ * The currency involved in the operation is not acceptable for this server. Check your configuration and make sure the currency specified for a given service provider is one of the currencies supported by that provider.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_CURRENCY_MISMATCH = 30,
+
/**
- * The URI is longer than the longest URI the HTTP server is willing to parse.
+ * The URI is longer than the longest URI the HTTP server is willing to parse. If you believe this was a legitimate request, contact the server administrators and/or the software developers to increase the limit.
* Returned with an HTTP status code of #MHD_HTTP_URI_TOO_LONG (414).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_URI_TOO_LONG = 31,
+
/**
- * The body is too large to be permissible for the endpoint.
- * Returned with an HTTP status code of #MHD_HTTP_PAYLOAD_TOO_LARGE (413).
+ * The body is too large to be permissible for the endpoint. If you believe this was a legitimate request, contact the server administrators and/or the software developers to increase the limit.
+ * Returned with an HTTP status code of #MHD_HTTP_CONTENT_TOO_LARGE (413).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_UPLOAD_EXCEEDS_LIMIT = 32,
+
+ /**
+ * The service refused the request due to lack of proper authorization.
+ * Returned with an HTTP status code of #MHD_HTTP_UNAUTHORIZED (401).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GENERIC_UNAUTHORIZED = 40,
+
+
/**
- * The service failed initialize its connection to the database.
+ * The service refused the request as the given authorization token is unknown.
+ * Returned with an HTTP status code of #MHD_HTTP_UNAUTHORIZED (401).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GENERIC_TOKEN_UNKNOWN = 41,
+
+
+ /**
+ * The service refused the request as the given authorization token expired.
+ * Returned with an HTTP status code of #MHD_HTTP_UNAUTHORIZED (401).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GENERIC_TOKEN_EXPIRED = 42,
+
+
+ /**
+ * The service refused the request as the given authorization token is malformed.
+ * Returned with an HTTP status code of #MHD_HTTP_UNAUTHORIZED (401).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GENERIC_TOKEN_MALFORMED = 43,
+
+
+ /**
+ * The service refused the request due to lack of proper rights on the resource.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GENERIC_FORBIDDEN = 44,
+
+
+ /**
+ * The service failed initialize its connection to the database. The system administrator should check that the service has permissions to access the database and that the database is running.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_DB_SETUP_FAILED = 50,
+
/**
- * The service encountered an error event to just start the database transaction.
+ * The service encountered an error event to just start the database transaction. The system administrator should check that the database is running.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_DB_START_FAILED = 51,
+
/**
- * The service failed to store information in its database.
+ * The service failed to store information in its database. The system administrator should check that the database is running and review the service logs.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_DB_STORE_FAILED = 52,
+
/**
- * The service failed to fetch information from its database.
+ * The service failed to fetch information from its database. The system administrator should check that the database is running and review the service logs.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_DB_FETCH_FAILED = 53,
+
/**
- * The service encountered an error event to commit the database transaction (hard, unrecoverable error).
+ * The service encountered an unrecoverable error trying to commit a transaction to the database. The system administrator should check that the database is running and review the service logs.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_DB_COMMIT_FAILED = 54,
+
/**
- * The service encountered an error event to commit the database transaction, even after repeatedly retrying it there was always a conflicting transaction. (This indicates a repeated serialization error; should only happen if some client maliciously tries to create conflicting concurrent transactions.)
+ * The service encountered an error event to commit the database transaction, even after repeatedly retrying it there was always a conflicting transaction. This indicates a repeated serialization error; it should only happen if some client maliciously tries to create conflicting concurrent transactions. It could also be a sign of a missing index. Check if you are using the latest available version and/or file a report with the developers.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_DB_SOFT_FAILURE = 55,
+
/**
- * The service's database is inconsistent and violates service-internal invariants.
+ * The service's database is inconsistent and violates service-internal invariants. Check if you are using the latest available version and/or file a report with the developers.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_DB_INVARIANT_FAILURE = 56,
+
/**
- * The HTTP server experienced an internal invariant failure (bug).
+ * The HTTP server experienced an internal invariant failure (bug). Check if you are using the latest available version and/or file a report with the developers.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_INTERNAL_INVARIANT_FAILURE = 60,
+
/**
- * The service could not compute a cryptographic hash over some JSON value.
+ * The service could not compute a cryptographic hash over some JSON value. Check if you are using the latest available version and/or file a report with the developers.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_FAILED_COMPUTE_JSON_HASH = 61,
+
/**
- * The service could not compute an amount.
+ * The service could not compute an amount. Check if you are using the latest available version and/or file a report with the developers.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_FAILED_COMPUTE_AMOUNT = 62,
+
/**
- * The HTTP server had insufficient memory to parse the request.
+ * The HTTP server had insufficient memory to parse the request. Restarting services periodically can help, especially if Postgres is using excessive amounts of memory. Check with the system administrator to investigate.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_PARSER_OUT_OF_MEMORY = 70,
+
/**
- * The HTTP server failed to allocate memory.
+ * The HTTP server failed to allocate memory. Restarting services periodically can help, especially if Postgres is using excessive amounts of memory. Check with the system administrator to investigate.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_ALLOCATION_FAILURE = 71,
+
/**
- * The HTTP server failed to allocate memory for building JSON reply.
+ * The HTTP server failed to allocate memory for building JSON reply. Restarting services periodically can help, especially if Postgres is using excessive amounts of memory. Check with the system administrator to investigate.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_JSON_ALLOCATION_FAILURE = 72,
+
/**
- * The HTTP server failed to allocate memory for making a CURL request.
+ * The HTTP server failed to allocate memory for making a CURL request. Restarting services periodically can help, especially if Postgres is using excessive amounts of memory. Check with the system administrator to investigate.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_CURL_ALLOCATION_FAILURE = 73,
+
/**
- * The backend could not locate a required template to generate an HTML reply.
- * Returned with an HTTP status code of #MHD_HTTP_NOT_ACCEPTABLE (406).
+ * The backend could not locate a required template to generate an HTML reply. The system administrator should check if the resource files are installed in the correct location and are readable to the service.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_FAILED_TO_LOAD_TEMPLATE = 74,
+
/**
- * The backend could not expand the template to generate an HTML reply.
+ * The backend could not expand the template to generate an HTML reply. The system administrator should investigate the logs and check if the templates are well-formed.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
* (A value of 0 indicates that the error is generated client-side).
*/
GENERIC_FAILED_TO_EXPAND_TEMPLATE = 75,
+
/**
* Exchange is badly configured and thus cannot operate.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -281,6 +375,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_BAD_CONFIGURATION = 1000,
+
/**
* Operation specified unknown for this endpoint.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -288,6 +383,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_OPERATION_UNKNOWN = 1001,
+
/**
* The number of segments included in the URI does not match the number of segments expected by the endpoint.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -295,6 +391,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_WRONG_NUMBER_OF_SEGMENTS = 1002,
+
/**
* The same coin was already used with a different denomination previously.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -302,6 +399,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY = 1003,
+
/**
* The public key of given to a "/coins/" endpoint of the exchange was malformed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -309,6 +407,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_COINS_INVALID_COIN_PUB = 1004,
+
/**
* The exchange is not aware of the denomination key the wallet requested for the operation.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -316,6 +415,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_DENOMINATION_KEY_UNKNOWN = 1005,
+
/**
* The signature of the denomination key over the coin is not valid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -323,6 +423,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_DENOMINATION_SIGNATURE_INVALID = 1006,
+
/**
* The exchange failed to perform the operation as it could not find the private keys. This is a problem with the exchange setup, not with the client's request.
* Returned with an HTTP status code of #MHD_HTTP_SERVICE_UNAVAILABLE (503).
@@ -330,6 +431,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_KEYS_MISSING = 1007,
+
/**
* Validity period of the denomination lies in the future.
* Returned with an HTTP status code of #MHD_HTTP_PRECONDITION_FAILED (412).
@@ -337,6 +439,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_DENOMINATION_VALIDITY_IN_FUTURE = 1008,
+
/**
* Denomination key of the coin is past its expiration time for the requested operation.
* Returned with an HTTP status code of #MHD_HTTP_GONE (410).
@@ -344,6 +447,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_DENOMINATION_EXPIRED = 1009,
+
/**
* Denomination key of the coin has been revoked.
* Returned with an HTTP status code of #MHD_HTTP_GONE (410).
@@ -351,6 +455,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_DENOMINATION_REVOKED = 1010,
+
/**
* An operation where the exchange interacted with a security module timed out.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -358,6 +463,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_SECMOD_TIMEOUT = 1011,
+
/**
* The respective coin did not have sufficient residual value for the operation. The "history" in this response provides the "residual_value" of the coin, which may be less than its "original_value".
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -365,6 +471,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_INSUFFICIENT_FUNDS = 1012,
+
/**
* The exchange had an internal error reconstructing the transaction history of the coin that was being processed.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -372,6 +479,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_COIN_HISTORY_COMPUTATION_FAILED = 1013,
+
/**
* The exchange failed to obtain the transaction history of the given coin from the database while generating an insufficient funds errors.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -379,6 +487,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_HISTORY_DB_ERROR_INSUFFICIENT_FUNDS = 1014,
+
/**
* The same coin was already used with a different age hash previously.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -386,6 +495,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_COIN_CONFLICTING_AGE_HASH = 1015,
+
/**
* The requested operation is not valid for the cipher used by the selected denomination.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -393,6 +503,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_INVALID_DENOMINATION_CIPHER_FOR_OPERATION = 1016,
+
/**
* The provided arguments for the operation use inconsistent ciphers.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -400,6 +511,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_CIPHER_MISMATCH = 1017,
+
/**
* The number of denominations specified in the request exceeds the limit of the exchange.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -407,6 +519,15 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_NEW_DENOMS_ARRAY_SIZE_EXCESSIVE = 1018,
+
+ /**
+ * The coin is not known to the exchange (yet).
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_GENERIC_COIN_UNKNOWN = 1019,
+
+
/**
* The time at the server is too far off from the time specified in the request. Most likely the client system time is wrong.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -414,6 +535,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_CLOCK_SKEW = 1020,
+
/**
* The specified amount for the coin is higher than the value of the denomination of the coin.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -421,6 +543,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_AMOUNT_EXCEEDS_DENOMINATION_VALUE = 1021,
+
/**
* The exchange was not properly configured with global fees.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -428,6 +551,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_GLOBAL_FEES_MISSING = 1022,
+
/**
* The exchange was not properly configured with wire fees.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -435,6 +559,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_WIRE_FEES_MISSING = 1023,
+
/**
* The purse public key was malformed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -442,6 +567,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_PURSE_PUB_MALFORMED = 1024,
+
/**
* The purse is unknown.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -449,6 +575,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_PURSE_UNKNOWN = 1025,
+
/**
* The purse has expired.
* Returned with an HTTP status code of #MHD_HTTP_GONE (410).
@@ -456,6 +583,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_PURSE_EXPIRED = 1026,
+
/**
* The exchange has no information about the "reserve_pub" that was given.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -463,6 +591,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_RESERVE_UNKNOWN = 1027,
+
/**
* The exchange is not allowed to proceed with the operation until the client has satisfied a KYC check.
* Returned with an HTTP status code of #MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS (451).
@@ -470,6 +599,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_KYC_REQUIRED = 1028,
+
/**
* Inconsistency between provided age commitment and attest: either none or both must be provided
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -477,6 +607,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSE_DEPOSIT_COIN_CONFLICTING_ATTEST_VS_AGE_COMMITMENT = 1029,
+
/**
* The provided attestation for the minimum age couldn't be verified by the exchange.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -484,6 +615,63 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSE_DEPOSIT_COIN_AGE_ATTESTATION_FAILURE = 1030,
+
+ /**
+ * The purse was deleted.
+ * Returned with an HTTP status code of #MHD_HTTP_GONE (410).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_GENERIC_PURSE_DELETED = 1031,
+
+
+ /**
+ * The public key of the AML officer in the URL was malformed.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_GENERIC_AML_OFFICER_PUB_MALFORMED = 1032,
+
+
+ /**
+ * The signature affirming the GET request of the AML officer is invalid.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_GENERIC_AML_OFFICER_GET_SIGNATURE_INVALID = 1033,
+
+
+ /**
+ * The specified AML officer does not have access at this time.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_GENERIC_AML_OFFICER_ACCESS_DENIED = 1034,
+
+
+ /**
+ * The requested operation is denied pending the resolution of an anti-money laundering investigation by the exchange operator. This is a manual process, please wait and retry later.
+ * Returned with an HTTP status code of #MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS (451).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_GENERIC_AML_PENDING = 1035,
+
+
+ /**
+ * The requested operation is denied as the account was frozen on suspicion of money laundering. Please contact the exchange operator.
+ * Returned with an HTTP status code of #MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS (451).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_GENERIC_AML_FROZEN = 1036,
+
+
+ /**
+ * The exchange failed to start a KYC attribute conversion helper process. It is likely configured incorrectly.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_GENERIC_KYC_CONVERTER_FAILED = 1037,
+
+
/**
* The exchange did not find information about the specified transaction in the database.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -491,6 +679,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_DEPOSITS_GET_NOT_FOUND = 1100,
+
/**
* The wire hash of given to a "/deposits/" handler was malformed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -498,6 +687,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_DEPOSITS_GET_INVALID_H_WIRE = 1101,
+
/**
* The merchant key of given to a "/deposits/" handler was malformed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -505,6 +695,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_DEPOSITS_GET_INVALID_MERCHANT_PUB = 1102,
+
/**
* The hash of the contract terms given to a "/deposits/" handler was malformed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -512,6 +703,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_DEPOSITS_GET_INVALID_H_CONTRACT_TERMS = 1103,
+
/**
* The coin public key of given to a "/deposits/" handler was malformed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -519,6 +711,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_DEPOSITS_GET_INVALID_COIN_PUB = 1104,
+
/**
* The signature returned by the exchange in a /deposits/ request was malformed.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -526,6 +719,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_DEPOSITS_GET_INVALID_SIGNATURE_BY_EXCHANGE = 1105,
+
/**
* The signature of the merchant is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -533,6 +727,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_DEPOSITS_GET_MERCHANT_SIGNATURE_INVALID = 1106,
+
/**
* The provided policy data was not accepted
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -540,6 +735,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_DEPOSITS_POLICY_NOT_ACCEPTED = 1107,
+
/**
* The given reserve does not have sufficient funds to admit the requested withdraw operation at this time. The response includes the current "balance" of the reserve as well as the transaction "history" that lead to this balance.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -547,6 +743,15 @@ export enum TalerErrorCode {
*/
EXCHANGE_WITHDRAW_INSUFFICIENT_FUNDS = 1150,
+
+ /**
+ * The given reserve does not have sufficient funds to admit the requested age-withdraw operation at this time. The response includes the current "balance" of the reserve as well as the transaction "history" that lead to this balance.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_AGE_WITHDRAW_INSUFFICIENT_FUNDS = 1151,
+
+
/**
* The amount to withdraw together with the fee exceeds the numeric range for Taler amounts. This is not a client failure, as the coin value and fees come from the exchange's configuration.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -554,6 +759,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_WITHDRAW_AMOUNT_FEE_OVERFLOW = 1152,
+
/**
* The exchange failed to create the signature using the denomination key.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -561,6 +767,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_WITHDRAW_SIGNATURE_FAILED = 1153,
+
/**
* The signature of the reserve is not valid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -568,12 +775,22 @@ export enum TalerErrorCode {
*/
EXCHANGE_WITHDRAW_RESERVE_SIGNATURE_INVALID = 1154,
+
/**
* When computing the reserve history, we ended up with a negative overall balance, which should be impossible.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
* (A value of 0 indicates that the error is generated client-side).
*/
- EXCHANGE_WITHDRAW_HISTORY_ERROR_INSUFFICIENT_FUNDS = 1155,
+ EXCHANGE_RESERVE_HISTORY_ERROR_INSUFFICIENT_FUNDS = 1155,
+
+
+ /**
+ * The reserve did not have sufficient funds in it to pay for a full reserve history statement.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_GET_RESERVE_HISTORY_ERROR_INSUFFICIENT_BALANCE = 1156,
+
/**
* Withdraw period of the coin to be withdrawn is in the past.
@@ -582,6 +799,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_WITHDRAW_DENOMINATION_KEY_LOST = 1158,
+
/**
* The client failed to unblind the blind signature.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -589,6 +807,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_WITHDRAW_UNBLIND_FAILURE = 1159,
+
/**
* The client re-used a withdraw nonce, which is not allowed.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -596,6 +815,47 @@ export enum TalerErrorCode {
*/
EXCHANGE_WITHDRAW_NONCE_REUSE = 1160,
+
+ /**
+ * The client provided an unknown commitment for an age-withdraw request.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_AGE_WITHDRAW_COMMITMENT_UNKNOWN = 1161,
+
+
+ /**
+ * The total sum of amounts from the denominations did overflow.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_AGE_WITHDRAW_AMOUNT_OVERFLOW = 1162,
+
+
+ /**
+ * The total sum of value and fees from the denominations differs from the committed amount with fees.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_AGE_WITHDRAW_AMOUNT_INCORRECT = 1163,
+
+
+ /**
+ * The original commitment differs from the calculated hash
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_AGE_WITHDRAW_REVEAL_INVALID_HASH = 1164,
+
+
+ /**
+ * The maximum age in the commitment is too large for the reserve
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_AGE_WITHDRAW_MAXIMUM_AGE_TOO_LARGE = 1165,
+
+
/**
* The batch withdraw included a planchet that was already withdrawn. This is not allowed.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -603,6 +863,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_WITHDRAW_BATCH_IDEMPOTENT_PLANCHET = 1175,
+
/**
* The signature made by the coin over the deposit permission is not valid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -610,6 +871,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_DEPOSIT_COIN_SIGNATURE_INVALID = 1205,
+
/**
* The same coin was already deposited for the same merchant and contract with other details.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -617,6 +879,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_DEPOSIT_CONFLICTING_CONTRACT = 1206,
+
/**
* The stated value of the coin after the deposit fee is subtracted would be negative.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -624,6 +887,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_DEPOSIT_NEGATIVE_VALUE_AFTER_FEE = 1207,
+
/**
* The stated refund deadline is after the wire deadline.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -631,6 +895,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_DEPOSIT_REFUND_DEADLINE_AFTER_WIRE_DEADLINE = 1208,
+
/**
* The stated wire deadline is "never", which makes no sense.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -638,6 +903,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_DEPOSIT_WIRE_DEADLINE_IS_NEVER = 1209,
+
/**
* The exchange failed to canonicalize and hash the given wire format. For example, the merchant failed to provide the "salt" or a valid payto:// URI in the wire details. Note that while the exchange will do some basic sanity checking on the wire details, it cannot warrant that the banking system will ultimately be able to route to the specified address, even if this check passed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -645,6 +911,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_DEPOSIT_INVALID_WIRE_FORMAT_JSON = 1210,
+
/**
* The hash of the given wire address does not match the wire hash specified in the proposal data.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -652,6 +919,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_DEPOSIT_INVALID_WIRE_FORMAT_CONTRACT_HASH_CONFLICT = 1211,
+
/**
* The signature provided by the exchange is not valid.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -659,6 +927,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_DEPOSIT_INVALID_SIGNATURE_BY_EXCHANGE = 1221,
+
/**
* The deposited amount is smaller than the deposit fee, which would result in a negative contribution.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -666,6 +935,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_DEPOSIT_FEE_ABOVE_AMOUNT = 1222,
+
/**
* The proof of policy fulfillment was invalid.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -673,26 +943,22 @@ export enum TalerErrorCode {
*/
EXCHANGE_EXTENSIONS_INVALID_FULFILLMENT = 1240,
- /**
- * The reserve balance, status or history was requested for a reserve which is not known to the exchange.
- * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
- * (A value of 0 indicates that the error is generated client-side).
- */
- EXCHANGE_RESERVES_STATUS_UNKNOWN = 1250,
/**
- * The reserve status was requested with a bad signature.
+ * The coin history was requested with a bad signature.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
* (A value of 0 indicates that the error is generated client-side).
*/
- EXCHANGE_RESERVES_STATUS_BAD_SIGNATURE = 1251,
+ EXCHANGE_COIN_HISTORY_BAD_SIGNATURE = 1251,
+
/**
* The reserve history was requested with a bad signature.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
* (A value of 0 indicates that the error is generated client-side).
*/
- EXCHANGE_RESERVES_HISTORY_BAD_SIGNATURE = 1252,
+ EXCHANGE_RESERVE_HISTORY_BAD_SIGNATURE = 1252,
+
/**
* The exchange encountered melt fees exceeding the melted coin's contribution.
@@ -701,6 +967,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MELT_FEES_EXCEED_CONTRIBUTION = 1302,
+
/**
* The signature made with the coin to be melted is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -708,6 +975,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MELT_COIN_SIGNATURE_INVALID = 1303,
+
/**
* The denomination of the given coin has past its expiration date and it is also not a valid zombie (that is, was not refreshed with the fresh coin being subjected to recoup).
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -715,6 +983,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MELT_COIN_EXPIRED_NO_ZOMBIE = 1305,
+
/**
* The signature returned by the exchange in a melt request was malformed.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -722,6 +991,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MELT_INVALID_SIGNATURE_BY_EXCHANGE = 1306,
+
/**
* The provided transfer keys do not match up with the original commitment. Information about the original commitment is included in the response.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -729,6 +999,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFRESHES_REVEAL_COMMITMENT_VIOLATION = 1353,
+
/**
* Failed to produce the blinded signatures over the coins to be returned.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -736,6 +1007,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFRESHES_REVEAL_SIGNING_ERROR = 1354,
+
/**
* The exchange is unaware of the refresh session specified in the request.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -743,6 +1015,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFRESHES_REVEAL_SESSION_UNKNOWN = 1355,
+
/**
* The size of the cut-and-choose dimension of the private transfer keys request does not match #TALER_CNC_KAPPA - 1.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -750,6 +1023,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFRESHES_REVEAL_CNC_TRANSFER_ARRAY_SIZE_INVALID = 1356,
+
/**
* The number of envelopes given does not match the number of denomination keys given.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -757,6 +1031,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFRESHES_REVEAL_NEW_DENOMS_ARRAY_SIZE_MISMATCH = 1358,
+
/**
* The exchange encountered a numeric overflow totaling up the cost for the refresh operation.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -764,6 +1039,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFRESHES_REVEAL_COST_CALCULATION_OVERFLOW = 1359,
+
/**
* The exchange's cost calculation shows that the melt amount is below the costs of the transaction.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -771,6 +1047,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFRESHES_REVEAL_AMOUNT_INSUFFICIENT = 1360,
+
/**
* The signature made with the coin over the link data is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -778,6 +1055,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFRESHES_REVEAL_LINK_SIGNATURE_INVALID = 1361,
+
/**
* The refresh session hash given to a /refreshes/ handler was malformed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -785,6 +1063,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFRESHES_REVEAL_INVALID_RCH = 1362,
+
/**
* Operation specified invalid for this endpoint.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -792,6 +1071,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFRESHES_REVEAL_OPERATION_INVALID = 1363,
+
/**
* The client provided age commitment data, but age restriction is not supported on this server.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -799,6 +1079,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFRESHES_REVEAL_AGE_RESTRICTION_NOT_SUPPORTED = 1364,
+
/**
* The client provided invalid age commitment data: missing, not an array, or array of invalid size.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -806,6 +1087,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFRESHES_REVEAL_AGE_RESTRICTION_COMMITMENT_INVALID = 1365,
+
/**
* The coin specified in the link request is unknown to the exchange.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -813,6 +1095,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_LINK_COIN_UNKNOWN = 1400,
+
/**
* The public key of given to a /transfers/ handler was malformed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -820,6 +1103,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_TRANSFERS_GET_WTID_MALFORMED = 1450,
+
/**
* The exchange did not find information about the specified wire transfer identifier in the database.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -827,6 +1111,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_TRANSFERS_GET_WTID_NOT_FOUND = 1451,
+
/**
* The exchange did not find information about the wire transfer fees it charged.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -834,6 +1119,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_TRANSFERS_GET_WIRE_FEE_NOT_FOUND = 1452,
+
/**
* The exchange found a wire fee that was above the total transfer value (and thus could not have been charged).
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -841,6 +1127,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_TRANSFERS_GET_WIRE_FEE_INCONSISTENT = 1453,
+
/**
* The wait target of the URL was not in the set of expected values.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -848,6 +1135,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSES_INVALID_WAIT_TARGET = 1475,
+
/**
* The signature on the purse status returned by the exchange was invalid.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -855,6 +1143,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSES_GET_INVALID_SIGNATURE_BY_EXCHANGE = 1476,
+
/**
* The exchange knows literally nothing about the coin we were asked to refund. But without a transaction history, we cannot issue a refund. This is kind-of OK, the owner should just refresh it directly without executing the refund.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -862,6 +1151,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFUND_COIN_NOT_FOUND = 1500,
+
/**
* We could not process the refund request as the coin's transaction history does not permit the requested refund because then refunds would exceed the deposit amount. The "history" in the response proves this.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -869,6 +1159,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFUND_CONFLICT_DEPOSIT_INSUFFICIENT = 1501,
+
/**
* The exchange knows about the coin we were asked to refund, but not about the specific /deposit operation. Hence, we cannot issue a refund (as we do not know if this merchant public key is authorized to do a refund).
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -876,6 +1167,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFUND_DEPOSIT_NOT_FOUND = 1502,
+
/**
* The exchange can no longer refund the customer/coin as the money was already transferred (paid out) to the merchant. (It should be past the refund deadline.)
* Returned with an HTTP status code of #MHD_HTTP_GONE (410).
@@ -883,6 +1175,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFUND_MERCHANT_ALREADY_PAID = 1503,
+
/**
* The refund fee specified for the request is lower than the refund fee charged by the exchange for the given denomination key of the refunded coin.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -890,6 +1183,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFUND_FEE_TOO_LOW = 1504,
+
/**
* The refunded amount is smaller than the refund fee, which would result in a negative refund.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -897,6 +1191,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFUND_FEE_ABOVE_AMOUNT = 1505,
+
/**
* The signature of the merchant is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -904,6 +1199,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFUND_MERCHANT_SIGNATURE_INVALID = 1506,
+
/**
* Merchant backend failed to create the refund confirmation signature.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -911,6 +1207,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFUND_MERCHANT_SIGNING_FAILED = 1507,
+
/**
* The signature returned by the exchange in a refund request was malformed.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -918,6 +1215,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFUND_INVALID_SIGNATURE_BY_EXCHANGE = 1508,
+
/**
* The failure proof returned by the exchange is incorrect.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -925,6 +1223,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFUND_INVALID_FAILURE_PROOF_BY_EXCHANGE = 1509,
+
/**
* Conflicting refund granted before with different amount but same refund transaction ID.
* Returned with an HTTP status code of #MHD_HTTP_FAILED_DEPENDENCY (424).
@@ -932,6 +1231,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_REFUND_INCONSISTENT_AMOUNT = 1510,
+
/**
* The given coin signature is invalid for the request.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -939,6 +1239,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RECOUP_SIGNATURE_INVALID = 1550,
+
/**
* The exchange could not find the corresponding withdraw operation. The request is denied.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -946,6 +1247,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RECOUP_WITHDRAW_NOT_FOUND = 1551,
+
/**
* The coin's remaining balance is zero. The request is denied.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -953,6 +1255,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RECOUP_COIN_BALANCE_ZERO = 1552,
+
/**
* The exchange failed to reproduce the coin's blinding.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -960,6 +1263,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RECOUP_BLINDING_FAILED = 1553,
+
/**
* The coin's remaining balance is zero. The request is denied.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -967,6 +1271,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RECOUP_COIN_BALANCE_NEGATIVE = 1554,
+
/**
* The coin's denomination has not been revoked yet.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -974,6 +1279,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RECOUP_NOT_ELIGIBLE = 1555,
+
/**
* The given coin signature is invalid for the request.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -981,6 +1287,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RECOUP_REFRESH_SIGNATURE_INVALID = 1575,
+
/**
* The exchange could not find the corresponding melt operation. The request is denied.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -988,6 +1295,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RECOUP_REFRESH_MELT_NOT_FOUND = 1576,
+
/**
* The exchange failed to reproduce the coin's blinding.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -995,6 +1303,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RECOUP_REFRESH_BLINDING_FAILED = 1578,
+
/**
* The coin's denomination has not been revoked yet.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -1002,6 +1311,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RECOUP_REFRESH_NOT_ELIGIBLE = 1580,
+
/**
* This exchange does not allow clients to request /keys for times other than the current (exchange) time.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1009,6 +1319,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_KEYS_TIMETRAVEL_FORBIDDEN = 1600,
+
/**
* A signature in the server's response was malformed.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -1016,6 +1327,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_WIRE_SIGNATURE_INVALID = 1650,
+
/**
* No bank accounts are enabled for the exchange. The administrator should enable-account using the taler-exchange-offline tool.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -1023,6 +1335,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_WIRE_NO_ACCOUNTS_CONFIGURED = 1651,
+
/**
* The payto:// URI stored in the exchange database for its bank account is malformed.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -1030,6 +1343,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_WIRE_INVALID_PAYTO_CONFIGURED = 1652,
+
/**
* No wire fees are configured for an enabled wire method of the exchange. The administrator must set the wire-fee using the taler-exchange-offline tool.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -1037,6 +1351,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_WIRE_FEES_NOT_CONFIGURED = 1653,
+
/**
* This purse was previously created with different meta data.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -1044,6 +1359,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RESERVES_PURSE_CREATE_CONFLICTING_META_DATA = 1675,
+
/**
* This purse was previously merged with different meta data.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -1051,6 +1367,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RESERVES_PURSE_MERGE_CONFLICTING_META_DATA = 1676,
+
/**
* The reserve has insufficient funds to create another purse.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -1058,6 +1375,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RESERVES_PURSE_CREATE_INSUFFICIENT_FUNDS = 1677,
+
/**
* The purse fee specified for the request is lower than the purse fee charged by the exchange at this time.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -1065,13 +1383,39 @@ export enum TalerErrorCode {
*/
EXCHANGE_RESERVES_PURSE_FEE_TOO_LOW = 1678,
+
/**
- * The exchange failed to talk to the process responsible for its private denomination keys.
- * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * The payment request cannot be deleted anymore, as it either already completed or timed out.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_PURSE_DELETE_ALREADY_DECIDED = 1679,
+
+
+ /**
+ * The signature affirming the purse deletion is invalid.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_PURSE_DELETE_SIGNATURE_INVALID = 1680,
+
+
+ /**
+ * Withdrawal from the reserve requires age restriction to be set.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_RESERVES_AGE_RESTRICTION_REQUIRED = 1681,
+
+
+ /**
+ * The exchange failed to talk to the process responsible for its private denomination keys or the helpers had no denominations (properly) configured.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
* (A value of 0 indicates that the error is generated client-side).
*/
EXCHANGE_DENOMINATION_HELPER_UNAVAILABLE = 1700,
+
/**
* The response from the denomination key helper process was malformed.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -1079,6 +1423,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_DENOMINATION_HELPER_BUG = 1701,
+
/**
* The helper refuses to sign with the key, because it is too early: the validity period has not yet started.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -1086,6 +1431,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_DENOMINATION_HELPER_TOO_EARLY = 1702,
+
/**
* The signature of the exchange on the reply was invalid.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -1093,13 +1439,15 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSE_DEPOSIT_EXCHANGE_SIGNATURE_INVALID = 1725,
+
/**
* The exchange failed to talk to the process responsible for its private signing keys.
- * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
* (A value of 0 indicates that the error is generated client-side).
*/
EXCHANGE_SIGNKEY_HELPER_UNAVAILABLE = 1750,
+
/**
* The response from the online signing key helper process was malformed.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -1107,6 +1455,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_SIGNKEY_HELPER_BUG = 1751,
+
/**
* The helper refuses to sign with the key, because it is too early: the validity period has not yet started.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -1114,6 +1463,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_SIGNKEY_HELPER_TOO_EARLY = 1752,
+
/**
* The purse expiration time is in the past at the time of its creation.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -1121,6 +1471,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RESERVES_PURSE_EXPIRATION_BEFORE_NOW = 1775,
+
/**
* The purse expiration time is set to never, which is not allowed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -1128,6 +1479,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RESERVES_PURSE_EXPIRATION_IS_NEVER = 1776,
+
/**
* The signature affirming the merge of the purse is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1135,6 +1487,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RESERVES_PURSE_MERGE_SIGNATURE_INVALID = 1777,
+
/**
* The signature by the reserve affirming the merge is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1142,6 +1495,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RESERVES_RESERVE_MERGE_SIGNATURE_INVALID = 1778,
+
/**
* The signature by the reserve affirming the open operation is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1149,6 +1503,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RESERVES_OPEN_BAD_SIGNATURE = 1785,
+
/**
* The signature by the reserve affirming the close operation is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1156,6 +1511,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RESERVES_CLOSE_BAD_SIGNATURE = 1786,
+
/**
* The signature by the reserve affirming the attestion request is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1163,6 +1519,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_RESERVES_ATTEST_BAD_SIGNATURE = 1787,
+
/**
* The exchange does not know an origin account to which the remaining reserve balance could be wired to, and the wallet failed to provide one.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -1170,6 +1527,15 @@ export enum TalerErrorCode {
*/
EXCHANGE_RESERVES_CLOSE_NO_TARGET_ACCOUNT = 1788,
+
+ /**
+ * The reserve balance is insufficient to pay for the open operation.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_RESERVES_OPEN_INSUFFICIENT_FUNDS = 1789,
+
+
/**
* The auditor that was supposed to be disabled is unknown to this exchange.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -1177,6 +1543,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MANAGEMENT_AUDITOR_NOT_FOUND = 1800,
+
/**
* The exchange has a more recently signed conflicting instruction and is thus refusing the current change (replay detected).
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -1184,6 +1551,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MANAGEMENT_AUDITOR_MORE_RECENT_PRESENT = 1801,
+
/**
* The signature to add or enable the auditor does not validate.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1191,6 +1559,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MANAGEMENT_AUDITOR_ADD_SIGNATURE_INVALID = 1802,
+
/**
* The signature to disable the auditor does not validate.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1198,6 +1567,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MANAGEMENT_AUDITOR_DEL_SIGNATURE_INVALID = 1803,
+
/**
* The signature to revoke the denomination does not validate.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1205,6 +1575,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MANAGEMENT_DENOMINATION_REVOKE_SIGNATURE_INVALID = 1804,
+
/**
* The signature to revoke the online signing key does not validate.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1212,6 +1583,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MANAGEMENT_SIGNKEY_REVOKE_SIGNATURE_INVALID = 1805,
+
/**
* The exchange has a more recently signed conflicting instruction and is thus refusing the current change (replay detected).
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -1219,6 +1591,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MANAGEMENT_WIRE_MORE_RECENT_PRESENT = 1806,
+
/**
* The signingkey specified is unknown to the exchange.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -1226,6 +1599,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MANAGEMENT_KEYS_SIGNKEY_UNKNOWN = 1807,
+
/**
* The signature to publish wire account does not validate.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1233,6 +1607,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MANAGEMENT_WIRE_DETAILS_SIGNATURE_INVALID = 1808,
+
/**
* The signature to add the wire account does not validate.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1240,6 +1615,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MANAGEMENT_WIRE_ADD_SIGNATURE_INVALID = 1809,
+
/**
* The signature to disable the wire account does not validate.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1247,6 +1623,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MANAGEMENT_WIRE_DEL_SIGNATURE_INVALID = 1810,
+
/**
* The wire account to be disabled is unknown to the exchange.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -1254,6 +1631,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MANAGEMENT_WIRE_NOT_FOUND = 1811,
+
/**
* The signature to affirm wire fees does not validate.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1261,6 +1639,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MANAGEMENT_WIRE_FEE_SIGNATURE_INVALID = 1812,
+
/**
* The signature conflicts with a previous signature affirming different fees.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -1268,6 +1647,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MANAGEMENT_WIRE_FEE_MISMATCH = 1813,
+
/**
* The signature affirming the denomination key is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1275,6 +1655,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MANAGEMENT_KEYS_DENOMKEY_ADD_SIGNATURE_INVALID = 1814,
+
/**
* The signature affirming the signing key is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1282,6 +1663,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MANAGEMENT_KEYS_SIGNKEY_ADD_SIGNATURE_INVALID = 1815,
+
/**
* The signature conflicts with a previous signature affirming different fees.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -1289,6 +1671,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MANAGEMENT_GLOBAL_FEE_MISMATCH = 1816,
+
/**
* The signature affirming the fee structure is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1296,6 +1679,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_MANAGEMENT_GLOBAL_FEE_SIGNATURE_INVALID = 1817,
+
/**
* The signature affirming the profit drain is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1303,6 +1687,55 @@ export enum TalerErrorCode {
*/
EXCHANGE_MANAGEMENT_DRAIN_PROFITS_SIGNATURE_INVALID = 1818,
+
+ /**
+ * The signature affirming the AML decision is invalid.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_AML_DECISION_ADD_SIGNATURE_INVALID = 1825,
+
+
+ /**
+ * The AML officer specified is not allowed to make AML decisions right now.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_AML_DECISION_INVALID_OFFICER = 1826,
+
+
+ /**
+ * There is a more recent AML decision on file. The decision was rejected as timestamps of AML decisions must be monotonically increasing.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_AML_DECISION_MORE_RECENT_PRESENT = 1827,
+
+
+ /**
+ * There AML decision would impose an AML check of a type that is not provided by any KYC provider known to the exchange.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_AML_DECISION_UNKNOWN_CHECK = 1828,
+
+
+ /**
+ * The signature affirming the change in the AML officer status is invalid.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_MANAGEMENT_UPDATE_AML_OFFICER_SIGNATURE_INVALID = 1830,
+
+
+ /**
+ * A more recent decision about the AML officer status is known to the exchange.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_MANAGEMENT_AML_OFFICERS_MORE_RECENT_PRESENT = 1831,
+
+
/**
* The purse was previously created with different meta data.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -1310,6 +1743,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSE_CREATE_CONFLICTING_META_DATA = 1850,
+
/**
* The purse was previously created with a different contract.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -1317,6 +1751,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSE_CREATE_CONFLICTING_CONTRACT_STORED = 1851,
+
/**
* A coin signature for a deposit into the purse is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1324,6 +1759,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSE_CREATE_COIN_SIGNATURE_INVALID = 1852,
+
/**
* The purse expiration time is in the past.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -1331,6 +1767,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSE_CREATE_EXPIRATION_BEFORE_NOW = 1853,
+
/**
* The purse expiration time is "never".
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -1338,6 +1775,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSE_CREATE_EXPIRATION_IS_NEVER = 1854,
+
/**
* The purse signature over the purse meta data is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1345,6 +1783,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSE_CREATE_SIGNATURE_INVALID = 1855,
+
/**
* The signature over the encrypted contract is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1352,6 +1791,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSE_ECONTRACT_SIGNATURE_INVALID = 1856,
+
/**
* The signature from the exchange over the confirmation is invalid.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -1359,6 +1799,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSE_CREATE_EXCHANGE_SIGNATURE_INVALID = 1857,
+
/**
* The coin was previously deposited with different meta data.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -1366,6 +1807,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSE_DEPOSIT_CONFLICTING_META_DATA = 1858,
+
/**
* The encrypted contract was previously uploaded with different meta data.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -1373,6 +1815,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSE_ECONTRACT_CONFLICTING_META_DATA = 1859,
+
/**
* The deposited amount is less than the purse fee.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -1380,6 +1823,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_CREATE_PURSE_NEGATIVE_VALUE_AFTER_FEE = 1860,
+
/**
* The signature using the merge key is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1387,6 +1831,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSE_MERGE_INVALID_MERGE_SIGNATURE = 1876,
+
/**
* The signature using the reserve key is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1394,6 +1839,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSE_MERGE_INVALID_RESERVE_SIGNATURE = 1877,
+
/**
* The targeted purse is not yet full and thus cannot be merged. Retrying the request later may succeed.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -1401,6 +1847,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSE_NOT_FULL = 1878,
+
/**
* The signature from the exchange over the confirmation is invalid.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -1408,6 +1855,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSE_MERGE_EXCHANGE_SIGNATURE_INVALID = 1879,
+
/**
* The exchange of the target account is not a partner of this exchange.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -1415,6 +1863,23 @@ export enum TalerErrorCode {
*/
EXCHANGE_MERGE_PURSE_PARTNER_UNKNOWN = 1880,
+
+ /**
+ * The signature affirming the new partner is invalid.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_MANAGEMENT_ADD_PARTNER_SIGNATURE_INVALID = 1890,
+
+
+ /**
+ * Conflicting data for the partner already exists with the exchange.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_MANAGEMENT_ADD_PARTNER_DATA_CONFLICT = 1891,
+
+
/**
* The auditor signature over the denomination meta data is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1422,6 +1887,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_AUDITORS_AUDITOR_SIGNATURE_INVALID = 1900,
+
/**
* The auditor that was specified is unknown to this exchange.
* Returned with an HTTP status code of #MHD_HTTP_PRECONDITION_FAILED (412).
@@ -1429,6 +1895,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_AUDITORS_AUDITOR_UNKNOWN = 1901,
+
/**
* The auditor that was specified is no longer used by this exchange.
* Returned with an HTTP status code of #MHD_HTTP_GONE (410).
@@ -1436,6 +1903,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_AUDITORS_AUDITOR_INACTIVE = 1902,
+
/**
* The signature affirming the wallet's KYC request was invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1443,6 +1911,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_KYC_WALLET_SIGNATURE_INVALID = 1925,
+
/**
* The exchange received an unexpected malformed response from its KYC backend.
* Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
@@ -1450,6 +1919,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_KYC_PROOF_BACKEND_INVALID_RESPONSE = 1926,
+
/**
* The backend signaled an unexpected failure.
* Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
@@ -1457,6 +1927,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_KYC_PROOF_BACKEND_ERROR = 1927,
+
/**
* The backend signaled an authorization failure.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1464,6 +1935,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_KYC_PROOF_BACKEND_AUTHORIZATION_FAILED = 1928,
+
/**
* The exchange is unaware of having made an the authorization request.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -1471,6 +1943,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_KYC_PROOF_REQUEST_UNKNOWN = 1929,
+
/**
* The payto-URI hash did not match. Hence the request was denied.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1478,6 +1951,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_KYC_CHECK_AUTHORIZATION_FAILED = 1930,
+
/**
* The request used a logic specifier that is not known to the exchange.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -1485,6 +1959,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_KYC_GENERIC_LOGIC_UNKNOWN = 1931,
+
/**
* The request requires a logic which is no longer configured at the exchange.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -1492,6 +1967,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_KYC_GENERIC_LOGIC_GONE = 1932,
+
/**
* The logic plugin had a bug in its interaction with the KYC provider.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -1499,6 +1975,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_KYC_GENERIC_LOGIC_BUG = 1933,
+
/**
* The exchange could not process the request with its KYC provider because the provider refused access to the service. This indicates some configuration issue at the Taler exchange operator.
* Returned with an HTTP status code of #MHD_HTTP_NETWORK_AUTHENTICATION_REQUIRED (511).
@@ -1506,6 +1983,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_KYC_GENERIC_PROVIDER_ACCESS_REFUSED = 1934,
+
/**
* There was a timeout in the interaction between the exchange and the KYC provider. The most likely cause is some networking problem. Trying again later might succeed.
* Returned with an HTTP status code of #MHD_HTTP_GATEWAY_TIMEOUT (504).
@@ -1513,6 +1991,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_KYC_GENERIC_PROVIDER_TIMEOUT = 1935,
+
/**
* The KYC provider responded with a status that was completely unexpected by the KYC logic of the exchange.
* Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
@@ -1520,6 +1999,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_KYC_GENERIC_PROVIDER_UNEXPECTED_REPLY = 1936,
+
/**
* The rate limit of the exchange at the KYC provider has been exceeded. Trying much later might work.
* Returned with an HTTP status code of #MHD_HTTP_SERVICE_UNAVAILABLE (503).
@@ -1527,6 +2007,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_KYC_GENERIC_PROVIDER_RATE_LIMIT_EXCEEDED = 1937,
+
/**
* The request to the webhook lacked proper authorization or authentication data.
* Returned with an HTTP status code of #MHD_HTTP_UNAUTHORIZED (401).
@@ -1534,6 +2015,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_KYC_WEBHOOK_UNAUTHORIZED = 1938,
+
/**
* The exchange does not know a contract under the given contract public key.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -1541,6 +2023,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_CONTRACTS_UNKNOWN = 1950,
+
/**
* The URL does not encode a valid exchange public key in its path.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -1548,6 +2031,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_CONTRACTS_INVALID_CONTRACT_PUB = 1951,
+
/**
* The returned encrypted contract did not decrypt.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -1555,6 +2039,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_CONTRACTS_DECRYPTION_FAILED = 1952,
+
/**
* The signature on the encrypted contract did not validate.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -1562,6 +2047,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_CONTRACTS_SIGNATURE_INVALID = 1953,
+
/**
* The decrypted contract was malformed.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -1569,6 +2055,7 @@ export enum TalerErrorCode {
*/
EXCHANGE_CONTRACTS_DECODING_FAILED = 1954,
+
/**
* A coin signature for a deposit into the purse is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1576,6 +2063,23 @@ export enum TalerErrorCode {
*/
EXCHANGE_PURSE_DEPOSIT_COIN_SIGNATURE_INVALID = 1975,
+
+ /**
+ * It is too late to deposit coins into the purse.
+ * Returned with an HTTP status code of #MHD_HTTP_GONE (410).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_PURSE_DEPOSIT_DECIDED_ALREADY = 1976,
+
+
+ /**
+ * TOTP key is not valid.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_TOTP_KEY_INVALID = 1980,
+
+
/**
* The backend could not find the merchant instance specified in the request.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -1583,6 +2087,7 @@ export enum TalerErrorCode {
*/
MERCHANT_GENERIC_INSTANCE_UNKNOWN = 2000,
+
/**
* The start and end-times in the wire fee structure leave a hole. This is not allowed.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -1590,6 +2095,15 @@ export enum TalerErrorCode {
*/
MERCHANT_GENERIC_HOLE_IN_WIRE_FEE_STRUCTURE = 2001,
+
+ /**
+ * The merchant was unable to obtain a valid answer to /wire from the exchange.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_GENERIC_EXCHANGE_WIRE_REQUEST_FAILED = 2002,
+
+
/**
* The proposal is not known to the backend.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -1597,6 +2111,7 @@ export enum TalerErrorCode {
*/
MERCHANT_GENERIC_ORDER_UNKNOWN = 2005,
+
/**
* The order provided to the backend could not be completed, because a product to be completed via inventory data is not actually in our inventory.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -1604,12 +2119,14 @@ export enum TalerErrorCode {
*/
MERCHANT_GENERIC_PRODUCT_UNKNOWN = 2006,
+
/**
- * The tip ID is unknown. This could happen if the tip has expired.
+ * The reward ID is unknown. This could happen if the reward has expired.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
* (A value of 0 indicates that the error is generated client-side).
*/
- MERCHANT_GENERIC_TIP_ID_UNKNOWN = 2007,
+ MERCHANT_GENERIC_REWARD_ID_UNKNOWN = 2007,
+
/**
* The contract obtained from the merchant backend was malformed.
@@ -1618,6 +2135,7 @@ export enum TalerErrorCode {
*/
MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID = 2008,
+
/**
* The order we found does not match the provided contract hash.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1625,6 +2143,7 @@ export enum TalerErrorCode {
*/
MERCHANT_GENERIC_CONTRACT_HASH_DOES_NOT_MATCH_ORDER = 2009,
+
/**
* The exchange failed to provide a valid response to the merchant's /keys request.
* Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
@@ -1632,6 +2151,7 @@ export enum TalerErrorCode {
*/
MERCHANT_GENERIC_EXCHANGE_KEYS_FAILURE = 2010,
+
/**
* The exchange failed to respond to the merchant on time.
* Returned with an HTTP status code of #MHD_HTTP_GATEWAY_TIMEOUT (504).
@@ -1639,6 +2159,7 @@ export enum TalerErrorCode {
*/
MERCHANT_GENERIC_EXCHANGE_TIMEOUT = 2011,
+
/**
* The merchant failed to talk to the exchange.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -1646,6 +2167,7 @@ export enum TalerErrorCode {
*/
MERCHANT_GENERIC_EXCHANGE_CONNECT_FAILURE = 2012,
+
/**
* The exchange returned a maformed response.
* Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
@@ -1653,6 +2175,7 @@ export enum TalerErrorCode {
*/
MERCHANT_GENERIC_EXCHANGE_REPLY_MALFORMED = 2013,
+
/**
* The exchange returned an unexpected response status.
* Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
@@ -1660,6 +2183,7 @@ export enum TalerErrorCode {
*/
MERCHANT_GENERIC_EXCHANGE_UNEXPECTED_STATUS = 2014,
+
/**
* The merchant refused the request due to lack of authorization.
* Returned with an HTTP status code of #MHD_HTTP_UNAUTHORIZED (401).
@@ -1667,6 +2191,7 @@ export enum TalerErrorCode {
*/
MERCHANT_GENERIC_UNAUTHORIZED = 2015,
+
/**
* The merchant instance specified in the request was deleted.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -1674,6 +2199,7 @@ export enum TalerErrorCode {
*/
MERCHANT_GENERIC_INSTANCE_DELETED = 2016,
+
/**
* The backend could not find the inbound wire transfer specified in the request.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -1681,6 +2207,63 @@ export enum TalerErrorCode {
*/
MERCHANT_GENERIC_TRANSFER_UNKNOWN = 2017,
+
+ /**
+ * The backend could not find the template(id) because it is not exist.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_GENERIC_TEMPLATE_UNKNOWN = 2018,
+
+
+ /**
+ * The backend could not find the webhook(id) because it is not exist.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_GENERIC_WEBHOOK_UNKNOWN = 2019,
+
+
+ /**
+ * The backend could not find the webhook(serial) because it is not exist.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_GENERIC_PENDING_WEBHOOK_UNKNOWN = 2020,
+
+
+ /**
+ * The backend could not find the OTP device(id) because it is not exist.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_GENERIC_OTP_DEVICE_UNKNOWN = 2021,
+
+
+ /**
+ * The account is not known to the backend.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_GENERIC_ACCOUNT_UNKNOWN = 2022,
+
+
+ /**
+ * The wire hash was malformed.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_GENERIC_H_WIRE_MALFORMED = 2023,
+
+
+ /**
+ * The currency specified in the operation does not work with the current state of the given resource.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_GENERIC_CURRENCY_MISMATCH = 2024,
+
+
/**
* The exchange failed to provide a valid answer to the tracking request, thus those details are not in the response.
* Returned with an HTTP status code of #MHD_HTTP_OK (200).
@@ -1688,6 +2271,7 @@ export enum TalerErrorCode {
*/
MERCHANT_GET_ORDERS_EXCHANGE_TRACKING_FAILURE = 2100,
+
/**
* The merchant backend failed to construct the request for tracking to the exchange, thus tracking details are not in the response.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -1695,6 +2279,7 @@ export enum TalerErrorCode {
*/
MERCHANT_GET_ORDERS_ID_EXCHANGE_REQUEST_FAILURE = 2103,
+
/**
* The merchant backend failed trying to contact the exchange for tracking details, thus those details are not in the response.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -1702,6 +2287,7 @@ export enum TalerErrorCode {
*/
MERCHANT_GET_ORDERS_ID_EXCHANGE_LOOKUP_START_FAILURE = 2104,
+
/**
* The claim token used to authenticate the client is invalid for this order.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1709,6 +2295,7 @@ export enum TalerErrorCode {
*/
MERCHANT_GET_ORDERS_ID_INVALID_TOKEN = 2105,
+
/**
* The contract terms hash used to authenticate the client is invalid for this order.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1716,6 +2303,7 @@ export enum TalerErrorCode {
*/
MERCHANT_GET_ORDERS_ID_INVALID_CONTRACT_HASH = 2106,
+
/**
* The exchange responded saying that funds were insufficient (for example, due to double-spending).
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -1723,6 +2311,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_INSUFFICIENT_FUNDS = 2150,
+
/**
* The denomination key used for payment is not listed among the denomination keys of the exchange.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -1730,6 +2319,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_DENOMINATION_KEY_NOT_FOUND = 2151,
+
/**
* The denomination key used for payment is not audited by an auditor approved by the merchant.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -1737,6 +2327,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_DENOMINATION_KEY_AUDITOR_FAILURE = 2152,
+
/**
* There was an integer overflow totaling up the amounts or deposit fees in the payment.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -1744,6 +2335,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_AMOUNT_OVERFLOW = 2153,
+
/**
* The deposit fees exceed the total value of the payment.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -1751,20 +2343,23 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_FEES_EXCEED_PAYMENT = 2154,
+
/**
* After considering deposit and wire fees, the payment is insufficient to satisfy the required amount for the contract. The client should revisit the logic used to calculate fees it must cover.
- * Returned with an HTTP status code of #MHD_HTTP_NOT_ACCEPTABLE (406).
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
* (A value of 0 indicates that the error is generated client-side).
*/
MERCHANT_POST_ORDERS_ID_PAY_INSUFFICIENT_DUE_TO_FEES = 2155,
+
/**
* Even if we do not consider deposit and wire fees, the payment is insufficient to satisfy the required amount for the contract.
- * Returned with an HTTP status code of #MHD_HTTP_NOT_ACCEPTABLE (406).
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
* (A value of 0 indicates that the error is generated client-side).
*/
MERCHANT_POST_ORDERS_ID_PAY_PAYMENT_INSUFFICIENT = 2156,
+
/**
* The signature over the contract of one of the coins was invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1772,6 +2367,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_COIN_SIGNATURE_INVALID = 2157,
+
/**
* When we tried to find information about the exchange to issue the deposit, we failed. This usually only happens if the merchant backend is somehow unable to get its own HTTP client logic to work.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -1779,6 +2375,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_EXCHANGE_LOOKUP_FAILED = 2158,
+
/**
* The refund deadline in the contract is after the transfer deadline.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -1786,6 +2383,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_REFUND_DEADLINE_PAST_WIRE_TRANSFER_DEADLINE = 2159,
+
/**
* The order was already paid (maybe by another wallet).
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -1793,6 +2391,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_ALREADY_PAID = 2160,
+
/**
* The payment is too late, the offer has expired.
* Returned with an HTTP status code of #MHD_HTTP_GONE (410).
@@ -1800,6 +2399,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_OFFER_EXPIRED = 2161,
+
/**
* The "merchant" field is missing in the proposal data. This is an internal error as the proposal is from the merchant's own database at this point.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -1807,6 +2407,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_MERCHANT_FIELD_MISSING = 2162,
+
/**
* Failed to locate merchant's account information matching the wire hash given in the proposal.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -1814,6 +2415,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_WIRE_HASH_UNKNOWN = 2163,
+
/**
* The deposit time for the denomination has expired.
* Returned with an HTTP status code of #MHD_HTTP_GONE (410).
@@ -1821,6 +2423,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_DENOMINATION_DEPOSIT_EXPIRED = 2165,
+
/**
* The exchange of the deposited coin charges a wire fee that could not be added to the total (total amount too high).
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -1828,6 +2431,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_EXCHANGE_WIRE_FEE_ADDITION_FAILED = 2166,
+
/**
* The contract was not fully paid because of refunds. Note that clients MAY treat this as paid if, for example, contracts must be executed despite of refunds.
* Returned with an HTTP status code of #MHD_HTTP_PAYMENT_REQUIRED (402).
@@ -1835,6 +2439,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_REFUNDED = 2167,
+
/**
* According to our database, we have refunded more than we were paid (which should not be possible).
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -1842,6 +2447,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_REFUNDS_EXCEED_PAYMENTS = 2168,
+
/**
* Legacy stuff. Remove me with protocol v1.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -1849,6 +2455,7 @@ export enum TalerErrorCode {
*/
DEAD_QQQ_PAY_MERCHANT_POST_ORDERS_ID_ABORT_REFUND_REFUSED_PAYMENT_COMPLETE = 2169,
+
/**
* The payment failed at the exchange.
* Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
@@ -1856,6 +2463,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_EXCHANGE_FAILED = 2170,
+
/**
* The payment required a minimum age but one of the coins (of a denomination with support for age restriction) did not provide any age_commitment.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -1863,6 +2471,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_AGE_COMMITMENT_MISSING = 2171,
+
/**
* The payment required a minimum age but one of the coins provided an age_commitment that contained a wrong number of public keys compared to the number of age groups defined in the denomination of the coin.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -1870,6 +2479,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_AGE_COMMITMENT_SIZE_MISMATCH = 2172,
+
/**
* The payment required a minimum age but one of the coins provided a minimum_age_sig that couldn't be verified with the given age_commitment for that particular minimum age.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -1877,6 +2487,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_AGE_VERIFICATION_FAILED = 2173,
+
/**
* The payment required no minimum age but one of the coins (of a denomination with support for age restriction) did not provide the required h_age_commitment.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -1884,6 +2495,71 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAY_AGE_COMMITMENT_HASH_MISSING = 2174,
+
+ /**
+ * The exchange does not support the selected bank account of the merchant. Likely the merchant had stale data on the bank accounts of the exchange and thus selected an inappropriate exchange when making the offer.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_ORDERS_ID_PAY_WIRE_METHOD_UNSUPPORTED = 2175,
+
+
+ /**
+ * The payment requires the wallet to select a choice from the choices array and pass it in the 'choice_index' field of the request.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_ORDERS_ID_PAY_CHOICE_INDEX_MISSING = 2176,
+
+
+ /**
+ * The 'choice_index' field is invalid.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_ORDERS_ID_PAY_CHOICE_INDEX_OUT_OF_BOUNDS = 2177,
+
+
+ /**
+ * The provided 'tokens' array does not match with the required input tokens of the order.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_ORDERS_ID_PAY_INPUT_TOKENS_MISMATCH = 2178,
+
+
+ /**
+ * Invalid token issue signature (blindly signed by merchant) for provided token.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_ORDERS_ID_PAY_TOKEN_ISSUE_SIG_INVALID = 2179,
+
+
+ /**
+ * Invalid token use signature (EdDSA, signed by wallet) for provided token.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_ORDERS_ID_PAY_TOKEN_USE_SIG_INVALID = 2180,
+
+
+ /**
+ * The provided number of tokens does not match the required number.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_ORDERS_ID_PAY_TOKEN_COUNT_MISMATCH = 2181,
+
+
+ /**
+ * The provided number of token envelopes does not match the specified number.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_ORDERS_ID_PAY_TOKEN_ENVELOPE_COUNT_MISMATCH = 2182,
+
+
/**
* The contract hash does not match the given order ID.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -1891,6 +2567,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAID_CONTRACT_HASH_MISMATCH = 2200,
+
/**
* The signature of the merchant is not valid for the given contract hash.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1898,6 +2575,23 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_PAID_COIN_SIGNATURE_INVALID = 2201,
+
+ /**
+ * A token family with this ID but conflicting data exists.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_TOKEN_FAMILY_CONFLICT = 2225,
+
+
+ /**
+ * The backend is unaware of a token family with the given ID.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PATCH_TOKEN_FAMILY_NOT_FOUND = 2226,
+
+
/**
* The merchant failed to send the exchange the refund request.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -1905,6 +2599,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_ABORT_EXCHANGE_REFUND_FAILED = 2251,
+
/**
* The merchant failed to find the exchange to process the lookup.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -1912,6 +2607,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_ABORT_EXCHANGE_LOOKUP_FAILED = 2252,
+
/**
* The merchant could not find the contract.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -1919,6 +2615,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_ABORT_CONTRACT_NOT_FOUND = 2253,
+
/**
* The payment was already completed and thus cannot be aborted anymore.
* Returned with an HTTP status code of #MHD_HTTP_PRECONDITION_FAILED (412).
@@ -1926,6 +2623,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_ABORT_REFUND_REFUSED_PAYMENT_COMPLETE = 2254,
+
/**
* The hash provided by the wallet does not match the order.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -1933,6 +2631,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_ABORT_CONTRACT_HASH_MISSMATCH = 2255,
+
/**
* The array of coins cannot be empty.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -1940,6 +2639,63 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_ABORT_COINS_ARRAY_EMPTY = 2256,
+
+ /**
+ * We are waiting for the exchange to provide us with key material before checking the wire transfer.
+ * Returned with an HTTP status code of #MHD_HTTP_ACCEPTED (202).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_EXCHANGE_TRANSFERS_AWAITING_KEYS = 2258,
+
+
+ /**
+ * We are waiting for the exchange to provide us with the list of aggregated transactions.
+ * Returned with an HTTP status code of #MHD_HTTP_ACCEPTED (202).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_EXCHANGE_TRANSFERS_AWAITING_LIST = 2259,
+
+
+ /**
+ * The endpoint indicated in the wire transfer does not belong to a GNU Taler exchange.
+ * Returned with an HTTP status code of #MHD_HTTP_OK (200).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_EXCHANGE_TRANSFERS_FATAL_NO_EXCHANGE = 2260,
+
+
+ /**
+ * The exchange indicated in the wire transfer claims to know nothing about the wire transfer.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_EXCHANGE_TRANSFERS_FATAL_NOT_FOUND = 2261,
+
+
+ /**
+ * The interaction with the exchange is delayed due to rate limiting.
+ * Returned with an HTTP status code of #MHD_HTTP_ACCEPTED (202).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_EXCHANGE_TRANSFERS_RATE_LIMITED = 2262,
+
+
+ /**
+ * We experienced a transient failure in our interaction with the exchange.
+ * Returned with an HTTP status code of #MHD_HTTP_ACCEPTED (202).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_EXCHANGE_TRANSFERS_TRANSIENT_FAILURE = 2263,
+
+
+ /**
+ * The response from the exchange was unacceptable and should be reviewed with an auditor.
+ * Returned with an HTTP status code of #MHD_HTTP_OK (200).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_EXCHANGE_TRANSFERS_HARD_FAILURE = 2264,
+
+
/**
* We could not claim the order because the backend is unaware of it.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -1947,6 +2703,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_CLAIM_NOT_FOUND = 2300,
+
/**
* We could not claim the order because someone else claimed it first.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -1954,6 +2711,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_CLAIM_ALREADY_CLAIMED = 2301,
+
/**
* The client-side experienced an internal failure.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -1961,6 +2719,7 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_CLAIM_CLIENT_INTERNAL_FAILURE = 2302,
+
/**
* The backend failed to sign the refund request.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -1968,97 +2727,135 @@ export enum TalerErrorCode {
*/
MERCHANT_POST_ORDERS_ID_REFUND_SIGNATURE_FAILED = 2350,
+
/**
* The client failed to unblind the signature returned by the merchant.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
* (A value of 0 indicates that the error is generated client-side).
*/
- MERCHANT_TIP_PICKUP_UNBLIND_FAILURE = 2400,
+ MERCHANT_REWARD_PICKUP_UNBLIND_FAILURE = 2400,
+
/**
* The exchange returned a failure code for the withdraw operation.
* Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
* (A value of 0 indicates that the error is generated client-side).
*/
- MERCHANT_TIP_PICKUP_EXCHANGE_ERROR = 2403,
+ MERCHANT_REWARD_PICKUP_EXCHANGE_ERROR = 2403,
+
/**
* The merchant failed to add up the amounts to compute the pick up value.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
* (A value of 0 indicates that the error is generated client-side).
*/
- MERCHANT_TIP_PICKUP_SUMMATION_FAILED = 2404,
+ MERCHANT_REWARD_PICKUP_SUMMATION_FAILED = 2404,
+
/**
- * The tip expired.
+ * The reward expired.
* Returned with an HTTP status code of #MHD_HTTP_GONE (410).
* (A value of 0 indicates that the error is generated client-side).
*/
- MERCHANT_TIP_PICKUP_HAS_EXPIRED = 2405,
+ MERCHANT_REWARD_PICKUP_HAS_EXPIRED = 2405,
+
/**
* The requested withdraw amount exceeds the amount remaining to be picked up.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
* (A value of 0 indicates that the error is generated client-side).
*/
- MERCHANT_TIP_PICKUP_AMOUNT_EXCEEDS_TIP_REMAINING = 2406,
+ MERCHANT_REWARD_PICKUP_AMOUNT_EXCEEDS_REWARD_REMAINING = 2406,
+
/**
* The merchant did not find the specified denomination key in the exchange's key set.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
* (A value of 0 indicates that the error is generated client-side).
*/
- MERCHANT_TIP_PICKUP_DENOMINATION_UNKNOWN = 2407,
+ MERCHANT_REWARD_PICKUP_DENOMINATION_UNKNOWN = 2407,
+
/**
- * The backend lacks a wire transfer method configuration option for the given instance. Thus, this instance is unavailable (not findable for creating new orders).
+ * The merchant instance has no active bank accounts configured. However, at least one bank account must be available to create new orders.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
* (A value of 0 indicates that the error is generated client-side).
*/
MERCHANT_PRIVATE_POST_ORDERS_INSTANCE_CONFIGURATION_LACKS_WIRE = 2500,
+
/**
- * The proposal had no timestamp and the backend failed to obtain the local time. Likely to be an internal error.
+ * The proposal had no timestamp and the merchant backend failed to obtain the current local time.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
* (A value of 0 indicates that the error is generated client-side).
*/
MERCHANT_PRIVATE_POST_ORDERS_NO_LOCALTIME = 2501,
+
/**
- * The order provided to the backend could not be parsed, some required fields were missing or ill-formed.
+ * The order provided to the backend could not be parsed; likely some required fields were missing or ill-formed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
* (A value of 0 indicates that the error is generated client-side).
*/
MERCHANT_PRIVATE_POST_ORDERS_PROPOSAL_PARSE_ERROR = 2502,
+
/**
- * The backend encountered an error: the proposal already exists.
+ * A conflicting order (sharing the same order identifier) already exists at this merchant backend instance.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
* (A value of 0 indicates that the error is generated client-side).
*/
MERCHANT_PRIVATE_POST_ORDERS_ALREADY_EXISTS = 2503,
+
/**
- * The request is invalid: the wire deadline is before the refund deadline.
+ * The order creation request is invalid because the given wire deadline is before the refund deadline.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
* (A value of 0 indicates that the error is generated client-side).
*/
MERCHANT_PRIVATE_POST_ORDERS_REFUND_AFTER_WIRE_DEADLINE = 2504,
+
/**
- * The request is invalid: a delivery date was given, but it is in the past.
+ * The order creation request is invalid because the delivery date given is in the past.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
* (A value of 0 indicates that the error is generated client-side).
*/
MERCHANT_PRIVATE_POST_ORDERS_DELIVERY_DATE_IN_PAST = 2505,
+
/**
- * The request is invalid: the wire deadline for the order would be "never".
+ * The order creation request is invalid because a wire deadline of "never" is not allowed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
* (A value of 0 indicates that the error is generated client-side).
*/
MERCHANT_PRIVATE_POST_ORDERS_WIRE_DEADLINE_IS_NEVER = 2506,
+
+ /**
+ * The order creation request is invalid because the given payment deadline is in the past.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_POST_ORDERS_PAY_DEADLINE_IN_PAST = 2507,
+
+
+ /**
+ * The order creation request is invalid because the given refund deadline is in the past.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_POST_ORDERS_REFUND_DEADLINE_IN_PAST = 2508,
+
+
+ /**
+ * The backend does not trust any exchange that would allow funds to be wired to any bank account of this instance using the wire method specified with the order. Note that right now, we do not support the use of exchange bank accounts with mandatory currency conversion.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_POST_ORDERS_NO_EXCHANGES_FOR_WIRE_METHOD = 2509,
+
+
/**
* One of the paths to forget is malformed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2066,6 +2863,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_PATCH_ORDERS_ID_FORGET_PATH_SYNTAX_INCORRECT = 2510,
+
/**
* One of the paths to forget was not marked as forgettable.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2073,13 +2871,15 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_PATCH_ORDERS_ID_FORGET_PATH_NOT_FORGETTABLE = 2511,
+
/**
- * The order provided to the backend could not be deleted, our offer is still valid and awaiting payment.
+ * The order provided to the backend could not be deleted, our offer is still valid and awaiting payment. Deletion may work later after the offer has expired if it remains unpaid.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
* (A value of 0 indicates that the error is generated client-side).
*/
MERCHANT_PRIVATE_DELETE_ORDERS_AWAITING_PAYMENT = 2520,
+
/**
* The order provided to the backend could not be deleted as the order was already paid.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2087,27 +2887,39 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_DELETE_ORDERS_ALREADY_PAID = 2521,
+
/**
- * The amount to be refunded is inconsistent: either is lower than the previous amount being awarded, or it is too big to be paid back. In this second case, the fault stays on the business dept. side.
+ * The amount to be refunded is inconsistent: either is lower than the previous amount being awarded, or it exceeds the original price paid by the customer.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
* (A value of 0 indicates that the error is generated client-side).
*/
MERCHANT_PRIVATE_POST_ORDERS_ID_REFUND_INCONSISTENT_AMOUNT = 2530,
+
/**
- * The frontend gave an unpaid order id to issue the refund to.
+ * Only paid orders can be refunded, and the frontend specified an unpaid order to issue a refund for.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
* (A value of 0 indicates that the error is generated client-side).
*/
MERCHANT_PRIVATE_POST_ORDERS_ID_REFUND_ORDER_UNPAID = 2531,
+
/**
- * The refund delay was set to 0 and thus no refunds are allowed for this order.
+ * The refund delay was set to 0 and thus no refunds are ever allowed for this order.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
* (A value of 0 indicates that the error is generated client-side).
*/
MERCHANT_PRIVATE_POST_ORDERS_ID_REFUND_NOT_ALLOWED_BY_CONTRACT = 2532,
+
+ /**
+ * The token family slug provided in this order could not be found in the merchant database.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_POST_ORDERS_TOKEN_FAMILY_SLUG_UNKNOWN = 2533,
+
+
/**
* The exchange says it does not know this transfer.
* Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
@@ -2115,6 +2927,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_TRANSFERS_EXCHANGE_UNKNOWN = 2550,
+
/**
* We internally failed to execute the /track/transfer request.
* Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
@@ -2122,6 +2935,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_TRANSFERS_REQUEST_ERROR = 2551,
+
/**
* The amount transferred differs between what was submitted and what the exchange claimed.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2129,6 +2943,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_TRANSFERS_CONFLICTING_TRANSFERS = 2552,
+
/**
* The exchange gave conflicting information about a coin which has been wire transferred.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2136,6 +2951,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_TRANSFERS_CONFLICTING_REPORTS = 2553,
+
/**
* The exchange charged a different wire fee than what it originally advertised, and it is higher.
* Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
@@ -2143,6 +2959,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_TRANSFERS_BAD_WIRE_FEE = 2554,
+
/**
* We did not find the account that the transfer was made to.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -2150,6 +2967,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_TRANSFERS_ACCOUNT_NOT_FOUND = 2555,
+
/**
* The backend could not delete the transfer as the echange already replied to our inquiry about it and we have integrated the result.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2157,6 +2975,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_DELETE_TRANSFERS_ALREADY_CONFIRMED = 2556,
+
/**
* The backend was previously informed about a wire transfer with the same ID but a different amount. Multiple wire transfers with the same ID are not allowed. If the new amount is correct, the old transfer should first be deleted.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2164,6 +2983,15 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_TRANSFERS_CONFLICTING_SUBMISSION = 2557,
+
+ /**
+ * The amount transferred differs between what was submitted and what the exchange claimed.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_EXCHANGE_TRANSFERS_CONFLICTING_TRANSFERS = 2563,
+
+
/**
* The merchant backend cannot create an instance under the given identifier as one already exists. Use PATCH to modify the existing entry.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2171,6 +2999,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_INSTANCES_ALREADY_EXISTS = 2600,
+
/**
* The merchant backend cannot create an instance because the authentication configuration field is malformed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2178,6 +3007,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_INSTANCES_BAD_AUTH = 2601,
+
/**
* The merchant backend cannot update an instance's authentication settings because the provided authentication settings are malformed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2185,6 +3015,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_INSTANCE_AUTH_BAD_AUTH = 2602,
+
/**
* The merchant backend cannot create an instance under the given identifier, the previous one was deleted but must be purged first.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2192,6 +3023,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_INSTANCES_PURGE_REQUIRED = 2603,
+
/**
* The merchant backend cannot update an instance under the given identifier, the previous one was deleted but must be purged first.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2199,6 +3031,23 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_PATCH_INSTANCES_PURGE_REQUIRED = 2625,
+
+ /**
+ * The bank account referenced in the requested operation was not found.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_ACCOUNT_DELETE_UNKNOWN_ACCOUNT = 2626,
+
+
+ /**
+ * The bank account specified in the request already exists at the merchant.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_ACCOUNT_EXISTS = 2627,
+
+
/**
* The product ID exists.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2206,6 +3055,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_PRODUCTS_CONFLICT_PRODUCT_EXISTS = 2650,
+
/**
* The update would have reduced the total amount of product lost, which is not allowed.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2213,6 +3063,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_LOST_REDUCED = 2660,
+
/**
* The update would have mean that more stocks were lost than what remains from total inventory after sales, which is not allowed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2220,6 +3071,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_LOST_EXCEEDS_STOCKS = 2661,
+
/**
* The update would have reduced the total amount of product in stock, which is not allowed.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2227,6 +3079,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_STOCKED_REDUCED = 2662,
+
/**
* The update would have reduced the total amount of product sold, which is not allowed.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2234,6 +3087,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_SOLD_REDUCED = 2663,
+
/**
* The lock request is for more products than we have left (unlocked) in stock.
* Returned with an HTTP status code of #MHD_HTTP_GONE (410).
@@ -2241,6 +3095,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_PRODUCTS_LOCK_INSUFFICIENT_STOCKS = 2670,
+
/**
* The deletion request is for a product that is locked.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2248,6 +3103,7 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_DELETE_PRODUCTS_CONFLICTING_LOCK = 2680,
+
/**
* The requested wire method is not supported by the exchange.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2255,6 +3111,15 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_POST_RESERVES_UNSUPPORTED_WIRE_METHOD = 2700,
+
+ /**
+ * The requested exchange does not allow rewards.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_POST_RESERVES_REWARDS_NOT_ALLOWED = 2701,
+
+
/**
* The reserve could not be deleted because it is unknown.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -2262,33 +3127,38 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_DELETE_RESERVES_NO_SUCH_RESERVE = 2710,
+
/**
- * The reserve that was used to fund the tips has expired.
+ * The reserve that was used to fund the rewards has expired.
* Returned with an HTTP status code of #MHD_HTTP_GONE (410).
* (A value of 0 indicates that the error is generated client-side).
*/
- MERCHANT_PRIVATE_POST_TIP_AUTHORIZE_RESERVE_EXPIRED = 2750,
+ MERCHANT_PRIVATE_POST_REWARD_AUTHORIZE_RESERVE_EXPIRED = 2750,
+
/**
- * The reserve that was used to fund the tips was not found in the DB.
+ * The reserve that was used to fund the rewards was not found in the DB.
* Returned with an HTTP status code of #MHD_HTTP_SERVICE_UNAVAILABLE (503).
* (A value of 0 indicates that the error is generated client-side).
*/
- MERCHANT_PRIVATE_POST_TIP_AUTHORIZE_RESERVE_UNKNOWN = 2751,
+ MERCHANT_PRIVATE_POST_REWARD_AUTHORIZE_RESERVE_UNKNOWN = 2751,
+
/**
- * The backend knows the instance that was supposed to support the tip, and it was configured for tipping. However, the funds remaining are insufficient to cover the tip, and the merchant should top up the reserve.
+ * The backend knows the instance that was supposed to support the reward, and it was configured for rewardping. However, the funds remaining are insufficient to cover the reward, and the merchant should top up the reserve.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
* (A value of 0 indicates that the error is generated client-side).
*/
- MERCHANT_PRIVATE_POST_TIP_AUTHORIZE_INSUFFICIENT_FUNDS = 2752,
+ MERCHANT_PRIVATE_POST_REWARD_AUTHORIZE_INSUFFICIENT_FUNDS = 2752,
+
/**
- * The backend failed to find a reserve needed to authorize the tip.
+ * The backend failed to find a reserve needed to authorize the reward.
* Returned with an HTTP status code of #MHD_HTTP_SERVICE_UNAVAILABLE (503).
* (A value of 0 indicates that the error is generated client-side).
*/
- MERCHANT_PRIVATE_POST_TIP_AUTHORIZE_RESERVE_NOT_FOUND = 2753,
+ MERCHANT_PRIVATE_POST_REWARD_AUTHORIZE_RESERVE_NOT_FOUND = 2753,
+
/**
* The merchant backend encountered a failure in computing the deposit total.
@@ -2297,6 +3167,71 @@ export enum TalerErrorCode {
*/
MERCHANT_PRIVATE_GET_ORDERS_ID_AMOUNT_ARITHMETIC_FAILURE = 2800,
+
+ /**
+ * The template ID already exists.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_POST_TEMPLATES_CONFLICT_TEMPLATE_EXISTS = 2850,
+
+
+ /**
+ * The OTP device ID already exists.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_POST_OTP_DEVICES_CONFLICT_OTP_DEVICE_EXISTS = 2851,
+
+
+ /**
+ * Amount given in the using template and in the template contract. There is a conflict.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_USING_TEMPLATES_AMOUNT_CONFLICT_TEMPLATES_CONTRACT_AMOUNT = 2860,
+
+
+ /**
+ * Subject given in the using template and in the template contract. There is a conflict.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_USING_TEMPLATES_SUMMARY_CONFLICT_TEMPLATES_CONTRACT_SUBJECT = 2861,
+
+
+ /**
+ * Amount not given in the using template and in the template contract. There is a conflict.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_USING_TEMPLATES_NO_AMOUNT = 2862,
+
+
+ /**
+ * Subject not given in the using template and in the template contract. There is a conflict.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_USING_TEMPLATES_NO_SUMMARY = 2863,
+
+
+ /**
+ * The webhook ID elready exists.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_POST_WEBHOOKS_CONFLICT_WEBHOOK_EXISTS = 2900,
+
+
+ /**
+ * The webhook serial elready exists.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_POST_PENDING_WEBHOOKS_CONFLICT_PENDING_WEBHOOK_EXISTS = 2910,
+
+
/**
* The signature from the exchange on the deposit confirmation is invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -2304,6 +3239,7 @@ export enum TalerErrorCode {
*/
AUDITOR_DEPOSIT_CONFIRMATION_SIGNATURE_INVALID = 3100,
+
/**
* The exchange key used for the signature on the deposit confirmation was revoked.
* Returned with an HTTP status code of #MHD_HTTP_GONE (410).
@@ -2311,6 +3247,23 @@ export enum TalerErrorCode {
*/
AUDITOR_EXCHANGE_SIGNING_KEY_REVOKED = 3101,
+
+ /**
+ * The requested resource could not be found.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ AUDITOR_RESOURCE_NOT_FOUND = 3102,
+
+
+ /**
+ * The URI is missing a path component.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ AUDITOR_URI_MISSING_PATH_COMPONENT = 3103,
+
+
/**
* Wire transfer attempted with credit and debit party being the same bank account.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2318,6 +3271,7 @@ export enum TalerErrorCode {
*/
BANK_SAME_ACCOUNT = 5101,
+
/**
* Wire transfer impossible, due to financial limitation of the party that attempted the payment.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2325,6 +3279,7 @@ export enum TalerErrorCode {
*/
BANK_UNALLOWED_DEBIT = 5102,
+
/**
* Negative numbers are not allowed (as value and/or fraction) to instantiate an amount object.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2332,6 +3287,7 @@ export enum TalerErrorCode {
*/
BANK_NEGATIVE_NUMBER_AMOUNT = 5103,
+
/**
* A too big number was used (as value and/or fraction) to instantiate an amount object.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2339,12 +3295,6 @@ export enum TalerErrorCode {
*/
BANK_NUMBER_TOO_BIG = 5104,
- /**
- * Could not login for the requested operation.
- * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
- * (A value of 0 indicates that the error is generated client-side).
- */
- BANK_LOGIN_FAILED = 5105,
/**
* The bank account referenced in the requested operation was not found.
@@ -2353,6 +3303,7 @@ export enum TalerErrorCode {
*/
BANK_UNKNOWN_ACCOUNT = 5106,
+
/**
* The transaction referenced in the requested operation (typically a reject operation), was not found.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -2360,6 +3311,7 @@ export enum TalerErrorCode {
*/
BANK_TRANSACTION_NOT_FOUND = 5107,
+
/**
* Bank received a malformed amount string.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2367,6 +3319,7 @@ export enum TalerErrorCode {
*/
BANK_BAD_FORMAT_AMOUNT = 5108,
+
/**
* The client does not own the account credited by the transaction which is to be rejected, so it has no rights do reject it.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -2374,6 +3327,7 @@ export enum TalerErrorCode {
*/
BANK_REJECT_NO_RIGHTS = 5109,
+
/**
* This error code is returned when no known exception types captured the exception.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -2381,6 +3335,7 @@ export enum TalerErrorCode {
*/
BANK_UNMANAGED_EXCEPTION = 5110,
+
/**
* This error code is used for all those exceptions that do not really need a specific error code to return to the client. Used for example when a client is trying to register with a unavailable username.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -2388,6 +3343,7 @@ export enum TalerErrorCode {
*/
BANK_SOFT_EXCEPTION = 5111,
+
/**
* The request UID for a request to transfer funds has already been used, but with different details for the transfer.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2395,6 +3351,7 @@ export enum TalerErrorCode {
*/
BANK_TRANSFER_REQUEST_UID_REUSED = 5112,
+
/**
* The withdrawal operation already has a reserve selected. The current request conflicts with the existing selection.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2402,6 +3359,7 @@ export enum TalerErrorCode {
*/
BANK_WITHDRAWAL_OPERATION_RESERVE_SELECTION_CONFLICT = 5113,
+
/**
* The wire transfer subject duplicates an existing reserve public key. But wire transfer subjects must be unique.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2409,6 +3367,7 @@ export enum TalerErrorCode {
*/
BANK_DUPLICATE_RESERVE_PUB_SUBJECT = 5114,
+
/**
* The client requested a transaction that is so far in the past, that it has been forgotten by the bank.
* Returned with an HTTP status code of #MHD_HTTP_GONE (410).
@@ -2416,6 +3375,7 @@ export enum TalerErrorCode {
*/
BANK_ANCIENT_TRANSACTION_GONE = 5115,
+
/**
* The client attempted to abort a transaction that was already confirmed.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2423,6 +3383,7 @@ export enum TalerErrorCode {
*/
BANK_ABORT_CONFIRM_CONFLICT = 5116,
+
/**
* The client attempted to confirm a transaction that was already aborted.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2430,6 +3391,7 @@ export enum TalerErrorCode {
*/
BANK_CONFIRM_ABORT_CONFLICT = 5117,
+
/**
* The client attempted to register an account with the same name.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2437,6 +3399,7 @@ export enum TalerErrorCode {
*/
BANK_REGISTER_CONFLICT = 5118,
+
/**
* The client attempted to confirm a withdrawal operation before the wallet posted the required details.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2444,6 +3407,231 @@ export enum TalerErrorCode {
*/
BANK_POST_WITHDRAWAL_OPERATION_REQUIRED = 5119,
+
+ /**
+ * The client tried to register a new account under a reserved username (like 'admin' for example).
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_RESERVED_USERNAME_CONFLICT = 5120,
+
+
+ /**
+ * The client tried to register a new account with an username already in use.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_REGISTER_USERNAME_REUSE = 5121,
+
+
+ /**
+ * The client tried to register a new account with a payto:// URI already in use.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_REGISTER_PAYTO_URI_REUSE = 5122,
+
+
+ /**
+ * The client tried to delete an account with a non null balance.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_ACCOUNT_BALANCE_NOT_ZERO = 5123,
+
+
+ /**
+ * The client tried to create a transaction or an operation that credit an unknown account.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_UNKNOWN_CREDITOR = 5124,
+
+
+ /**
+ * The client tried to create a transaction or an operation that debit an unknown account.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_UNKNOWN_DEBTOR = 5125,
+
+
+ /**
+ * The client tried to perform an action prohibited for exchange accounts.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_ACCOUNT_IS_EXCHANGE = 5126,
+
+
+ /**
+ * The client tried to perform an action reserved for exchange accounts.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_ACCOUNT_IS_NOT_EXCHANGE = 5127,
+
+
+ /**
+ * Received currency conversion is wrong.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_BAD_CONVERSION = 5128,
+
+
+ /**
+ * The account referenced in this operation is missing tan info for the chosen channel.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_MISSING_TAN_INFO = 5129,
+
+
+ /**
+ * The client attempted to confirm a transaction with incomplete info.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_CONFIRM_INCOMPLETE = 5130,
+
+
+ /**
+ * The request rate is too high. The server is refusing requests to guard against brute-force attacks.
+ * Returned with an HTTP status code of #MHD_HTTP_TOO_MANY_REQUESTS (429).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_TAN_RATE_LIMITED = 5131,
+
+
+ /**
+ * This TAN channel is not supported.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_IMPLEMENTED (501).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_TAN_CHANNEL_NOT_SUPPORTED = 5132,
+
+
+ /**
+ * Failed to send TAN using the helper script. Either script is not found, or script timeout, or script terminated with a non-successful result.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_TAN_CHANNEL_SCRIPT_FAILED = 5133,
+
+
+ /**
+ * The client's response to the challenge was invalid.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_TAN_CHALLENGE_FAILED = 5134,
+
+
+ /**
+ * A non-admin user has tried to change their legal name.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_NON_ADMIN_PATCH_LEGAL_NAME = 5135,
+
+
+ /**
+ * A non-admin user has tried to change their debt limit.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_NON_ADMIN_PATCH_DEBT_LIMIT = 5136,
+
+
+ /**
+ * A non-admin user has tried to change their password whihout providing the current one.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_NON_ADMIN_PATCH_MISSING_OLD_PASSWORD = 5137,
+
+
+ /**
+ * Provided old password does not match current password.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_PATCH_BAD_OLD_PASSWORD = 5138,
+
+
+ /**
+ * An admin user has tried to become an exchange.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_PATCH_ADMIN_EXCHANGE = 5139,
+
+
+ /**
+ * A non-admin user has tried to change their cashout account.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_NON_ADMIN_PATCH_CASHOUT = 5140,
+
+
+ /**
+ * A non-admin user has tried to change their contact info.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_NON_ADMIN_PATCH_CONTACT = 5141,
+
+
+ /**
+ * The client tried to create a transaction that credit the admin account.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_ADMIN_CREDITOR = 5142,
+
+
+ /**
+ * The referenced challenge was not found.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_CHALLENGE_NOT_FOUND = 5143,
+
+
+ /**
+ * The referenced challenge has expired.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_TAN_CHALLENGE_EXPIRED = 5144,
+
+
+ /**
+ * A non-admin user has tried to create an account with 2fa.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_NON_ADMIN_SET_TAN_CHANNEL = 5145,
+
+
+ /**
+ * A non-admin user has tried to set their minimum cashout amount.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_NON_ADMIN_SET_MIN_CASHOUT = 5146,
+
+
+ /**
+ * Amount of currency conversion it less than the minimum allowed.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_CONVERSION_AMOUNT_TO_SMALL = 5147,
+
+
/**
* The sync service failed find the account in its database.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -2451,6 +3639,7 @@ export enum TalerErrorCode {
*/
SYNC_ACCOUNT_UNKNOWN = 6100,
+
/**
* The SHA-512 hash provided in the If-None-Match header is malformed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2458,6 +3647,7 @@ export enum TalerErrorCode {
*/
SYNC_BAD_IF_NONE_MATCH = 6101,
+
/**
* The SHA-512 hash provided in the If-Match header is malformed or missing.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2465,6 +3655,7 @@ export enum TalerErrorCode {
*/
SYNC_BAD_IF_MATCH = 6102,
+
/**
* The signature provided in the "Sync-Signature" header is malformed or missing.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2472,6 +3663,7 @@ export enum TalerErrorCode {
*/
SYNC_BAD_SYNC_SIGNATURE = 6103,
+
/**
* The signature provided in the "Sync-Signature" header does not match the account, old or new Etags.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -2479,6 +3671,7 @@ export enum TalerErrorCode {
*/
SYNC_INVALID_SIGNATURE = 6104,
+
/**
* The "Content-length" field for the upload is not a number.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2486,20 +3679,23 @@ export enum TalerErrorCode {
*/
SYNC_MALFORMED_CONTENT_LENGTH = 6105,
+
/**
* The "Content-length" field for the upload is too big based on the server's terms of service.
- * Returned with an HTTP status code of #MHD_HTTP_PAYLOAD_TOO_LARGE (413).
+ * Returned with an HTTP status code of #MHD_HTTP_CONTENT_TOO_LARGE (413).
* (A value of 0 indicates that the error is generated client-side).
*/
SYNC_EXCESSIVE_CONTENT_LENGTH = 6106,
+
/**
* The server is out of memory to handle the upload. Trying again later may succeed.
- * Returned with an HTTP status code of #MHD_HTTP_PAYLOAD_TOO_LARGE (413).
+ * Returned with an HTTP status code of #MHD_HTTP_CONTENT_TOO_LARGE (413).
* (A value of 0 indicates that the error is generated client-side).
*/
SYNC_OUT_OF_MEMORY_ON_CONTENT_LENGTH = 6107,
+
/**
* The uploaded data does not match the Etag.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2507,6 +3703,7 @@ export enum TalerErrorCode {
*/
SYNC_INVALID_UPLOAD = 6108,
+
/**
* HTTP server experienced a timeout while awaiting promised payment.
* Returned with an HTTP status code of #MHD_HTTP_REQUEST_TIMEOUT (408).
@@ -2514,6 +3711,7 @@ export enum TalerErrorCode {
*/
SYNC_PAYMENT_GENERIC_TIMEOUT = 6109,
+
/**
* Sync could not setup the payment request with its own backend.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -2521,6 +3719,7 @@ export enum TalerErrorCode {
*/
SYNC_PAYMENT_CREATE_BACKEND_ERROR = 6110,
+
/**
* The sync service failed find the backup to be updated in its database.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -2528,6 +3727,7 @@ export enum TalerErrorCode {
*/
SYNC_PREVIOUS_BACKUP_UNKNOWN = 6111,
+
/**
* The "Content-length" field for the upload is missing.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2535,6 +3735,7 @@ export enum TalerErrorCode {
*/
SYNC_MISSING_CONTENT_LENGTH = 6112,
+
/**
* Sync had problems communicating with its payment backend.
* Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
@@ -2542,6 +3743,7 @@ export enum TalerErrorCode {
*/
SYNC_GENERIC_BACKEND_ERROR = 6113,
+
/**
* Sync experienced a timeout communicating with its payment backend.
* Returned with an HTTP status code of #MHD_HTTP_GATEWAY_TIMEOUT (504).
@@ -2549,6 +3751,7 @@ export enum TalerErrorCode {
*/
SYNC_GENERIC_BACKEND_TIMEOUT = 6114,
+
/**
* The wallet does not implement a version of the exchange protocol that is compatible with the protocol version of the exchange.
* Returned with an HTTP status code of #MHD_HTTP_NOT_IMPLEMENTED (501).
@@ -2556,6 +3759,7 @@ export enum TalerErrorCode {
*/
WALLET_EXCHANGE_PROTOCOL_VERSION_INCOMPATIBLE = 7000,
+
/**
* The wallet encountered an unexpected exception. This is likely a bug in the wallet implementation.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -2563,6 +3767,7 @@ export enum TalerErrorCode {
*/
WALLET_UNEXPECTED_EXCEPTION = 7001,
+
/**
* The wallet received a response from a server, but the response can't be parsed.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -2570,6 +3775,7 @@ export enum TalerErrorCode {
*/
WALLET_RECEIVED_MALFORMED_RESPONSE = 7002,
+
/**
* The wallet tried to make a network request, but it received no response.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -2577,6 +3783,7 @@ export enum TalerErrorCode {
*/
WALLET_NETWORK_ERROR = 7003,
+
/**
* The wallet tried to make a network request, but it was throttled.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -2584,6 +3791,7 @@ export enum TalerErrorCode {
*/
WALLET_HTTP_REQUEST_THROTTLED = 7004,
+
/**
* The wallet made a request to a service, but received an error response it does not know how to handle.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -2591,6 +3799,7 @@ export enum TalerErrorCode {
*/
WALLET_UNEXPECTED_REQUEST_ERROR = 7005,
+
/**
* The denominations offered by the exchange are insufficient. Likely the exchange is badly configured or not maintained.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -2598,6 +3807,7 @@ export enum TalerErrorCode {
*/
WALLET_EXCHANGE_DENOMINATIONS_INSUFFICIENT = 7006,
+
/**
* The wallet does not support the operation requested by a client.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -2605,6 +3815,7 @@ export enum TalerErrorCode {
*/
WALLET_CORE_API_OPERATION_UNKNOWN = 7007,
+
/**
* The given taler://pay URI is invalid.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -2612,6 +3823,7 @@ export enum TalerErrorCode {
*/
WALLET_INVALID_TALER_PAY_URI = 7008,
+
/**
* The signature on a coin by the exchange's denomination key is invalid after unblinding it.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -2619,6 +3831,7 @@ export enum TalerErrorCode {
*/
WALLET_EXCHANGE_COIN_SIGNATURE_INVALID = 7009,
+
/**
* The exchange does not know about the reserve (yet), and thus withdrawal can't progress.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -2626,6 +3839,7 @@ export enum TalerErrorCode {
*/
WALLET_EXCHANGE_WITHDRAW_RESERVE_UNKNOWN_AT_EXCHANGE = 7010,
+
/**
* The wallet core service is not available.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -2633,6 +3847,7 @@ export enum TalerErrorCode {
*/
WALLET_CORE_NOT_AVAILABLE = 7011,
+
/**
* The bank has aborted a withdrawal operation, and thus a withdrawal can't complete.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -2640,6 +3855,7 @@ export enum TalerErrorCode {
*/
WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK = 7012,
+
/**
* An HTTP request made by the wallet timed out.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -2647,6 +3863,7 @@ export enum TalerErrorCode {
*/
WALLET_HTTP_REQUEST_GENERIC_TIMEOUT = 7013,
+
/**
* The order has already been claimed by another wallet.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -2654,6 +3871,7 @@ export enum TalerErrorCode {
*/
WALLET_ORDER_ALREADY_CLAIMED = 7014,
+
/**
* A group of withdrawal operations (typically for the same reserve at the same exchange) has errors and will be tried again later.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -2661,12 +3879,14 @@ export enum TalerErrorCode {
*/
WALLET_WITHDRAWAL_GROUP_INCOMPLETE = 7015,
+
/**
- * The signature on a coin by the exchange's denomination key (obtained through the merchant via tipping) is invalid after unblinding it.
+ * The signature on a coin by the exchange's denomination key (obtained through the merchant via a reward) is invalid after unblinding it.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
* (A value of 0 indicates that the error is generated client-side).
*/
- WALLET_TIPPING_COIN_SIGNATURE_INVALID = 7016,
+ WALLET_REWARD_COIN_SIGNATURE_INVALID = 7016,
+
/**
* The wallet does not implement a version of the bank integration API that is compatible with the version offered by the bank.
@@ -2675,6 +3895,7 @@ export enum TalerErrorCode {
*/
WALLET_BANK_INTEGRATION_PROTOCOL_VERSION_INCOMPATIBLE = 7017,
+
/**
* The wallet processed a taler://pay URI, but the merchant base URL in the downloaded contract terms does not match the merchant base URL derived from the URI.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -2682,6 +3903,7 @@ export enum TalerErrorCode {
*/
WALLET_CONTRACT_TERMS_BASE_URL_MISMATCH = 7018,
+
/**
* The merchant's signature on the contract terms is invalid.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -2689,6 +3911,7 @@ export enum TalerErrorCode {
*/
WALLET_CONTRACT_TERMS_SIGNATURE_INVALID = 7019,
+
/**
* The contract terms given by the merchant are malformed.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -2696,6 +3919,7 @@ export enum TalerErrorCode {
*/
WALLET_CONTRACT_TERMS_MALFORMED = 7020,
+
/**
* A pending operation failed, and thus the request can't be completed.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -2703,6 +3927,7 @@ export enum TalerErrorCode {
*/
WALLET_PENDING_OPERATION_FAILED = 7021,
+
/**
* A payment was attempted, but the merchant had an internal server error (5xx).
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -2710,6 +3935,7 @@ export enum TalerErrorCode {
*/
WALLET_PAY_MERCHANT_SERVER_ERROR = 7022,
+
/**
* The crypto worker failed.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -2717,6 +3943,7 @@ export enum TalerErrorCode {
*/
WALLET_CRYPTO_WORKER_ERROR = 7023,
+
/**
* The crypto worker received a bad request.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -2724,6 +3951,7 @@ export enum TalerErrorCode {
*/
WALLET_CRYPTO_WORKER_BAD_REQUEST = 7024,
+
/**
* A KYC step is required before withdrawal can proceed.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -2731,6 +3959,95 @@ export enum TalerErrorCode {
*/
WALLET_WITHDRAWAL_KYC_REQUIRED = 7025,
+
+ /**
+ * The wallet does not have sufficient balance to create a deposit group.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE = 7026,
+
+
+ /**
+ * The wallet does not have sufficient balance to create a peer push payment.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_PEER_PUSH_PAYMENT_INSUFFICIENT_BALANCE = 7027,
+
+
+ /**
+ * The wallet does not have sufficient balance to pay for an invoice.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_PEER_PULL_PAYMENT_INSUFFICIENT_BALANCE = 7028,
+
+
+ /**
+ * A group of refresh operations has errors and will be tried again later.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_REFRESH_GROUP_INCOMPLETE = 7029,
+
+
+ /**
+ * The exchange's self-reported base URL does not match the one that the wallet is using.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_EXCHANGE_BASE_URL_MISMATCH = 7030,
+
+
+ /**
+ * The order has already been paid by another wallet.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_ORDER_ALREADY_PAID = 7031,
+
+
+ /**
+ * An exchange that is required for some request is currently not available.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_EXCHANGE_UNAVAILABLE = 7032,
+
+
+ /**
+ * An exchange entry is still used by the exchange, thus it can't be deleted without purging.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_EXCHANGE_ENTRY_USED = 7033,
+
+
+ /**
+ * The wallet database is unavailable and the wallet thus is not operational.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_DB_UNAVAILABLE = 7034,
+
+
+ /**
+ * A taler:// URI is malformed and can't be parsed.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_TALER_URI_MALFORMED = 7035,
+
+
+ /**
+ * A wallet-core request was cancelled and thus can't provide a response.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_CORE_REQUEST_CANCELLED = 7036,
+
+
/**
* We encountered a timeout with our payment backend.
* Returned with an HTTP status code of #MHD_HTTP_GATEWAY_TIMEOUT (504).
@@ -2738,6 +4055,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_GENERIC_BACKEND_TIMEOUT = 8000,
+
/**
* The backend requested payment, but the request is malformed.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -2745,6 +4063,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_GENERIC_INVALID_PAYMENT_REQUEST = 8001,
+
/**
* The backend got an unexpected reply from the payment processor.
* Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
@@ -2752,6 +4071,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_GENERIC_BACKEND_ERROR = 8002,
+
/**
* The "Content-length" field for the upload is missing.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2759,6 +4079,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_GENERIC_MISSING_CONTENT_LENGTH = 8003,
+
/**
* The "Content-length" field for the upload is malformed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2766,6 +4087,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_GENERIC_MALFORMED_CONTENT_LENGTH = 8004,
+
/**
* The backend failed to setup an order with the payment processor.
* Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
@@ -2773,6 +4095,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_GENERIC_ORDER_CREATE_BACKEND_ERROR = 8005,
+
/**
* The backend was not authorized to check for payment with the payment processor.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -2780,6 +4103,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_GENERIC_PAYMENT_CHECK_UNAUTHORIZED = 8006,
+
/**
* The backend could not check payment status with the payment processor.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -2787,6 +4111,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_GENERIC_PAYMENT_CHECK_START_FAILED = 8007,
+
/**
* The Anastasis provider could not be reached.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -2794,6 +4119,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_GENERIC_PROVIDER_UNREACHABLE = 8008,
+
/**
* HTTP server experienced a timeout while awaiting promised payment.
* Returned with an HTTP status code of #MHD_HTTP_REQUEST_TIMEOUT (408).
@@ -2801,6 +4127,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_PAYMENT_GENERIC_TIMEOUT = 8009,
+
/**
* The key share is unknown to the provider.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -2808,6 +4135,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_TRUTH_UNKNOWN = 8108,
+
/**
* The authorization method used for the key share is no longer supported by the provider.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -2815,6 +4143,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_TRUTH_AUTHORIZATION_METHOD_NO_LONGER_SUPPORTED = 8109,
+
/**
* The client needs to respond to the challenge.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -2822,6 +4151,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_TRUTH_CHALLENGE_RESPONSE_REQUIRED = 8110,
+
/**
* The client's response to the challenge was invalid.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -2829,6 +4159,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_TRUTH_CHALLENGE_FAILED = 8111,
+
/**
* The backend is not aware of having issued the provided challenge code. Either this is the wrong code, or it has expired.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -2836,6 +4167,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_TRUTH_CHALLENGE_UNKNOWN = 8112,
+
/**
* The backend failed to initiate the authorization process.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -2843,6 +4175,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_TRUTH_AUTHORIZATION_START_FAILED = 8114,
+
/**
* The authorization succeeded, but the key share is no longer available.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -2850,6 +4183,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_TRUTH_KEY_SHARE_GONE = 8115,
+
/**
* The backend forgot the order we asked the client to pay for
* Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
@@ -2857,6 +4191,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_TRUTH_ORDER_DISAPPEARED = 8116,
+
/**
* The backend itself reported a bad exchange interaction.
* Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
@@ -2864,6 +4199,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_TRUTH_BACKEND_EXCHANGE_BAD = 8117,
+
/**
* The backend reported a payment status we did not expect.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -2871,6 +4207,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_TRUTH_UNEXPECTED_PAYMENT_STATUS = 8118,
+
/**
* The backend failed to setup the order for payment.
* Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
@@ -2878,6 +4215,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_TRUTH_PAYMENT_CREATE_BACKEND_ERROR = 8119,
+
/**
* The decryption of the key share failed with the provided key.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2885,6 +4223,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_TRUTH_DECRYPTION_FAILED = 8120,
+
/**
* The request rate is too high. The server is refusing requests to guard against brute-force attacks.
* Returned with an HTTP status code of #MHD_HTTP_TOO_MANY_REQUESTS (429).
@@ -2892,6 +4231,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_TRUTH_RATE_LIMITED = 8121,
+
/**
* A request to issue a challenge is not valid for this authentication method.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2899,6 +4239,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_TRUTH_CHALLENGE_WRONG_METHOD = 8123,
+
/**
* The backend failed to store the key share because the UUID is already in use.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2906,6 +4247,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_TRUTH_UPLOAD_UUID_EXISTS = 8150,
+
/**
* The backend failed to store the key share because the authorization method is not supported.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -2913,6 +4255,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_TRUTH_UPLOAD_METHOD_NOT_SUPPORTED = 8151,
+
/**
* The provided phone number is not an acceptable number.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2920,6 +4263,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_SMS_PHONE_INVALID = 8200,
+
/**
* Failed to run the SMS transmission helper process.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -2927,6 +4271,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_SMS_HELPER_EXEC_FAILED = 8201,
+
/**
* Provider failed to send SMS. Helper terminated with a non-successful result.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -2934,6 +4279,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_SMS_HELPER_COMMAND_FAILED = 8202,
+
/**
* The provided email address is not an acceptable address.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2941,6 +4287,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_EMAIL_INVALID = 8210,
+
/**
* Failed to run the E-mail transmission helper process.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -2948,6 +4295,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_EMAIL_HELPER_EXEC_FAILED = 8211,
+
/**
* Provider failed to send E-mail. Helper terminated with a non-successful result.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -2955,6 +4303,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_EMAIL_HELPER_COMMAND_FAILED = 8212,
+
/**
* The provided postal address is not an acceptable address.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2962,6 +4311,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_POST_INVALID = 8220,
+
/**
* Failed to run the mail transmission helper process.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -2969,6 +4319,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_POST_HELPER_EXEC_FAILED = 8221,
+
/**
* Provider failed to send mail. Helper terminated with a non-successful result.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -2976,6 +4327,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_POST_HELPER_COMMAND_FAILED = 8222,
+
/**
* The provided IBAN address is not an acceptable IBAN.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2983,6 +4335,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_IBAN_INVALID = 8230,
+
/**
* The provider has not yet received the IBAN wire transfer authorizing the disclosure of the key share.
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
@@ -2990,6 +4343,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_IBAN_MISSING_TRANSFER = 8231,
+
/**
* The backend did not find a TOTP key in the data provided.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -2997,6 +4351,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_TOTP_KEY_MISSING = 8240,
+
/**
* The key provided does not satisfy the format restrictions for an Anastasis TOTP key.
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
@@ -3004,6 +4359,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_TOTP_KEY_INVALID = 8241,
+
/**
* The given if-none-match header is malformed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -3011,13 +4367,15 @@ export enum TalerErrorCode {
*/
ANASTASIS_POLICY_BAD_IF_NONE_MATCH = 8301,
+
/**
* The server is out of memory to handle the upload. Trying again later may succeed.
- * Returned with an HTTP status code of #MHD_HTTP_PAYLOAD_TOO_LARGE (413).
+ * Returned with an HTTP status code of #MHD_HTTP_CONTENT_TOO_LARGE (413).
* (A value of 0 indicates that the error is generated client-side).
*/
ANASTASIS_POLICY_OUT_OF_MEMORY_ON_CONTENT_LENGTH = 8304,
+
/**
* The signature provided in the "Anastasis-Policy-Signature" header is malformed or missing.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -3025,6 +4383,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_POLICY_BAD_SIGNATURE = 8305,
+
/**
* The given if-match header is malformed.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -3032,6 +4391,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_POLICY_BAD_IF_MATCH = 8306,
+
/**
* The uploaded data does not match the Etag.
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
@@ -3039,6 +4399,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_POLICY_INVALID_UPLOAD = 8307,
+
/**
* The provider is unaware of the requested policy.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -3046,6 +4407,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_POLICY_NOT_FOUND = 8350,
+
/**
* The given action is invalid for the current state of the reducer.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3053,6 +4415,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_ACTION_INVALID = 8400,
+
/**
* The given state of the reducer is invalid.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3060,6 +4423,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_STATE_INVALID = 8401,
+
/**
* The given input to the reducer is invalid.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3067,6 +4431,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_INPUT_INVALID = 8402,
+
/**
* The selected authentication method does not work for the Anastasis provider.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3074,6 +4439,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_AUTHENTICATION_METHOD_NOT_SUPPORTED = 8403,
+
/**
* The given input and action do not work for the current state.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3081,6 +4447,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_INPUT_INVALID_FOR_STATE = 8404,
+
/**
* We experienced an unexpected failure interacting with the backend.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3088,6 +4455,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_BACKEND_FAILURE = 8405,
+
/**
* The contents of a resource file did not match our expectations.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3095,6 +4463,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_RESOURCE_MALFORMED = 8406,
+
/**
* A required resource file is missing.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3102,6 +4471,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_RESOURCE_MISSING = 8407,
+
/**
* An input did not match the regular expression.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3109,6 +4479,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_INPUT_REGEX_FAILED = 8408,
+
/**
* An input did not match the custom validation logic.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3116,6 +4487,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_INPUT_VALIDATION_FAILED = 8409,
+
/**
* Our attempts to download the recovery document failed with all providers. Most likely the personal information you entered differs from the information you provided during the backup process and you should go back to the previous step. Alternatively, if you used a backup provider that is unknown to this application, you should add that provider manually.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3123,6 +4495,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_POLICY_LOOKUP_FAILED = 8410,
+
/**
* Anastasis provider reported a fatal failure.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3130,6 +4503,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_BACKUP_PROVIDER_FAILED = 8411,
+
/**
* Anastasis provider failed to respond to the configuration request.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3137,6 +4511,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_PROVIDER_CONFIG_FAILED = 8412,
+
/**
* The policy we downloaded is malformed. Must have been a client error while creating the backup.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3144,6 +4519,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_POLICY_MALFORMED = 8413,
+
/**
* We failed to obtain the policy, likely due to a network issue.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3151,6 +4527,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_NETWORK_FAILED = 8414,
+
/**
* The recovered secret did not match the required syntax.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3158,6 +4535,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_SECRET_MALFORMED = 8415,
+
/**
* The challenge data provided is too large for the available providers.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3165,6 +4543,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_CHALLENGE_DATA_TOO_BIG = 8416,
+
/**
* The provided core secret is too large for some of the providers.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3172,6 +4551,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_SECRET_TOO_BIG = 8417,
+
/**
* The provider returned in invalid configuration.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3179,6 +4559,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_PROVIDER_INVALID_CONFIG = 8418,
+
/**
* The reducer encountered an internal error, likely a bug that needs to be reported.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3186,6 +4567,7 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_INTERNAL_ERROR = 8419,
+
/**
* The reducer already synchronized with all providers.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3193,6 +4575,39 @@ export enum TalerErrorCode {
*/
ANASTASIS_REDUCER_PROVIDERS_ALREADY_SYNCED = 8420,
+
+ /**
+ * The Donau failed to perform the operation as it could not find the private keys. This is a problem with the Donau setup, not with the client's request.
+ * Returned with an HTTP status code of #MHD_HTTP_SERVICE_UNAVAILABLE (503).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DONAU_GENERIC_KEYS_MISSING = 8607,
+
+
+ /**
+ * The signature of the charity key is not valid.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DONAU_CHARITY_SIGNATURE_INVALID = 8608,
+
+
+ /**
+ * The charity is unknown.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DONAU_CHARITY_NOT_FOUND = 8609,
+
+
+ /**
+ * The donation amount specified in the request exceeds the limit of the charity.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DONAU_EXCEEDING_DONATION_LIMIT = 8610,
+
+
/**
* A generic error happened in the LibEuFin nexus. See the enclose details JSON for more information.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3200,6 +4615,7 @@ export enum TalerErrorCode {
*/
LIBEUFIN_NEXUS_GENERIC_ERROR = 9000,
+
/**
* An uncaught exception happened in the LibEuFin nexus service.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -3207,6 +4623,7 @@ export enum TalerErrorCode {
*/
LIBEUFIN_NEXUS_UNCAUGHT_EXCEPTION = 9001,
+
/**
* A generic error happened in the LibEuFin sandbox. See the enclose details JSON for more information.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
@@ -3214,6 +4631,7 @@ export enum TalerErrorCode {
*/
LIBEUFIN_SANDBOX_GENERIC_ERROR = 9500,
+
/**
* An uncaught exception happened in the LibEuFin sandbox service.
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
@@ -3221,6 +4639,7 @@ export enum TalerErrorCode {
*/
LIBEUFIN_SANDBOX_UNCAUGHT_EXCEPTION = 9501,
+
/**
* This validation method is not supported by the service.
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
@@ -3228,6 +4647,7 @@ export enum TalerErrorCode {
*/
TALDIR_METHOD_NOT_SUPPORTED = 9600,
+
/**
* Number of allowed attempts for initiating a challenge exceeded.
* Returned with an HTTP status code of #MHD_HTTP_TOO_MANY_REQUESTS (429).
@@ -3235,10 +4655,93 @@ export enum TalerErrorCode {
*/
TALDIR_REGISTER_RATE_LIMITED = 9601,
+
+ /**
+ * The client is unknown or unauthorized.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ CHALLENGER_GENERIC_CLIENT_UNKNOWN = 9750,
+
+
+ /**
+ * The client is not authorized to use the given redirect URI.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ CHALLENGER_GENERIC_CLIENT_FORBIDDEN_BAD_REDIRECT_URI = 9751,
+
+
+ /**
+ * The service failed to execute its helper process to send the challenge.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ CHALLENGER_HELPER_EXEC_FAILED = 9752,
+
+
+ /**
+ * The grant is unknown to the service (it could also have expired).
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ CHALLENGER_GRANT_UNKNOWN = 9753,
+
+
+ /**
+ * The code given is not even well-formed.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ CHALLENGER_CLIENT_FORBIDDEN_BAD_CODE = 9754,
+
+
+ /**
+ * The service is not aware of the referenced validation process.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ CHALLENGER_GENERIC_VALIDATION_UNKNOWN = 9755,
+
+
+ /**
+ * The code given is not valid.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ CHALLENGER_CLIENT_FORBIDDEN_INVALID_CODE = 9756,
+
+
+ /**
+ * Too many attempts have been made, validation is temporarily disabled for this address.
+ * Returned with an HTTP status code of #MHD_HTTP_TOO_MANY_REQUESTS (429).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ CHALLENGER_TOO_MANY_ATTEMPTS = 9757,
+
+
+ /**
+ * The PIN code provided is incorrect.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ CHALLENGER_INVALID_PIN = 9758,
+
+
+ /**
+ * The token cannot be valid as no address was ever provided by the client.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ CHALLENGER_MISSING_ADDRESS = 9759,
+
+
/**
* End of error code range.
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
* (A value of 0 indicates that the error is generated client-side).
*/
END = 9999,
+
+
}
diff --git a/packages/taler-util/src/taler-types.ts b/packages/taler-util/src/taler-types.ts
index 292ace94b..e2536b74a 100644
--- a/packages/taler-util/src/taler-types.ts
+++ b/packages/taler-util/src/taler-types.ts
@@ -25,29 +25,34 @@
* Imports.
*/
-import { codecForAmountString } from "./amounts.js";
+import { Amounts, codecForAmountString } from "./amounts.js";
import {
+ Codec,
buildCodecForObject,
buildCodecForUnion,
- Codec,
codecForAny,
codecForBoolean,
- codecForConstNumber,
codecForConstString,
codecForList,
codecForMap,
codecForNumber,
codecForString,
+ codecForStringURL,
codecOptional,
} from "./codec.js";
import { strcmp } from "./helpers.js";
-import { AgeCommitmentProof, Edx25519PublicKeyEnc } from "./taler-crypto.js";
import {
- codecForAbsoluteTime,
- codecForDuration,
- codecForTimestamp,
+ CurrencySpecification,
+ codecForCurrencySpecificiation,
+ codecForEither,
+ codecForProduct,
+} from "./index.js";
+import { Edx25519PublicKeyEnc } from "./taler-crypto.js";
+import {
TalerProtocolDuration,
TalerProtocolTimestamp,
+ codecForDuration,
+ codecForTimestamp,
} from "./time.js";
/**
@@ -298,15 +303,11 @@ export interface CoinDepositPermission {
* merchant's contract terms.
*/
export interface ExchangeHandle {
- /**
- * Master public signing key of the exchange.
- */
- master_pub: string;
-
- /**
- * Base URL of the exchange.
- */
+ // The exchange's base URL.
url: string;
+
+ // Master public key of the exchange.
+ master_pub: EddsaPublicKeyString;
}
export interface AuditorHandle {
@@ -318,7 +319,7 @@ export interface AuditorHandle {
/**
* Master public signing key of the auditor.
*/
- auditor_pub: string;
+ auditor_pub: EddsaPublicKeyString;
/**
* Base URL of the auditor.
@@ -362,12 +363,24 @@ export interface Location {
}
export interface MerchantInfo {
+ // The merchant's legal name of business.
name: string;
- jurisdiction?: Location;
- address?: Location;
- logo?: string;
- website?: string;
+
+ // Label for a location with the business address of the merchant.
email?: string;
+
+ // Label for a location with the business address of the merchant.
+ website?: string;
+
+ // An optional base64-encoded product image.
+ logo?: ImageDataUrl;
+
+ // Label for a location with the business address of the merchant.
+ address?: Location;
+
+ // Label for a location that denotes the jurisdiction for disputes.
+ // Some of the typical fields for a location (such as a street address) may be absent.
+ jurisdiction?: Location;
}
export interface Tax {
@@ -386,10 +399,10 @@ export interface Product {
description: string;
// Map from IETF BCP 47 language tags to localized descriptions
- description_i18n?: { [lang_tag: string]: string };
+ description_i18n?: InternationalizedString;
// The number of units of the product to deliver to the customer.
- quantity?: number;
+ quantity?: Integer;
// The unit in which the product is measured (liters, kilograms, packages, etc.)
unit?: string;
@@ -398,7 +411,7 @@ export interface Product {
price?: AmountString;
// An optional base64-encoded product image
- image?: string;
+ image?: ImageDataUrl;
// a list of taxes paid by the merchant for this product. Can be empty.
taxes?: Tax[];
@@ -416,147 +429,147 @@ export interface InternationalizedString {
* FIXME: Add type field!
*/
export interface MerchantContractTerms {
- /**
- * Hash of the merchant's wire details.
- */
+ // The hash of the merchant instance's wire details.
h_wire: string;
- /**
- * Hash of the merchant's wire details.
- */
+ // Specifies for how long the wallet should try to get an
+ // automatic refund for the purchase. If this field is
+ // present, the wallet should wait for a few seconds after
+ // the purchase and then automatically attempt to obtain
+ // a refund. The wallet should probe until "delay"
+ // after the payment was successful (i.e. via long polling
+ // or via explicit requests with exponential back-off).
+ //
+ // In particular, if the wallet is offline
+ // at that time, it MUST repeat the request until it gets
+ // one response from the merchant after the delay has expired.
+ // If the refund is granted, the wallet MUST automatically
+ // recover the payment. This is used in case a merchant
+ // knows that it might be unable to satisfy the contract and
+ // desires for the wallet to attempt to get the refund without any
+ // customer interaction. Note that it is NOT an error if the
+ // merchant does not grant a refund.
auto_refund?: TalerProtocolDuration;
- /**
- * Wire method the merchant wants to use.
- */
+ // Wire transfer method identifier for the wire method associated with h_wire.
+ // The wallet may only select exchanges via a matching auditor if the
+ // exchange also supports this wire method.
+ // The wire transfer fees must be added based on this wire transfer method.
wire_method: string;
- /**
- * Human-readable short summary of the contract.
- */
+ // Human-readable description of the whole purchase.
summary: string;
+ // Map from IETF BCP 47 language tags to localized summaries.
summary_i18n?: InternationalizedString;
- /**
- * Nonce used to ensure freshness.
- */
- nonce: string;
+ // Unique, free-form identifier for the proposal.
+ // Must be unique within a merchant instance.
+ // For merchants that do not store proposals in their DB
+ // before the customer paid for them, the order_id can be used
+ // by the frontend to restore a proposal from the information
+ // encoded in it (such as a short product identifier and timestamp).
+ order_id: string;
- /**
- * Total amount payable.
- */
+ // Total price for the transaction.
+ // The exchange will subtract deposit fees from that amount
+ // before transferring it to the merchant.
amount: string;
- /**
- * Auditors accepted by the merchant.
- */
- auditors: AuditorHandle[];
+ // Nonce generated by the wallet and echoed by the merchant
+ // in this field when the proposal is generated.
+ nonce: string;
- /**
- * Deadline to pay for the contract.
- */
+ // After this deadline, the merchant won't accept payments for the contract.
pay_deadline: TalerProtocolTimestamp;
- /**
- * Maximum deposit fee covered by the merchant.
- */
- max_fee: string;
-
- /**
- * Information about the merchant.
- */
+ // More info about the merchant, see below.
merchant: MerchantInfo;
- /**
- * Public key of the merchant.
- */
+ // Merchant's public key used to sign this proposal; this information
+ // is typically added by the backend. Note that this can be an ephemeral key.
merchant_pub: string;
- /**
- * Time indicating when the order should be delivered.
- * May be overwritten by individual products.
- */
+ // Time indicating when the order should be delivered.
+ // May be overwritten by individual products.
delivery_date?: TalerProtocolTimestamp;
- /**
- * Delivery location for (all!) products.
- */
+ // Delivery location for (all!) products.
delivery_location?: Location;
- /**
- * List of accepted exchanges.
- */
+ // Exchanges that the merchant accepts even if it does not accept any auditors that audit them.
exchanges: ExchangeHandle[];
- /**
- * Products that are sold in this contract.
- */
+ // List of products that are part of the purchase (see Product).
products?: Product[];
- /**
- * Deadline for refunds.
- */
+ // After this deadline has passed, no refunds will be accepted.
refund_deadline: TalerProtocolTimestamp;
- /**
- * Deadline for the wire transfer.
- */
+ // Transfer deadline for the exchange. Must be in the
+ // deposit permissions of coins used to pay for this order.
wire_transfer_deadline: TalerProtocolTimestamp;
- /**
- * Time when the contract was generated by the merchant.
- */
+ // Time when this contract was generated.
timestamp: TalerProtocolTimestamp;
- /**
- * Order id to uniquely identify the purchase within
- * one merchant instance.
- */
- order_id: string;
-
- /**
- * Base URL of the merchant's backend.
- */
+ // Base URL of the (public!) merchant backend API.
+ // Must be an absolute URL that ends with a slash.
merchant_base_url: string;
- /**
- * Fulfillment URL to view the product or
- * delivery status.
- */
+ // URL that will show that the order was successful after
+ // it has been paid for. Optional, but either fulfillment_url
+ // or fulfillment_message must be specified in every
+ // contract terms.
+ //
+ // If a non-unique fulfillment URL is used, a customer can only
+ // buy the order once and will be redirected to a previous purchase
+ // when trying to buy an order with the same fulfillment URL a second
+ // time. This is useful for digital goods that a customer only needs
+ // to buy once but should be able to repeatedly download.
+ //
+ // For orders where the customer is expected to be able to make
+ // repeated purchases (for equivalent goods), the fulfillment URL
+ // should be made unique for every order. The easiest way to do
+ // this is to include a unique order ID in the fulfillment URL.
+ //
+ // When POSTing to the merchant, the placeholder text "${ORDER_ID}"
+ // is be replaced with the actual order ID (useful if the
+ // order ID is generated server-side and needs to be
+ // in the URL). Note that this placeholder can only be used once.
+ // Front-ends may use other means to generate a unique fulfillment URL.
fulfillment_url?: string;
- /**
- * URL meant to share the shopping cart.
- */
+ // URL where the same contract could be ordered again (if
+ // available). Returned also at the public order endpoint
+ // for people other than the actual buyer (hence public,
+ // in case order IDs are guessable).
public_reorder_url?: string;
- /**
- * Plain text fulfillment message in the merchant's default language.
- */
+ // Message shown to the customer after paying for the order.
+ // Either fulfillment_url or fulfillment_message must be specified.
fulfillment_message?: string;
- /**
- * Internationalized fulfillment messages.
- */
+ // Map from IETF BCP 47 language tags to localized fulfillment
+ // messages.
fulfillment_message_i18n?: InternationalizedString;
- /**
- * Share of the wire fee that must be settled with one payment.
- */
- wire_fee_amortization?: number;
-
- /**
- * Maximum wire fee that the merchant agrees to pay for.
- */
- max_wire_fee?: string;
-
- minimum_age?: number;
+ // Maximum total deposit fee accepted by the merchant for this contract.
+ // Overrides defaults of the merchant instance.
+ max_fee: string;
- /**
- * Extra data, interpreted by the mechant only.
- */
+ // Extra data that is only interpreted by the merchant frontend.
+ // Useful when the merchant needs to store extra information on a
+ // contract without storing it separately in their database.
+ // Must really be an Object (not a string, integer, float or array).
extra?: any;
+
+ // Minimum age the buyer must have (in years). Default is 0.
+ // This value is at least as large as the maximum over all
+ // minimum age requirements of the products in this contract.
+ // It might also be set independent of any product, due to
+ // legal requirements.
+ minimum_age?: Integer;
}
/**
@@ -611,27 +624,6 @@ export interface MerchantAbortPayRefundDetails {
}
/**
- * Response for a refund pickup or a /pay in abort mode.
- */
-export interface MerchantRefundResponse {
- /**
- * Public key of the merchant
- */
- merchant_pub: string;
-
- /**
- * Contract terms hash of the contract that
- * is being refunded.
- */
- h_contract_terms: string;
-
- /**
- * The signed refund permissions, to be sent to the exchange.
- */
- refunds: MerchantAbortPayRefundDetails[];
-}
-
-/**
* Planchet detail sent to the merchant.
*/
export interface TipPlanchetDetail {
@@ -725,9 +717,11 @@ export class ExchangeSignKeyJson {
*/
export class ExchangeKeysJson {
/**
- * List of offered denominations.
+ * Canonical, public base URL of the exchange.
*/
- denoms: ExchangeDenomination[];
+ base_url: string;
+
+ currency: string;
/**
* The exchange's master public key.
@@ -763,6 +757,112 @@ export class ExchangeKeysJson {
reserve_closing_delay: TalerProtocolDuration;
global_fees: GlobalFees[];
+
+ accounts: ExchangeWireAccount[];
+
+ wire_fees: { [methodName: string]: WireFeesJson[] };
+
+ denominations: DenomGroup[];
+}
+
+export type DenomGroup =
+ | DenomGroupRsa
+ | DenomGroupCs
+ | DenomGroupRsaAgeRestricted
+ | DenomGroupCsAgeRestricted;
+
+export interface DenomGroupCommon {
+ // 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;
+
+ // XOR of all the SHA-512 hash values of the denominations' public keys
+ // in this group. Note that for hashing, the binary format of the
+ // public keys is used, and not their base32 encoding.
+ hash: HashCodeString;
+}
+
+export interface DenomCommon {
+ // 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 type RsaPublicKeySring = string;
+export type AgeMask = number;
+export type ImageDataUrl = string;
+
+/**
+ * 32-byte value representing a point on Curve25519.
+ */
+export type Cs25519Point = string;
+
+export interface DenomGroupRsa extends DenomGroupCommon {
+ cipher: "RSA";
+
+ denoms: ({
+ rsa_pub: RsaPublicKeySring;
+ } & DenomCommon)[];
+}
+
+export interface DenomGroupRsaAgeRestricted extends DenomGroupCommon {
+ cipher: "RSA+age_restricted";
+ age_mask: AgeMask;
+
+ denoms: ({
+ rsa_pub: RsaPublicKeySring;
+ } & DenomCommon)[];
+}
+
+export interface DenomGroupCs extends DenomGroupCommon {
+ cipher: "CS";
+ age_mask: AgeMask;
+
+ denoms: ({
+ cs_pub: Cs25519Point;
+ } & DenomCommon)[];
+}
+
+export interface DenomGroupCsAgeRestricted extends DenomGroupCommon {
+ cipher: "CS+age_restricted";
+ age_mask: AgeMask;
+
+ denoms: ({
+ cs_pub: Cs25519Point;
+ } & DenomCommon)[];
}
export interface GlobalFees {
@@ -837,16 +937,6 @@ export class WireFeesJson {
end_date: TalerProtocolTimestamp;
}
-export interface AccountInfo {
- payto_uri: string;
- master_sig: string;
-}
-
-export interface ExchangeWireJson {
- accounts: AccountInfo[];
- fees: { [methodName: string]: WireFeesJson[] };
-}
-
/**
* Proposal returned from the contract URL.
*/
@@ -880,6 +970,8 @@ export class CheckPaymentResponse {
* Response from the bank.
*/
export class WithdrawOperationStatusResponse {
+ status: "selected" | "aborted" | "confirmed" | "pending";
+
selection_done: boolean;
transfer_done: boolean;
@@ -900,11 +992,13 @@ export class WithdrawOperationStatusResponse {
/**
* Response from the merchant.
*/
-export class TipPickupGetResponse {
- tip_amount: string;
+export class RewardPickupGetResponse {
+ reward_amount: string;
exchange_url: string;
+ next_url?: string;
+
expiration: TalerProtocolTimestamp;
}
@@ -949,16 +1043,17 @@ export const codecForBlindedDenominationSignature = () =>
.alternative(DenomKeyType.Rsa, codecForRsaBlindedDenominationSignature())
.build("BlindedDenominationSignature");
-export class WithdrawResponse {
+export class ExchangeWithdrawResponse {
ev_sig: BlindedDenominationSignature;
}
-export class WithdrawBatchResponse {
- ev_sigs: WithdrawResponse[];
+export class ExchangeWithdrawBatchResponse {
+ ev_sigs: ExchangeWithdrawResponse[];
}
export interface MerchantPayResponse {
sig: string;
+ pos_confirmation?: string;
}
export interface ExchangeMeltRequest {
@@ -1128,9 +1223,42 @@ export interface MerchantOrderStatusUnpaid {
* POST {talerBankIntegrationApi}/withdrawal-operation/{wopid}
*/
export interface BankWithdrawalOperationPostResponse {
+ // Current status of the operation
+ // pending: the operation is pending parameters selection (exchange and reserve public key)
+ // selected: the operations has been selected and is pending confirmation
+ // aborted: the operation has been aborted
+ // confirmed: the transfer has been confirmed and registered by the bank
+ status: "selected" | "aborted" | "confirmed" | "pending";
+
+ // URL that the user needs to navigate to in order to
+ // complete some final confirmation (e.g. 2FA).
+ //
+ // Only applicable when status is selected or pending.
+ // It may contain withdrawal operation id
+ confirm_transfer_url?: string;
+
+ // Deprecated field use status instead
+ // The transfer has been confirmed and registered by the bank.
+ // Does not guarantee that the funds have arrived at the exchange already.
transfer_done: boolean;
}
+export const codeForBankWithdrawalOperationPostResponse =
+ (): Codec<BankWithdrawalOperationPostResponse> =>
+ buildCodecForObject<BankWithdrawalOperationPostResponse>()
+ .property(
+ "status",
+ codecForEither(
+ codecForConstString("selected"),
+ codecForConstString("confirmed"),
+ codecForConstString("aborted"),
+ codecForConstString("pending"),
+ ),
+ )
+ .property("confirm_transfer_url", codecOptional(codecForString()))
+ .property("transfer_done", codecForBoolean())
+ .build("BankWithdrawalOperationPostResponse");
+
export type DenominationPubKey = RsaDenominationPubKey | CsDenominationPubKey;
export interface RsaDenominationPubKey {
@@ -1201,16 +1329,17 @@ export const codecForDenominationPubKey = () =>
.alternative(DenomKeyType.ClauseSchnorr, codecForCsDenominationPubKey())
.build("DenominationPubKey");
-export const codecForBankWithdrawalOperationPostResponse =
- (): Codec<BankWithdrawalOperationPostResponse> =>
- buildCodecForObject<BankWithdrawalOperationPostResponse>()
- .property("transfer_done", codecForBoolean())
- .build("BankWithdrawalOperationPostResponse");
+export type LitAmountString = `${string}:${number}`;
-export type AmountString = string;
+declare const __amount_str: unique symbol;
+export type AmountString =
+ | (string & { [__amount_str]: true })
+ | LitAmountString;
+// export type AmountString = string;
export type Base32String = string;
export type EddsaSignatureString = string;
export type EddsaPublicKeyString = string;
+export type EddsaPrivateKeyString = string;
export type CoinPublicKeyString = string;
export const codecForDenomination = (): Codec<ExchangeDenomination> =>
@@ -1275,28 +1404,9 @@ export const codecForMerchantInfo = (): Codec<MerchantInfo> =>
.property("jurisdiction", codecOptional(codecForLocation()))
.build("MerchantInfo");
-export const codecForTax = (): Codec<Tax> =>
- buildCodecForObject<Tax>()
- .property("name", codecForString())
- .property("tax", codecForString())
- .build("Tax");
-
export const codecForInternationalizedString =
(): Codec<InternationalizedString> => codecForMap(codecForString());
-export const codecForProduct = (): Codec<Product> =>
- buildCodecForObject<Product>()
- .property("product_id", codecOptional(codecForString()))
- .property("description", codecForString())
- .property(
- "description_i18n",
- codecOptional(codecForInternationalizedString()),
- )
- .property("quantity", codecOptional(codecForNumber()))
- .property("unit", codecOptional(codecForString()))
- .property("price", codecOptional(codecForString()))
- .build("Tax");
-
export const codecForMerchantContractTerms = (): Codec<MerchantContractTerms> =>
buildCodecForObject<MerchantContractTerms>()
.property("order_id", codecForString())
@@ -1313,16 +1423,14 @@ export const codecForMerchantContractTerms = (): Codec<MerchantContractTerms> =>
.property("summary", codecForString())
.property("summary_i18n", codecOptional(codecForInternationalizedString()))
.property("nonce", codecForString())
- .property("amount", codecForString())
- .property("auditors", codecForList(codecForAuditorHandle()))
+ .property("amount", codecForAmountString())
.property("pay_deadline", codecForTimestamp)
.property("refund_deadline", codecForTimestamp)
.property("wire_transfer_deadline", codecForTimestamp)
.property("timestamp", codecForTimestamp)
.property("delivery_location", codecOptional(codecForLocation()))
.property("delivery_date", codecOptional(codecForTimestamp))
- .property("max_fee", codecForString())
- .property("max_wire_fee", codecOptional(codecForString()))
+ .property("max_fee", codecForAmountString())
.property("merchant", codecForMerchantInfo())
.property("merchant_pub", codecForString())
.property("exchanges", codecForList(codecForExchangeHandle()))
@@ -1334,7 +1442,7 @@ export const codecForMerchantContractTerms = (): Codec<MerchantContractTerms> =>
export const codecForPeerContractTerms = (): Codec<PeerContractTerms> =>
buildCodecForObject<PeerContractTerms>()
.property("summary", codecForString())
- .property("amount", codecForString())
+ .property("amount", codecForAmountString())
.property("purse_expiration", codecForTimestamp)
.build("PeerContractTerms");
@@ -1352,14 +1460,6 @@ export const codecForMerchantRefundPermission =
.property("exchange_pub", codecOptional(codecForString()))
.build("MerchantRefundPermission");
-export const codecForMerchantRefundResponse =
- (): Codec<MerchantRefundResponse> =>
- buildCodecForObject<MerchantRefundResponse>()
- .property("merchant_pub", codecForString())
- .property("h_contract_terms", codecForString())
- .property("refunds", codecForList(codecForMerchantRefundPermission()))
- .build("MerchantRefundResponse");
-
export const codecForBlindSigWrapperV2 = (): Codec<MerchantBlindSigWrapperV2> =>
buildCodecForObject<MerchantBlindSigWrapperV2>()
.property("blind_sig", codecForBlindedDenominationSignature())
@@ -1397,9 +1497,13 @@ export const codecForGlobalFees = (): Codec<GlobalFees> =>
.property("master_sig", codecForString())
.build("GlobalFees");
+// FIXME: Validate properly!
+export const codecForNgDenominations: Codec<DenomGroup> = codecForAny();
+
export const codecForExchangeKeysJson = (): Codec<ExchangeKeysJson> =>
buildCodecForObject<ExchangeKeysJson>()
- .property("denoms", codecForList(codecForDenomination()))
+ .property("base_url", codecForString())
+ .property("currency", codecForString())
.property("master_public_key", codecForString())
.property("auditors", codecForList(codecForAuditor()))
.property("list_issue_date", codecForTimestamp)
@@ -1408,6 +1512,9 @@ export const codecForExchangeKeysJson = (): Codec<ExchangeKeysJson> =>
.property("version", codecForString())
.property("reserve_closing_delay", codecForDuration)
.property("global_fees", codecForList(codecForGlobalFees()))
+ .property("accounts", codecForList(codecForExchangeWireAccount()))
+ .property("wire_fees", codecForMap(codecForList(codecForWireFeesJson())))
+ .property("denominations", codecForList(codecForNgDenominations))
.build("ExchangeKeysJson");
export const codecForWireFeesJson = (): Codec<WireFeesJson> =>
@@ -1419,18 +1526,6 @@ export const codecForWireFeesJson = (): Codec<WireFeesJson> =>
.property("end_date", codecForTimestamp)
.build("WireFeesJson");
-export const codecForAccountInfo = (): Codec<AccountInfo> =>
- buildCodecForObject<AccountInfo>()
- .property("payto_uri", codecForString())
- .property("master_sig", codecForString())
- .build("AccountInfo");
-
-export const codecForExchangeWireJson = (): Codec<ExchangeWireJson> =>
- buildCodecForObject<ExchangeWireJson>()
- .property("accounts", codecForList(codecForAccountInfo()))
- .property("fees", codecForMap(codecForList(codecForWireFeesJson())))
- .build("ExchangeWireJson");
-
export const codecForProposal = (): Codec<Proposal> =>
buildCodecForObject<Proposal>()
.property("contract_terms", codecForAny())
@@ -1450,6 +1545,15 @@ export const codecForCheckPaymentResponse = (): Codec<CheckPaymentResponse> =>
export const codecForWithdrawOperationStatusResponse =
(): Codec<WithdrawOperationStatusResponse> =>
buildCodecForObject<WithdrawOperationStatusResponse>()
+ .property(
+ "status",
+ codecForEither(
+ codecForConstString("selected"),
+ codecForConstString("confirmed"),
+ codecForConstString("aborted"),
+ codecForConstString("pending"),
+ ),
+ )
.property("selection_done", codecForBoolean())
.property("transfer_done", codecForBoolean())
.property("aborted", codecForBoolean())
@@ -1460,12 +1564,14 @@ export const codecForWithdrawOperationStatusResponse =
.property("wire_types", codecForList(codecForString()))
.build("WithdrawOperationStatusResponse");
-export const codecForTipPickupGetResponse = (): Codec<TipPickupGetResponse> =>
- buildCodecForObject<TipPickupGetResponse>()
- .property("tip_amount", codecForString())
- .property("exchange_url", codecForString())
- .property("expiration", codecForTimestamp)
- .build("TipPickupGetResponse");
+export const codecForRewardPickupGetResponse =
+ (): Codec<RewardPickupGetResponse> =>
+ buildCodecForObject<RewardPickupGetResponse>()
+ .property("reward_amount", codecForString())
+ .property("exchange_url", codecForString())
+ .property("next_url", codecOptional(codecForString()))
+ .property("expiration", codecForTimestamp)
+ .build("TipPickupGetResponse");
export const codecForRecoupConfirmation = (): Codec<RecoupConfirmation> =>
buildCodecForObject<RecoupConfirmation>()
@@ -1473,19 +1579,21 @@ export const codecForRecoupConfirmation = (): Codec<RecoupConfirmation> =>
.property("old_coin_pub", codecOptional(codecForString()))
.build("RecoupConfirmation");
-export const codecForWithdrawResponse = (): Codec<WithdrawResponse> =>
- buildCodecForObject<WithdrawResponse>()
+export const codecForWithdrawResponse = (): Codec<ExchangeWithdrawResponse> =>
+ buildCodecForObject<ExchangeWithdrawResponse>()
.property("ev_sig", codecForBlindedDenominationSignature())
.build("WithdrawResponse");
-export const codecForWithdrawBatchResponse = (): Codec<WithdrawBatchResponse> =>
- buildCodecForObject<WithdrawBatchResponse>()
- .property("ev_sigs", codecForList(codecForWithdrawResponse()))
- .build("WithdrawBatchResponse");
+export const codecForExchangeWithdrawBatchResponse =
+ (): Codec<ExchangeWithdrawBatchResponse> =>
+ buildCodecForObject<ExchangeWithdrawBatchResponse>()
+ .property("ev_sigs", codecForList(codecForWithdrawResponse()))
+ .build("WithdrawBatchResponse");
export const codecForMerchantPayResponse = (): Codec<MerchantPayResponse> =>
buildCodecForObject<MerchantPayResponse>()
.property("sig", codecForString())
+ .property("pos_confirmation", codecOptional(codecForString()))
.build("MerchantPayResponse");
export const codecForExchangeMeltResponse = (): Codec<ExchangeMeltResponse> =>
@@ -1507,57 +1615,15 @@ export const codecForExchangeRevealResponse =
.property("ev_sigs", codecForList(codecForExchangeRevealItem()))
.build("ExchangeRevealResponse");
-export const codecForMerchantCoinRefundSuccessStatus =
- (): Codec<MerchantCoinRefundSuccessStatus> =>
- buildCodecForObject<MerchantCoinRefundSuccessStatus>()
- .property("type", codecForConstString("success"))
- .property("coin_pub", codecForString())
- .property("exchange_status", codecForConstNumber(200))
- .property("exchange_sig", codecForString())
- .property("rtransaction_id", codecForNumber())
- .property("refund_amount", codecForString())
- .property("exchange_pub", codecForString())
- .property("execution_time", codecForTimestamp)
- .build("MerchantCoinRefundSuccessStatus");
-
-export const codecForMerchantCoinRefundFailureStatus =
- (): Codec<MerchantCoinRefundFailureStatus> =>
- buildCodecForObject<MerchantCoinRefundFailureStatus>()
- .property("type", codecForConstString("failure"))
- .property("coin_pub", codecForString())
- .property("exchange_status", codecForNumber())
- .property("rtransaction_id", codecForNumber())
- .property("refund_amount", codecForString())
- .property("exchange_code", codecOptional(codecForNumber()))
- .property("exchange_reply", codecOptional(codecForAny()))
- .property("execution_time", codecForTimestamp)
- .build("MerchantCoinRefundFailureStatus");
-
-export const codecForMerchantCoinRefundStatus =
- (): Codec<MerchantCoinRefundStatus> =>
- buildCodecForUnion<MerchantCoinRefundStatus>()
- .discriminateOn("type")
- .alternative("success", codecForMerchantCoinRefundSuccessStatus())
- .alternative("failure", codecForMerchantCoinRefundFailureStatus())
- .build("MerchantCoinRefundStatus");
-
export const codecForMerchantOrderStatusPaid =
(): Codec<MerchantOrderStatusPaid> =>
buildCodecForObject<MerchantOrderStatusPaid>()
- .property("refund_amount", codecForString())
- .property("refund_taken", codecForString())
+ .property("refund_amount", codecForAmountString())
+ .property("refund_taken", codecForAmountString())
.property("refund_pending", codecForBoolean())
.property("refunded", codecForBoolean())
.build("MerchantOrderStatusPaid");
-export const codecForMerchantOrderRefundPickupResponse =
- (): Codec<MerchantOrderRefundResponse> =>
- buildCodecForObject<MerchantOrderRefundResponse>()
- .property("merchant_pub", codecForString())
- .property("refund_amount", codecForString())
- .property("refunds", codecForList(codecForMerchantCoinRefundStatus()))
- .build("MerchantOrderRefundPickupResponse");
-
export const codecForMerchantOrderStatusUnpaid =
(): Codec<MerchantOrderStatusUnpaid> =>
buildCodecForObject<MerchantOrderStatusUnpaid>()
@@ -1595,37 +1661,6 @@ export interface AbortResponse {
refunds: MerchantAbortPayRefundStatus[];
}
-export const codecForMerchantAbortPayRefundSuccessStatus =
- (): Codec<MerchantAbortPayRefundSuccessStatus> =>
- buildCodecForObject<MerchantAbortPayRefundSuccessStatus>()
- .property("exchange_pub", codecForString())
- .property("exchange_sig", codecForString())
- .property("exchange_status", codecForConstNumber(200))
- .property("type", codecForConstString("success"))
- .build("MerchantAbortPayRefundSuccessStatus");
-
-export const codecForMerchantAbortPayRefundFailureStatus =
- (): Codec<MerchantAbortPayRefundFailureStatus> =>
- buildCodecForObject<MerchantAbortPayRefundFailureStatus>()
- .property("exchange_code", codecForNumber())
- .property("exchange_reply", codecForAny())
- .property("exchange_status", codecForNumber())
- .property("type", codecForConstString("failure"))
- .build("MerchantAbortPayRefundFailureStatus");
-
-export const codecForMerchantAbortPayRefundStatus =
- (): Codec<MerchantAbortPayRefundStatus> =>
- buildCodecForUnion<MerchantAbortPayRefundStatus>()
- .discriminateOn("type")
- .alternative("success", codecForMerchantAbortPayRefundSuccessStatus())
- .alternative("failure", codecForMerchantAbortPayRefundFailureStatus())
- .build("MerchantAbortPayRefundStatus");
-
-export const codecForAbortResponse = (): Codec<AbortResponse> =>
- buildCodecForObject<AbortResponse>()
- .property("refunds", codecForList(codecForMerchantAbortPayRefundStatus()))
- .build("AbortResponse");
-
export type MerchantAbortPayRefundStatus =
| MerchantAbortPayRefundSuccessStatus
| MerchantAbortPayRefundFailureStatus;
@@ -1667,19 +1702,6 @@ export interface MerchantAbortPayRefundSuccessStatus {
exchange_pub: string;
}
-export interface TalerConfigResponse {
- name: string;
- version: string;
- currency?: string;
-}
-
-export const codecForTalerConfigResponse = (): Codec<TalerConfigResponse> =>
- buildCodecForObject<TalerConfigResponse>()
- .property("name", codecForString())
- .property("version", codecForString())
- .property("currency", codecOptional(codecForString()))
- .build("TalerConfigResponse");
-
export interface FutureKeysResponse {
future_denoms: any[];
@@ -1750,6 +1772,10 @@ export interface ExchangeWithdrawRequest {
coin_ev: CoinEnvelope;
}
+export interface ExchangeBatchWithdrawRequest {
+ planchets: ExchangeWithdrawRequest[];
+}
+
export interface ExchangeRefreshRevealRequest {
new_denoms_h: HashCodeString[];
coin_evs: CoinEnvelope[];
@@ -1770,42 +1796,112 @@ export interface ExchangeRefreshRevealRequest {
old_age_commitment?: Edx25519PublicKeyEnc[];
}
-export interface DepositSuccess {
+interface DepositConfirmationSignature {
+ // The EdDSA signature of `TALER_DepositConfirmationPS` using a current
+ // `signing key of the exchange <sign-key-priv>` affirming the successful
+ // deposit and that the exchange will transfer the funds after the refund
+ // deadline, or as soon as possible if the refund deadline is zero.
+ exchange_sig: EddsaSignatureString;
+}
+
+export interface BatchDepositSuccess {
// Optional base URL of the exchange for looking up wire transfers
// associated with this transaction. If not given,
// the base URL is the same as the one used for this request.
- // Can be used if the base URL for /transactions/ differs from that
- // for /coins/, i.e. for load balancing. Clients SHOULD
- // respect the transaction_base_url if provided. Any HTTP server
+ // Can be used if the base URL for ``/transactions/`` differs from that
+ // for ``/coins/``, i.e. for load balancing. Clients SHOULD
+ // respect the ``transaction_base_url`` if provided. Any HTTP server
// belonging to an exchange MUST generate a 307 or 308 redirection
// to the correct base URL should a client uses the wrong base
// URL, or if the base URL has changed since the deposit.
transaction_base_url?: string;
- // timestamp when the deposit was received by the exchange.
+ // Timestamp when the deposit was received by the exchange.
exchange_timestamp: TalerProtocolTimestamp;
- // the EdDSA signature of TALER_DepositConfirmationPS using a current
- // signing key of the exchange affirming the successful
- // deposit and that the exchange will transfer the funds after the refund
- // deadline, or as soon as possible if the refund deadline is zero.
- exchange_sig: string;
-
- // public EdDSA key of the exchange that was used to
+ // `Public EdDSA key of the exchange <sign-key-pub>` that was used to
// generate the signature.
- // Should match one of the exchange's signing keys from /keys. It is given
+ // Should match one of the exchange's signing keys from ``/keys``. It is given
// explicitly as the client might otherwise be confused by clock skew as to
// which signing key was used.
- exchange_pub: string;
+ exchange_pub: EddsaPublicKeyString;
+
+ // Array of deposit confirmation signatures from the exchange
+ // Entries must be in the same order the coins were given
+ // in the batch deposit request.
+ exchange_sig: EddsaSignatureString;
}
-export const codecForDepositSuccess = (): Codec<DepositSuccess> =>
- buildCodecForObject<DepositSuccess>()
+export const codecForBatchDepositSuccess = (): Codec<BatchDepositSuccess> =>
+ buildCodecForObject<BatchDepositSuccess>()
.property("exchange_pub", codecForString())
.property("exchange_sig", codecForString())
.property("exchange_timestamp", codecForTimestamp)
.property("transaction_base_url", codecOptional(codecForString()))
- .build("DepositSuccess");
+ .build("BatchDepositSuccess");
+
+export interface TrackTransactionWired {
+ // Raw wire transfer identifier of the deposit.
+ wtid: Base32String;
+
+ // When was the wire transfer given to the bank.
+ execution_time: TalerProtocolTimestamp;
+
+ // The contribution of this coin to the total (without fees)
+ coin_contribution: AmountString;
+
+ // Binary-only Signature_ with purpose TALER_SIGNATURE_EXCHANGE_CONFIRM_WIRE
+ // over a TALER_ConfirmWirePS
+ // whereby the exchange affirms the successful wire transfer.
+ exchange_sig: EddsaSignatureString;
+
+ // Public EdDSA key of the exchange that was used to generate the signature.
+ // Should match one of the exchange's signing keys from /keys. Again given
+ // explicitly as the client might otherwise be confused by clock skew as to
+ // which signing key was used.
+ exchange_pub: EddsaPublicKeyString;
+}
+
+export const codecForTackTransactionWired = (): Codec<TrackTransactionWired> =>
+ buildCodecForObject<TrackTransactionWired>()
+ .property("wtid", codecForString())
+ .property("execution_time", codecForTimestamp)
+ .property("coin_contribution", codecForAmountString())
+ .property("exchange_sig", codecForString())
+ .property("exchange_pub", codecForString())
+ .build("TackTransactionWired");
+
+interface TrackTransactionAccepted {
+ // Legitimization target that the merchant should
+ // use to check for its KYC status using
+ // the /kyc-check/$REQUIREMENT_ROW/... endpoint.
+ // Optional, not present if the deposit has not
+ // yet been aggregated to the point that a KYC
+ // need has been evaluated.
+ requirement_row?: number;
+
+ // True if the KYC check for the merchant has been
+ // satisfied. False does not mean that KYC
+ // is strictly needed, unless also a
+ // legitimization_uuid is provided.
+ kyc_ok: boolean;
+
+ // Time by which the exchange currently thinks the deposit will be executed.
+ // Actual execution may be later if the KYC check is not satisfied by then.
+ execution_time: TalerProtocolTimestamp;
+}
+
+export const codecForTackTransactionAccepted =
+ (): Codec<TrackTransactionAccepted> =>
+ buildCodecForObject<TrackTransactionAccepted>()
+ .property("requirement_row", codecOptional(codecForNumber()))
+ .property("kyc_ok", codecForBoolean())
+ .property("execution_time", codecForTimestamp)
+ .build("TackTransactionAccepted");
+
+export type TrackTransaction =
+ | ({ type: "accepted" } & TrackTransactionAccepted)
+ | ({ type: "wired" } & TrackTransactionWired);
export interface PurseDeposit {
/**
@@ -1966,6 +2062,9 @@ export interface ExchangePurseDeposits {
deposits: PurseDeposit[];
}
+/**
+ * @deprecated batch deposit should be used.
+ */
export interface ExchangeDepositRequest {
// Amount to be deposited, can be a fraction of the
// coin's total value.
@@ -2027,3 +2126,296 @@ export interface ExchangeDepositRequest {
h_age_commitment?: string;
}
+
+export type WireSalt = string;
+
+export interface ExchangeBatchDepositRequest {
+ // The merchant's account details.
+ merchant_payto_uri: string;
+
+ // The salt is used to hide the ``payto_uri`` from customers
+ // when computing the ``h_wire`` of the merchant.
+ wire_salt: WireSalt;
+
+ // SHA-512 hash of the contract of the merchant with the customer. Further
+ // details are never disclosed to the exchange.
+ h_contract_terms: HashCodeString;
+
+ // The list of coins that are going to be deposited with this Request.
+ coins: BatchDepositRequestCoin[];
+
+ // Timestamp when the contract was finalized.
+ timestamp: TalerProtocolTimestamp;
+
+ // Indicative time by which the exchange undertakes to transfer the funds to
+ // the merchant, in case of successful payment. A wire transfer deadline of 'never'
+ // is not allowed.
+ wire_transfer_deadline: TalerProtocolTimestamp;
+
+ // EdDSA `public key of the merchant <merchant-pub>`, so that the client can identify the
+ // merchant for refund requests.
+ merchant_pub: EddsaPublicKeyString;
+
+ // Date until which the merchant can issue a refund to the customer via the
+ // exchange, to be omitted if refunds are not allowed.
+ //
+ // THIS FIELD WILL BE DEPRECATED, once the refund mechanism becomes a
+ // policy via extension.
+ refund_deadline?: TalerProtocolTimestamp;
+
+ // CAVEAT: THIS IS WORK IN PROGRESS
+ // (Optional) policy for the batch-deposit.
+ // This might be a refund, auction or escrow policy.
+ policy?: any;
+}
+
+export interface BatchDepositRequestCoin {
+ // EdDSA public key of the coin being deposited.
+ coin_pub: EddsaPublicKeyString;
+
+ // Hash of denomination RSA key with which the coin is signed.
+ denom_pub_hash: HashCodeString;
+
+ // Exchange's unblinded RSA signature of the coin.
+ ub_sig: UnblindedSignature;
+
+ // Amount to be deposited, can be a fraction of the
+ // coin's total value.
+ contribution: Amounts;
+
+ // Signature over `TALER_DepositRequestPS`, made by the customer with the
+ // `coin's private key <coin-priv>`.
+ coin_sig: EddsaSignatureString;
+
+ h_age_commitment?: string;
+}
+
+export interface WalletKycUuid {
+ // UUID that the wallet should use when initiating
+ // the KYC check.
+ requirement_row: number;
+
+ // Hash of the payto:// account URI for the wallet.
+ h_payto: string;
+}
+
+export const codecForWalletKycUuid = (): Codec<WalletKycUuid> =>
+ buildCodecForObject<WalletKycUuid>()
+ .property("requirement_row", codecForNumber())
+ .property("h_payto", codecForString())
+ .build("WalletKycUuid");
+
+export interface MerchantUsingTemplateDetails {
+ summary?: string;
+ amount?: AmountString;
+}
+
+export interface ExchangeRefundRequest {
+ // Amount to be refunded, can be a fraction of the
+ // coin's total deposit value (including deposit fee);
+ // must be larger than the refund fee.
+ refund_amount: AmountString;
+
+ // SHA-512 hash of the contact of the merchant with the customer.
+ h_contract_terms: HashCodeString;
+
+ // 64-bit transaction id of the refund transaction between merchant and customer.
+ rtransaction_id: number;
+
+ // EdDSA public key of the merchant.
+ merchant_pub: EddsaPublicKeyString;
+
+ // EdDSA signature of the merchant over a
+ // TALER_RefundRequestPS with purpose
+ // TALER_SIGNATURE_MERCHANT_REFUND
+ // affirming the refund.
+ merchant_sig: EddsaPublicKeyString;
+}
+
+export interface ExchangeRefundSuccessResponse {
+ // The EdDSA :ref:signature (binary-only) with purpose
+ // TALER_SIGNATURE_EXCHANGE_CONFIRM_REFUND over
+ // a TALER_RecoupRefreshConfirmationPS
+ // using a current signing key of the
+ // exchange affirming the successful refund.
+ exchange_sig: EddsaSignatureString;
+
+ // Public EdDSA key of the exchange that was used to generate the signature.
+ // Should match one of the exchange's signing keys from /keys. It is given
+ // explicitly as the client might otherwise be confused by clock skew as to
+ // which signing key was used.
+ exchange_pub: EddsaPublicKeyString;
+}
+
+export const codecForExchangeRefundSuccessResponse =
+ (): Codec<ExchangeRefundSuccessResponse> =>
+ buildCodecForObject<ExchangeRefundSuccessResponse>()
+ .property("exchange_pub", codecForString())
+ .property("exchange_sig", codecForString())
+ .build("ExchangeRefundSuccessResponse");
+
+export type AccountRestriction =
+ | RegexAccountRestriction
+ | DenyAllAccountRestriction;
+
+export interface DenyAllAccountRestriction {
+ type: "deny";
+}
+
+// Accounts interacting with this type of account
+// restriction must have a payto://-URI matching
+// the given regex.
+export interface RegexAccountRestriction {
+ type: "regex";
+
+ // Regular expression that the payto://-URI of the
+ // partner account must follow. The regular expression
+ // should follow posix-egrep, but without support for character
+ // classes, GNU extensions, back-references or intervals. See
+ // https://www.gnu.org/software/findutils/manual/html_node/find_html/posix_002degrep-regular-expression-syntax.html
+ // for a description of the posix-egrep syntax. Applications
+ // may support regexes with additional features, but exchanges
+ // must not use such regexes.
+ payto_regex: string;
+
+ // Hint for a human to understand the restriction
+ // (that is hopefully easier to comprehend than the regex itself).
+ human_hint: string;
+
+ // Map from IETF BCP 47 language tags to localized
+ // human hints.
+ human_hint_i18n?: InternationalizedString;
+}
+
+export interface ExchangeWireAccount {
+ // payto:// URI identifying the account and wire method
+ payto_uri: string;
+
+ // URI to convert amounts from or to the currency used by
+ // this wire account of the exchange. Missing if no
+ // conversion is applicable.
+ conversion_url?: string;
+
+ // Restrictions that apply to bank accounts that would send
+ // funds to the exchange (crediting this exchange bank account).
+ // Optional, empty array for unrestricted.
+ credit_restrictions: AccountRestriction[];
+
+ // Restrictions that apply to bank accounts that would receive
+ // funds from the exchange (debiting this exchange bank account).
+ // Optional, empty array for unrestricted.
+ debit_restrictions: AccountRestriction[];
+
+ // Signature using the exchange's offline key over
+ // a TALER_MasterWireDetailsPS
+ // with purpose TALER_SIGNATURE_MASTER_WIRE_DETAILS.
+ master_sig: EddsaSignatureString;
+
+ // Display label wallets should use to show this
+ // bank account.
+ // Since protocol **v19**.
+ bank_label?: string;
+ priority?: number;
+}
+
+export const codecForExchangeWireAccount = (): Codec<ExchangeWireAccount> =>
+ buildCodecForObject<ExchangeWireAccount>()
+ .property("conversion_url", codecOptional(codecForStringURL()))
+ .property("credit_restrictions", codecForList(codecForAny()))
+ .property("debit_restrictions", codecForList(codecForAny()))
+ .property("master_sig", codecForString())
+ .property("payto_uri", codecForString())
+ .property("bank_label", codecOptional(codecForString()))
+ .property("priority", codecOptional(codecForNumber()))
+ .build("WireAccount");
+
+export type Integer = number;
+
+export interface BankConversionInfoConfig {
+ // libtool-style representation of the Bank 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 API.
+ name: "taler-conversion-info";
+
+ regional_currency: string;
+
+ fiat_currency: string;
+
+ // Currency used by this bank.
+ regional_currency_specification: CurrencySpecification;
+
+ // External currency used during conversion.
+ fiat_currency_specification: CurrencySpecification;
+}
+
+export const codecForBankConversionInfoConfig =
+ (): Codec<BankConversionInfoConfig> =>
+ buildCodecForObject<BankConversionInfoConfig>()
+ .property("name", codecForConstString("taler-conversion-info"))
+ .property("version", codecForString())
+ .property("fiat_currency", codecForString())
+ .property("regional_currency", codecForString())
+ .property("fiat_currency_specification", codecForCurrencySpecificiation())
+ .property(
+ "regional_currency_specification",
+ codecForCurrencySpecificiation(),
+ )
+ .build("BankConversionInfoConfig");
+
+export interface DenominationExpiredMessage {
+ // Taler error code. Note that beyond
+ // expiration this message format is also
+ // used if the key is not yet valid, or
+ // has been revoked.
+ code: number;
+
+ // Signature by the exchange over a
+ // TALER_DenominationExpiredAffirmationPS.
+ // Must have purpose TALER_SIGNATURE_EXCHANGE_AFFIRM_DENOM_EXPIRED.
+ exchange_sig: EddsaSignatureString;
+
+ // Public key of the exchange used to create
+ // the 'exchange_sig.
+ exchange_pub: EddsaPublicKeyString;
+
+ // Hash of the denomination public key that is unknown.
+ h_denom_pub: HashCodeString;
+
+ // When was the signature created.
+ timestamp: TalerProtocolTimestamp;
+
+ // What kind of operation was requested that now
+ // failed?
+ oper: string;
+}
+
+export const codecForDenominationExpiredMessage = () =>
+ buildCodecForObject<DenominationExpiredMessage>()
+ .property("code", codecForNumber())
+ .property("exchange_sig", codecForString())
+ .property("exchange_pub", codecForString())
+ .property("h_denom_pub", codecForString())
+ .property("timestamp", codecForTimestamp)
+ .property("oper", codecForString())
+ .build("DenominationExpiredMessage");
+
+export interface CoinHistoryResponse {
+ // Current balance of the coin.
+ balance: AmountString;
+
+ // Hash of the coin's denomination.
+ h_denom_pub: HashCodeString;
+
+ // Transaction history for the coin.
+ history: any[];
+}
+
+export const codecForCoinHistoryResponse = () =>
+ buildCodecForObject<CoinHistoryResponse>()
+ .property("balance", codecForAmountString())
+ .property("h_denom_pub", codecForString())
+ .property("history", codecForAny())
+ .build("CoinHistoryResponse");
diff --git a/packages/taler-util/src/talerconfig.ts b/packages/taler-util/src/talerconfig.ts
index 59c789cae..2bd7b355f 100644
--- a/packages/taler-util/src/talerconfig.ts
+++ b/packages/taler-util/src/talerconfig.ts
@@ -1,6 +1,6 @@
/*
This file is part of GNU Taler
- (C) 2020 Taler Systems S.A.
+ (C) 2020-2023 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
@@ -23,12 +23,14 @@
/**
* Imports
*/
-import { AmountJson } from "./amounts.js";
-import { Amounts } from "./amounts.js";
+import { AmountJson, Amounts } from "./amounts.js";
+import { Logger } from "./logging.js";
-import nodejs_path from "path";
-import nodejs_os from "os";
import nodejs_fs from "fs";
+import nodejs_os from "os";
+import nodejs_path from "path";
+
+const logger = new Logger("talerconfig.ts");
export class ConfigError extends Error {
constructor(message: string) {
@@ -39,10 +41,30 @@ export class ConfigError extends Error {
}
}
+enum EntryOrigin {
+ /**
+ * From a default file.
+ */
+ DefaultFile = 1,
+ /**
+ * From a system/installation specific default value.
+ */
+ DefaultSystem = 2,
+ /**
+ * Loaded from file or string
+ */
+ Loaded = 3,
+ /**
+ * Changed after loading
+ */
+ Changed = 4,
+}
+
interface Entry {
value: string;
sourceLine: number;
sourceFile: string;
+ origin: EntryOrigin;
}
interface Section {
@@ -53,11 +75,59 @@ interface Section {
type SectionMap = { [sectionName: string]: Section };
+/**
+ * Different projects use the GNUnet/Taler-Style config.
+ *
+ * The config source determines where to locate the configuration.
+ */
+export interface ConfigSource {
+ projectName: string;
+ componentName: string;
+ installPathBinary: string;
+ baseConfigVarname: string;
+ prefixVarname: string;
+}
+
+export type ConfigSourceDef = { [x: string]: ConfigSource | undefined };
+
+export const ConfigSources = {
+ ["taler"]: {
+ projectName: "taler",
+ componentName: "taler",
+ installPathBinary: "taler-config",
+ baseConfigVarname: "TALER_BASE_CONFIG",
+ prefixVarname: "TALER_PREFIX",
+ } satisfies ConfigSource,
+ ["libeufin-bank"]: {
+ projectName: "libeufin",
+ componentName: "libeufin-bank",
+ installPathBinary: "libeufin-bank",
+ baseConfigVarname: "LIBEUFIN_BASE_CONFIG",
+ prefixVarname: "LIBEUFIN_PREFIX",
+ } satisfies ConfigSource,
+ ["libeufin-nexus"]: {
+ projectName: "libeufin",
+ componentName: "libeufin-nexus",
+ installPathBinary: "libeufin-nexus",
+ baseConfigVarname: "LIBEUFIN_BASE_CONFIG",
+ prefixVarname: "LIBEUFIN_PREFIX",
+ } satisfies ConfigSource,
+ ["gnunet"]: {
+ projectName: "gnunet",
+ componentName: "gnunet",
+ installPathBinary: "gnunet-config",
+ baseConfigVarname: "GNUNET_BASE_CONFIG",
+ prefixVarname: "GNUNET_PREFIX",
+ } satisfies ConfigSource,
+} satisfies ConfigSourceDef;
+
+const defaultConfigSource: ConfigSource = ConfigSources.taler;
+
export class ConfigValue<T> {
constructor(
private sectionName: string,
private optionName: string,
- public value: string | undefined,
+ private value: string | undefined,
private converter: (x: string) => T,
) {}
@@ -89,6 +159,10 @@ export class ConfigValue<T> {
isDefined(): boolean {
return this.value !== undefined;
}
+
+ getValue(): string | undefined {
+ return this.value;
+ }
}
/**
@@ -116,9 +190,9 @@ export function expandPath(path: string): string {
export function pathsub(
x: string,
lookup: (s: string, depth: number) => string | undefined,
- depth = 0,
+ recursionDepth = 0,
): string {
- if (depth >= 10) {
+ if (recursionDepth >= 128) {
throw Error("recursion in path substitution");
}
let s = x;
@@ -157,14 +231,14 @@ export function pathsub(
defaultValue = undefined;
}
- const r = lookup(inner, depth + 1);
+ const r = lookup(varname, depth + 1);
if (r !== undefined) {
- s = s.substr(0, start) + r + s.substr(p + 1);
+ s = s.substring(0, start) + r + s.substring(p + 1);
l = start + r.length;
continue;
} else if (defaultValue !== undefined) {
const resolvedDefault = pathsub(defaultValue, lookup, depth + 1);
- s = s.substr(0, start) + resolvedDefault + s.substr(p + 1);
+ s = s.substring(0, start) + resolvedDefault + s.substring(p + 1);
l = start + resolvedDefault.length;
continue;
}
@@ -174,9 +248,9 @@ export function pathsub(
} else {
const m = /^[a-zA-Z-_][a-zA-Z0-9-_]*/.exec(s.substring(l + 1));
if (m && m[0]) {
- const r = lookup(m[0], depth + 1);
+ const r = lookup(m[0], recursionDepth + 1);
if (r !== undefined) {
- s = s.substr(0, l) + r + s.substr(l + 1 + m[0].length);
+ s = s.substring(0, l) + r + s.substring(l + 1 + m[0].length);
l = l + r.length;
continue;
}
@@ -188,13 +262,14 @@ export function pathsub(
return s;
}
-export interface LoadOptions {
+interface LoadOptions {
filename?: string;
banDirectives?: boolean;
}
export interface StringifyOptions {
diagnostics?: boolean;
+ excludeDefaults?: boolean;
}
export interface LoadedFile {
@@ -282,7 +357,19 @@ export class Configuration {
private nestLevel = 0;
- private loadFromFilename(filename: string, opts: LoadOptions = {}): void {
+ /**
+ * Does the entrypoint config file contain complex
+ * directives?
+ */
+ private entrypointIsComplex: boolean = false;
+
+ constructor(private configSource: ConfigSource = defaultConfigSource) {}
+
+ private loadFromFilename(
+ filename: string,
+ isDefaultSource: boolean,
+ opts: LoadOptions = {},
+ ): void {
filename = expandPath(filename);
const checkCycle = () => {
@@ -309,7 +396,7 @@ export class Configuration {
const oldNestLevel = this.nestLevel;
this.nestLevel += 1;
try {
- this.loadFromString(s, {
+ this.internalLoadFromString(s, isDefaultSource, {
...opts,
filename: filename,
});
@@ -318,7 +405,11 @@ export class Configuration {
}
}
- private loadGlob(parentFilename: string, fileglob: string): void {
+ private loadGlob(
+ parentFilename: string,
+ isDefaultSource: boolean,
+ fileglob: string,
+ ): void {
const resolvedParent = nodejs_fs.realpathSync(parentFilename);
const parentDir = nodejs_path.dirname(resolvedParent);
@@ -339,12 +430,16 @@ export class Configuration {
for (const f of files) {
if (globMatch(tail, f)) {
const fullPath = nodejs_path.join(head, f);
- this.loadFromFilename(fullPath);
+ this.loadFromFilename(fullPath, isDefaultSource);
}
}
}
- private loadSecret(sectionName: string, filename: string): void {
+ private loadSecret(
+ sectionName: string,
+ filename: string,
+ isDefaultSource: boolean,
+ ): void {
const sec = this.provideSection(sectionName);
sec.secretFilename = filename;
const otherCfg = new Configuration();
@@ -354,7 +449,7 @@ export class Configuration {
sec.inaccessible = true;
return;
}
- otherCfg.loadFromFilename(filename, {
+ otherCfg.loadFromFilename(filename, isDefaultSource, {
banDirectives: true,
});
const otherSec = otherCfg.provideSection(sectionName);
@@ -363,7 +458,11 @@ export class Configuration {
}
}
- loadFromString(s: string, opts: LoadOptions = {}): void {
+ private internalLoadFromString(
+ s: string,
+ isDefaultSource: boolean,
+ opts: LoadOptions = {},
+ ): void {
let lineNo = 0;
const fn = opts.filename ?? "<input>";
const reComment = /^\s*#.*$/;
@@ -390,6 +489,9 @@ export class Configuration {
`invalid configuration, directive in ${fn}:${lineNo} forbidden`,
);
}
+ if (!isDefaultSource) {
+ this.entrypointIsComplex = true;
+ }
const directive = directiveMatch[1].toLowerCase();
switch (directive) {
case "inline": {
@@ -399,7 +501,10 @@ export class Configuration {
);
}
const arg = directiveMatch[2].trim();
- this.loadFromFilename(normalizeInlineFilename(opts.filename, arg));
+ this.loadFromFilename(
+ normalizeInlineFilename(opts.filename, arg),
+ isDefaultSource,
+ );
break;
}
case "inline-secret": {
@@ -419,7 +524,7 @@ export class Configuration {
opts.filename,
sp[1],
);
- this.loadSecret(sp[0], secretFilename);
+ this.loadSecret(sp[0], secretFilename, isDefaultSource);
break;
}
case "inline-matching": {
@@ -429,7 +534,7 @@ export class Configuration {
`invalid configuration, @inline-matching@ directive in ${fn}:${lineNo} can only be used from a file`,
);
}
- this.loadGlob(opts.filename, arg);
+ this.loadGlob(opts.filename, isDefaultSource, arg);
break;
}
default:
@@ -462,6 +567,9 @@ export class Configuration {
value: val,
sourceFile: opts.filename ?? "<unknown>",
sourceLine: lineNo,
+ origin: isDefaultSource
+ ? EntryOrigin.DefaultFile
+ : EntryOrigin.Loaded,
};
continue;
}
@@ -496,6 +604,24 @@ export class Configuration {
value,
sourceLine: 0,
sourceFile: "<unknown>",
+ origin: EntryOrigin.Changed,
+ };
+ }
+
+ /**
+ * Set a string value to a value from default locations.
+ */
+ private setStringSystemDefault(
+ section: string,
+ option: string,
+ value: string,
+ ): void {
+ const sec = this.provideSection(section);
+ sec.entries[option.toUpperCase()] = {
+ value,
+ sourceLine: 0,
+ sourceFile: "<unknown>",
+ origin: EntryOrigin.DefaultSystem,
};
}
@@ -563,11 +689,14 @@ export class Configuration {
if (val !== undefined) {
return pathsub(val, (v, d) => this.lookupVariable(v, d), depth);
}
+
// Environment variables can be case sensitive, respect that.
const envVal = process.env[x];
if (envVal !== undefined) {
return envVal;
}
+
+ logger.warn(`unable to resolve variable '${x}'`);
return;
}
@@ -578,71 +707,146 @@ export class Configuration {
);
}
- loadFrom(dirname: string): void {
+ private loadDefaultsFromDir(dirname: string): void {
const files = nodejs_fs.readdirSync(dirname);
for (const f of files) {
const fn = nodejs_path.join(dirname, f);
- this.loadFromFilename(fn);
+ this.loadFromFilename(fn, true);
}
}
private loadDefaults(): void {
- let bc = process.env["TALER_BASE_CONFIG"];
- if (!bc) {
+ const { projectName, prefixVarname, baseConfigVarname, installPathBinary } =
+ this.configSource;
+ let baseConfigDir = process.env[baseConfigVarname];
+ if (!baseConfigDir) {
/* Try to locate the configuration based on the location
* of the taler-config binary. */
- const path = which("taler-config");
+ const path = which(installPathBinary);
+ if (path) {
+ baseConfigDir = nodejs_fs.realpathSync(
+ nodejs_path.dirname(path) + `/../share/${projectName}/config.d`,
+ );
+ }
+ }
+ if (!baseConfigDir) {
+ baseConfigDir = `/usr/share/${projectName}/config.d`;
+ }
+
+ let installPrefix = process.env[prefixVarname];
+ if (!installPrefix) {
+ /* Try to locate install path based on the location
+ * of the taler-config binary. */
+ const path = which(installPathBinary);
if (path) {
- bc = nodejs_fs.realpathSync(
- nodejs_path.dirname(path) + "/../share/taler/config.d",
+ installPrefix = nodejs_fs.realpathSync(
+ nodejs_path.dirname(path) + "/..",
);
}
}
- if (!bc) {
- bc = "/usr/share/taler/config.d";
+ if (!installPrefix) {
+ installPrefix = "/usr";
}
- this.loadFrom(bc);
+
+ this.setStringSystemDefault(
+ "PATHS",
+ "LIBEXECDIR",
+ `${installPrefix}/${projectName}/libexec/`,
+ );
+ this.setStringSystemDefault(
+ "PATHS",
+ "DOCDIR",
+ `${installPrefix}/share/doc/${projectName}/`,
+ );
+ this.setStringSystemDefault(
+ "PATHS",
+ "ICONDIR",
+ `${installPrefix}/share/icons/`,
+ );
+ this.setStringSystemDefault(
+ "PATHS",
+ "LOCALEDIR",
+ `${installPrefix}/share/locale/`,
+ );
+ this.setStringSystemDefault("PATHS", "PREFIX", `${installPrefix}/`);
+ this.setStringSystemDefault("PATHS", "BINDIR", `${installPrefix}/bin`);
+ this.setStringSystemDefault(
+ "PATHS",
+ "LIBDIR",
+ `${installPrefix}/lib/${projectName}/`,
+ );
+ this.setStringSystemDefault(
+ "PATHS",
+ "DATADIR",
+ `${installPrefix}/share/${projectName}/`,
+ );
+
+ this.loadDefaultsFromDir(baseConfigDir);
}
- getDefaultConfigFilename(): string | undefined {
+ private findDefaultConfigFilename(): string | undefined {
const xdg = process.env["XDG_CONFIG_HOME"];
const home = process.env["HOME"];
let fn: string | undefined;
+ const { projectName, componentName } = this.configSource;
if (xdg) {
- fn = nodejs_path.join(xdg, "taler.conf");
+ fn = nodejs_path.join(xdg, `${componentName}.conf`);
} else if (home) {
- fn = nodejs_path.join(home, ".config/taler.conf");
+ fn = nodejs_path.join(home, `.config/${componentName}.conf`);
}
if (fn && nodejs_fs.existsSync(fn)) {
return fn;
}
- const etc1 = "/etc/taler.conf";
+ const etc1 = `/etc/${componentName}.conf`;
if (nodejs_fs.existsSync(etc1)) {
return etc1;
}
- const etc2 = "/etc/taler/taler.conf";
+ const etc2 = `/etc/${projectName}/${componentName}.conf`;
if (nodejs_fs.existsSync(etc2)) {
return etc2;
}
return undefined;
}
- static load(filename?: string): Configuration {
- const cfg = new Configuration();
+ static load(
+ filename?: string,
+ configSource?: ConfigSource | string,
+ ): Configuration {
+ let cs: ConfigSource;
+ if (configSource == null) {
+ cs = defaultConfigSource;
+ } else if (typeof configSource === "string") {
+ if (configSource in ConfigSources) {
+ cs = ConfigSources[configSource as keyof typeof ConfigSources];
+ } else {
+ throw Error("invalid config source");
+ }
+ } else {
+ cs = configSource;
+ }
+ const cfg = new Configuration(cs);
cfg.loadDefaults();
if (filename) {
- cfg.loadFromFilename(filename);
+ cfg.loadFromFilename(filename, false);
+ cfg.hintEntrypoint = filename;
} else {
- const fn = cfg.getDefaultConfigFilename();
+ const fn = cfg.findDefaultConfigFilename();
if (fn) {
- cfg.loadFromFilename(fn);
+ // It's the default filename for the main config file,
+ // but we don't consider the values default values.
+ cfg.loadFromFilename(fn, false);
+ cfg.hintEntrypoint = fn;
}
}
- cfg.hintEntrypoint = filename;
return cfg;
}
stringify(opts: StringifyOptions = {}): string {
+ if (opts.excludeDefaults && this.entrypointIsComplex) {
+ throw Error(
+ "unable to do diff serialization of config file, as entry point contains complex directives",
+ );
+ }
let s = "";
if (opts.diagnostics) {
s += "# Configuration file diagnostics\n";
@@ -657,26 +861,64 @@ export class Configuration {
}
for (const sectionName of Object.keys(this.sectionMap)) {
const sec = this.sectionMap[sectionName];
- if (opts.diagnostics && sec.secretFilename) {
- s += `# Secret section from ${sec.secretFilename}\n`;
- s += `# Secret accessible: ${!sec.inaccessible}\n`;
- }
- s += `[${sectionName}]\n`;
+ let headerWritten = false;
for (const optionName of Object.keys(sec.entries)) {
const entry = this.sectionMap[sectionName].entries[optionName];
+ if (
+ opts.excludeDefaults &&
+ (entry.origin === EntryOrigin.DefaultSystem ||
+ entry.origin === EntryOrigin.DefaultFile)
+ ) {
+ continue;
+ }
+ if (!headerWritten) {
+ if (opts.diagnostics && sec.secretFilename) {
+ s += `# Secret section from ${sec.secretFilename}\n`;
+ s += `# Secret accessible: ${!sec.inaccessible}\n`;
+ }
+ s += `[${sectionName}]\n`;
+ headerWritten = true;
+ }
if (entry !== undefined) {
if (opts.diagnostics) {
- s += `# ${entry.sourceFile}:${entry.sourceLine}\n`;
+ switch (entry.origin) {
+ case EntryOrigin.DefaultFile:
+ case EntryOrigin.Changed:
+ case EntryOrigin.Loaded:
+ s += `# ${entry.sourceFile}:${entry.sourceLine}\n`;
+ break;
+ case EntryOrigin.DefaultSystem:
+ s += `# (system/installation default)\n`;
+ break;
+ }
}
s += `${optionName} = ${entry.value}\n`;
}
}
- s += "\n";
+ if (headerWritten) {
+ s += "\n";
+ }
}
return s;
}
- write(filename: string): void {
- nodejs_fs.writeFileSync(filename, this.stringify());
+ write(opts: { excludeDefaults?: boolean } = {}): void {
+ const filename = this.hintEntrypoint;
+ if (!filename) {
+ throw Error(
+ "unknown configuration entrypoing, unable to write back config file",
+ );
+ }
+ nodejs_fs.writeFileSync(
+ filename,
+ this.stringify({ excludeDefaults: opts.excludeDefaults }),
+ );
+ }
+
+ writeTo(filename: string, opts: { excludeDefaults?: boolean } = {}): void {
+ nodejs_fs.writeFileSync(
+ filename,
+ this.stringify({ excludeDefaults: opts.excludeDefaults }),
+ );
}
}
diff --git a/packages/taler-util/src/taleruri.test.ts b/packages/taler-util/src/taleruri.test.ts
index 3ee243fb3..b92366fb3 100644
--- a/packages/taler-util/src/taleruri.test.ts
+++ b/packages/taler-util/src/taleruri.test.ts
@@ -15,25 +15,67 @@
*/
import test from "ava";
+import { AmountString } from "./taler-types.js";
import {
+ parseAddExchangeUri,
+ parseDevExperimentUri,
+ parsePayPullUri,
+ parsePayPushUri,
+ parsePayTemplateUri,
parsePayUri,
- parseWithdrawUri,
parseRefundUri,
- parseTipUri,
- parsePayPushUri,
- constructPayPushUri,
+ parseRestoreUri,
+ parseWithdrawExchangeUri,
+ parseWithdrawUri,
+ stringifyAddExchange,
+ stringifyDevExperimentUri,
+ stringifyPayPullUri,
+ stringifyPayPushUri,
+ stringifyPayTemplateUri,
+ stringifyPayUri,
+ stringifyRefundUri,
+ stringifyRestoreUri,
+ stringifyWithdrawExchange,
+ stringifyWithdrawUri,
} from "./taleruri.js";
-test("taler pay url parsing: wrong scheme", (t) => {
- const url1 = "talerfoo://";
- const r1 = parsePayUri(url1);
- t.is(r1, undefined);
+/**
+ * 5.1 action: withdraw https://lsd.gnunet.org/lsd0006/#name-action-withdraw
+ */
- const url2 = "taler://refund/a/b/c/d/e/f";
- const r2 = parsePayUri(url2);
- t.is(r2, undefined);
+test("taler withdraw uri parsing", (t) => {
+ const url1 = "taler://withdraw/bank.example.com/12345";
+ const r1 = parseWithdrawUri(url1);
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.is(r1.withdrawalOperationId, "12345");
+ t.is(r1.bankIntegrationApiBaseUrl, "https://bank.example.com/");
});
+test("taler withdraw uri parsing (http)", (t) => {
+ const url1 = "taler+http://withdraw/bank.example.com/12345";
+ const r1 = parseWithdrawUri(url1);
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.is(r1.withdrawalOperationId, "12345");
+ t.is(r1.bankIntegrationApiBaseUrl, "http://bank.example.com/");
+});
+
+test("taler withdraw URI (stringify)", (t) => {
+ const url = stringifyWithdrawUri({
+ bankIntegrationApiBaseUrl: "https://bank.taler.test/integration-api/",
+ withdrawalOperationId: "123",
+ });
+ t.deepEqual(url, "taler://withdraw/bank.taler.test/integration-api/123");
+});
+
+/**
+ * 5.2 action: pay https://lsd.gnunet.org/lsd0006/#name-action-pay
+ */
test("taler pay url parsing: defaults", (t) => {
const url1 = "taler://pay/example.com/myorder/";
const r1 = parsePayUri(url1);
@@ -77,17 +119,6 @@ test("taler pay url parsing (claim token)", (t) => {
t.is(r1.claimToken, "ASDF");
});
-test("taler refund uri parsing: non-https #1", (t) => {
- const url1 = "taler+http://refund/example.com/myorder/";
- const r1 = parseRefundUri(url1);
- if (!r1) {
- t.fail();
- return;
- }
- t.is(r1.merchantBaseUrl, "http://example.com/");
- t.is(r1.orderId, "myorder");
-});
-
test("taler pay uri parsing: non-https", (t) => {
const url1 = "taler+http://pay/example.com/myorder/";
const r1 = parsePayUri(url1);
@@ -109,26 +140,52 @@ test("taler pay uri parsing: missing session component", (t) => {
t.pass();
});
-test("taler withdraw uri parsing", (t) => {
- const url1 = "taler://withdraw/bank.example.com/12345";
- const r1 = parseWithdrawUri(url1);
- if (!r1) {
- t.fail();
- return;
- }
- t.is(r1.withdrawalOperationId, "12345");
- t.is(r1.bankIntegrationApiBaseUrl, "https://bank.example.com/");
+test("taler pay URI (stringify)", (t) => {
+ const url1 = stringifyPayUri({
+ merchantBaseUrl: "http://localhost:123/",
+ orderId: "foo",
+ sessionId: "",
+ });
+ t.deepEqual(url1, "taler+http://pay/localhost:123/foo/");
+
+ const url2 = stringifyPayUri({
+ merchantBaseUrl: "http://localhost:123/",
+ orderId: "foo",
+ sessionId: "bla",
+ });
+ t.deepEqual(url2, "taler+http://pay/localhost:123/foo/bla");
});
-test("taler withdraw uri parsing (http)", (t) => {
- const url1 = "taler+http://withdraw/bank.example.com/12345";
- const r1 = parseWithdrawUri(url1);
+test("taler pay URI (stringify with https)", (t) => {
+ const url1 = stringifyPayUri({
+ merchantBaseUrl: "https://localhost:123/",
+ orderId: "foo",
+ sessionId: "",
+ });
+ t.deepEqual(url1, "taler://pay/localhost:123/foo/");
+
+ const url2 = stringifyPayUri({
+ merchantBaseUrl: "https://localhost/",
+ orderId: "foo",
+ sessionId: "bla",
+ noncePriv: "123",
+ });
+ t.deepEqual(url2, "taler://pay/localhost/foo/bla?n=123");
+});
+
+/**
+ * 5.3 action: refund https://lsd.gnunet.org/lsd0006/#name-action-refund
+ */
+
+test("taler refund uri parsing: non-https #1", (t) => {
+ const url1 = "taler+http://refund/example.com/myorder/";
+ const r1 = parseRefundUri(url1);
if (!r1) {
t.fail();
return;
}
- t.is(r1.withdrawalOperationId, "12345");
- t.is(r1.bankIntegrationApiBaseUrl, "http://bank.example.com/");
+ t.is(r1.merchantBaseUrl, "http://example.com/");
+ t.is(r1.orderId, "myorder");
});
test("taler refund uri parsing", (t) => {
@@ -153,41 +210,66 @@ test("taler refund uri parsing with instance", (t) => {
t.is(r1.merchantBaseUrl, "https://merchant.example.com/instances/myinst/");
});
-test("taler tip pickup uri", (t) => {
- const url1 = "taler://tip/merchant.example.com/tipid";
- const r1 = parseTipUri(url1);
+test("taler refund URI (stringify)", (t) => {
+ const url = stringifyRefundUri({
+ merchantBaseUrl: "https://merchant.test/instance/pepe/",
+ orderId: "123",
+ });
+ t.deepEqual(url, "taler://refund/merchant.test/instance/pepe/123/");
+});
+
+/**
+ * 5.5 action: pay-push https://lsd.gnunet.org/lsd0006/#name-action-pay-push
+ */
+
+test("taler peer to peer push URI", (t) => {
+ const url1 = "taler://pay-push/exch.example.com/foo";
+ const r1 = parsePayPushUri(url1);
if (!r1) {
t.fail();
return;
}
- t.is(r1.merchantBaseUrl, "https://merchant.example.com/");
+ t.is(r1.exchangeBaseUrl, "https://exch.example.com/");
+ t.is(r1.contractPriv, "foo");
});
-test("taler tip pickup uri with instance", (t) => {
- const url1 = "taler://tip/merchant.example.com/instances/tipm/tipid";
- const r1 = parseTipUri(url1);
+test("taler peer to peer push URI (path)", (t) => {
+ const url1 = "taler://pay-push/exch.example.com:123/bla/foo";
+ const r1 = parsePayPushUri(url1);
if (!r1) {
t.fail();
return;
}
- t.is(r1.merchantBaseUrl, "https://merchant.example.com/instances/tipm/");
- t.is(r1.merchantTipId, "tipid");
+ t.is(r1.exchangeBaseUrl, "https://exch.example.com:123/bla/");
+ t.is(r1.contractPriv, "foo");
});
-test("taler tip pickup uri with instance and prefix", (t) => {
- const url1 = "taler://tip/merchant.example.com/my/pfx/tipm/tipid";
- const r1 = parseTipUri(url1);
+test("taler peer to peer push URI (http)", (t) => {
+ const url1 = "taler+http://pay-push/exch.example.com:123/bla/foo";
+ const r1 = parsePayPushUri(url1);
if (!r1) {
t.fail();
return;
}
- t.is(r1.merchantBaseUrl, "https://merchant.example.com/my/pfx/tipm/");
- t.is(r1.merchantTipId, "tipid");
+ t.is(r1.exchangeBaseUrl, "http://exch.example.com:123/bla/");
+ t.is(r1.contractPriv, "foo");
});
-test("taler peer to peer push URI", (t) => {
- const url1 = "taler://pay-push/exch.example.com/foo";
- const r1 = parsePayPushUri(url1);
+test("taler peer to peer push URI (stringify)", (t) => {
+ const url = stringifyPayPushUri({
+ exchangeBaseUrl: "https://foo.example.com/bla/",
+ contractPriv: "123",
+ });
+ t.deepEqual(url, "taler://pay-push/foo.example.com/bla/123");
+});
+
+/**
+ * 5.6 action: pay-pull https://lsd.gnunet.org/lsd0006/#name-action-pay-pull
+ */
+
+test("taler peer to peer pull URI", (t) => {
+ const url1 = "taler://pay-pull/exch.example.com/foo";
+ const r1 = parsePayPullUri(url1);
if (!r1) {
t.fail();
return;
@@ -196,9 +278,9 @@ test("taler peer to peer push URI", (t) => {
t.is(r1.contractPriv, "foo");
});
-test("taler peer to peer push URI (path)", (t) => {
- const url1 = "taler://pay-push/exch.example.com:123/bla/foo";
- const r1 = parsePayPushUri(url1);
+test("taler peer to peer pull URI (path)", (t) => {
+ const url1 = "taler://pay-pull/exch.example.com:123/bla/foo";
+ const r1 = parsePayPullUri(url1);
if (!r1) {
t.fail();
return;
@@ -207,9 +289,9 @@ test("taler peer to peer push URI (path)", (t) => {
t.is(r1.contractPriv, "foo");
});
-test("taler peer to peer push URI (http)", (t) => {
- const url1 = "taler+http://pay-push/exch.example.com:123/bla/foo";
- const r1 = parsePayPushUri(url1);
+test("taler peer to peer pull URI (http)", (t) => {
+ const url1 = "taler+http://pay-pull/exch.example.com:123/bla/foo";
+ const r1 = parsePayPullUri(url1);
if (!r1) {
t.fail();
return;
@@ -218,10 +300,247 @@ test("taler peer to peer push URI (http)", (t) => {
t.is(r1.contractPriv, "foo");
});
-test("taler peer to peer push URI (construction)", (t) => {
- const url = constructPayPushUri({
+test("taler peer to peer pull URI (stringify)", (t) => {
+ const url = stringifyPayPullUri({
exchangeBaseUrl: "https://foo.example.com/bla/",
contractPriv: "123",
});
- t.deepEqual(url, "taler://pay-push/foo.example.com/bla/123");
+ t.deepEqual(url, "taler://pay-pull/foo.example.com/bla/123");
+});
+
+/**
+ * 5.7 action: pay-template https://lsd.gnunet.org/lsd0006/#name-action-pay-template
+ */
+
+test("taler pay template URI (parsing)", (t) => {
+ const url1 =
+ "taler://pay-template/merchant.example.com/FEGHYJY48FEGU6WETYIOIDEDE2QW3OCZVY";
+ const r1 = parsePayTemplateUri(url1);
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.deepEqual(r1.merchantBaseUrl, "https://merchant.example.com/");
+ t.deepEqual(r1.templateId, "FEGHYJY48FEGU6WETYIOIDEDE2QW3OCZVY");
+});
+
+test("taler pay template URI (parsing, http with port)", (t) => {
+ const url1 =
+ "taler+http://pay-template/merchant.example.com:1234/FEGHYJY48FEGU6WETYIOIDEDE2QW3OCZVY";
+ const r1 = parsePayTemplateUri(url1);
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.deepEqual(r1.merchantBaseUrl, "http://merchant.example.com:1234/");
+ t.deepEqual(r1.templateId, "FEGHYJY48FEGU6WETYIOIDEDE2QW3OCZVY");
+});
+
+test("taler pay template URI (stringify)", (t) => {
+ const url1 = stringifyPayTemplateUri({
+ merchantBaseUrl: "http://merchant.example.com:1234/",
+ templateId: "FEGHYJY48FEGU6WETYIOIDEDE2QW3OCZVY",
+ });
+ t.deepEqual(
+ url1,
+ "taler+http://pay-template/merchant.example.com:1234/FEGHYJY48FEGU6WETYIOIDEDE2QW3OCZVY",
+ );
+});
+
+/**
+ * 5.10 action: restore https://lsd.gnunet.org/lsd0006/#name-action-restore
+ */
+test("taler restore URI (parsing, http with port)", (t) => {
+ const r1 = parseRestoreUri(
+ "taler+http://restore/GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0/prov1.example.com,prov2.example.com:123",
+ );
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.deepEqual(
+ r1.walletRootPriv,
+ "GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0",
+ );
+ t.deepEqual(r1.providers[0], "http://prov1.example.com/");
+ t.deepEqual(r1.providers[1], "http://prov2.example.com:123/");
+});
+test("taler restore URI (parsing, https with port)", (t) => {
+ const r1 = parseRestoreUri(
+ "taler://restore/GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0/prov1.example.com,prov2.example.com:234,https%3A%2F%2Fprov1.example.com%2F",
+ );
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.deepEqual(
+ r1.walletRootPriv,
+ "GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0",
+ );
+ t.deepEqual(r1.providers[0], "https://prov1.example.com/");
+ t.deepEqual(r1.providers[1], "https://prov2.example.com:234/");
+});
+
+test("taler restore URI (stringify)", (t) => {
+ const url = stringifyRestoreUri({
+ walletRootPriv: "GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0",
+ providers: ["http://prov1.example.com", "https://prov2.example.com:234/"],
+ });
+ t.deepEqual(
+ url,
+ "taler://restore/GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0/http%3A%2F%2Fprov1.example.com%2F,https%3A%2F%2Fprov2.example.com%3A234%2F",
+ );
+});
+
+/**
+ * 5.11 action: dev-experiment https://lsd.gnunet.org/lsd0006/#name-action-dev-experiment
+ */
+
+test("taler dev exp URI (parsing)", (t) => {
+ const url1 = "taler://dev-experiment/123";
+ const r1 = parseDevExperimentUri(url1);
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.deepEqual(r1.devExperimentId, "123");
+});
+
+test("taler dev exp URI (stringify)", (t) => {
+ const url1 = stringifyDevExperimentUri({
+ devExperimentId: "123",
+ });
+ t.deepEqual(url1, "taler://dev-experiment/123");
+});
+
+/**
+ * 5.12 action: withdraw-exchange https://lsd.gnunet.org/lsd0006/#name-action-withdraw-exchange
+ */
+
+test("taler withdraw exchange URI (parse)", (t) => {
+ // Pubkey has been phased out, may no longer be specified.
+ {
+ const rx1 = parseWithdrawExchangeUri(
+ "taler://withdraw-exchange/exchange.demo.taler.net/someroot/GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0?a=KUDOS%3A2",
+ );
+ if (rx1) {
+ t.fail();
+ return;
+ }
+ }
+ {
+ const rx2 = parseWithdrawExchangeUri(
+ "taler://withdraw-exchange/exchange.demo.taler.net/GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0",
+ );
+ if (rx2) {
+ t.fail();
+ return;
+ }
+ }
+
+ // Now test well-formed URIs
+ {
+ const r2 = parseWithdrawExchangeUri(
+ "taler://withdraw-exchange/exchange.demo.taler.net/someroot/",
+ );
+ if (!r2) {
+ t.fail();
+ return;
+ }
+ t.deepEqual(r2.amount, undefined);
+ t.deepEqual(
+ r2.exchangeBaseUrl,
+ "https://exchange.demo.taler.net/someroot/",
+ );
+ }
+
+ {
+ const r3 = parseWithdrawExchangeUri(
+ "taler://withdraw-exchange/exchange.demo.taler.net/",
+ );
+ if (!r3) {
+ t.fail();
+ return;
+ }
+ t.deepEqual(r3.amount, undefined);
+ t.deepEqual(r3.exchangeBaseUrl, "https://exchange.demo.taler.net/");
+ }
+
+ {
+ // No trailing slash, no path component
+ const r4 = parseWithdrawExchangeUri(
+ "taler://withdraw-exchange/exchange.demo.taler.net",
+ );
+ if (!r4) {
+ t.fail();
+ return;
+ }
+ t.deepEqual(r4.amount, undefined);
+ t.deepEqual(r4.exchangeBaseUrl, "https://exchange.demo.taler.net/");
+ }
+});
+
+test("taler withdraw exchange URI (stringify)", (t) => {
+ const url = stringifyWithdrawExchange({
+ exchangeBaseUrl: "https://exchange.demo.taler.net",
+ });
+ t.deepEqual(url, "taler://withdraw-exchange/exchange.demo.taler.net/");
+});
+
+test("taler withdraw exchange URI with amount (stringify)", (t) => {
+ const url = stringifyWithdrawExchange({
+ exchangeBaseUrl: "https://exchange.demo.taler.net",
+ amount: "KUDOS:19" as AmountString,
+ });
+ t.deepEqual(
+ url,
+ "taler://withdraw-exchange/exchange.demo.taler.net/?a=KUDOS%3A19",
+ );
+});
+
+/**
+ * 5.13 action: add-exchange https://lsd.gnunet.org/lsd0006/#name-action-add-exchange
+ */
+
+test("taler add exchange URI (parse)", (t) => {
+ {
+ const r1 = parseAddExchangeUri(
+ "taler://add-exchange/exchange.example.com/",
+ );
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.deepEqual(r1.exchangeBaseUrl, "https://exchange.example.com/");
+ }
+ {
+ const r2 = parseAddExchangeUri(
+ "taler://add-exchange/exchanges.example.com/api/",
+ );
+ if (!r2) {
+ t.fail();
+ return;
+ }
+ t.deepEqual(r2.exchangeBaseUrl, "https://exchanges.example.com/api/");
+ }
+});
+
+test("taler add exchange URI (stringify)", (t) => {
+ const url = stringifyAddExchange({
+ exchangeBaseUrl: "https://exchange.demo.taler.net",
+ });
+ t.deepEqual(url, "taler://add-exchange/exchange.demo.taler.net/");
+});
+
+/**
+ * wrong uris
+ */
+test("taler pay url parsing: wrong scheme", (t) => {
+ const url1 = "talerfoo://";
+ const r1 = parsePayUri(url1);
+ t.is(r1, undefined);
+
+ const url2 = "taler://refund/a/b/c/d/e/f";
+ const r2 = parsePayUri(url2);
+ t.is(r2, undefined);
});
diff --git a/packages/taler-util/src/taleruri.ts b/packages/taler-util/src/taleruri.ts
index 4e47acbce..54b7525e3 100644
--- a/packages/taler-util/src/taleruri.ts
+++ b/packages/taler-util/src/taleruri.ts
@@ -14,61 +14,138 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { BackupRecovery } from "./backup-types.js";
+/**
+ * @fileoverview
+ * Construction and parsing of taler:// URIs.
+ * Specification: https://lsd.gnunet.org/lsd0006/
+ */
+
+/**
+ * Imports.
+ */
+import { Codec, Context, DecodingError, renderContext } from "./codec.js";
import { canonicalizeBaseUrl } from "./helpers.js";
-import { initNodePrng } from "./prng-node.js";
-import { URLSearchParams, URL } from "./url.js";
+import { opFixedSuccess, opKnownTalerFailure } from "./operation.js";
+import { TalerErrorCode } from "./taler-error-codes.js";
+import { AmountString } from "./taler-types.js";
+import { URL, URLSearchParams } from "./url.js";
+/**
+ * A parsed taler URI.
+ */
+export type TalerUri =
+ | PayUriResult
+ | PayTemplateUriResult
+ | DevExperimentUri
+ | PayPullUriResult
+ | PayPushUriResult
+ | BackupRestoreUri
+ | RefundUriResult
+ | WithdrawUriResult
+ | WithdrawExchangeUri
+ | AddExchangeUri;
+
+declare const __action_str: unique symbol;
+export type TalerUriString = string & { [__action_str]: true };
+
+export function codecForTalerUriString(): Codec<TalerUriString> {
+ return {
+ decode(x: any, c?: Context): TalerUriString {
+ if (typeof x !== "string") {
+ throw new DecodingError(
+ `expected string at ${renderContext(c)} but got ${typeof x}`,
+ );
+ }
+ if (parseTalerUri(x) === undefined) {
+ throw new DecodingError(
+ `invalid taler URI at ${renderContext(c)} but got "${x}"`,
+ );
+ }
+ return x as TalerUriString;
+ },
+ };
+}
export interface PayUriResult {
+ type: TalerUriAction.Pay;
merchantBaseUrl: string;
orderId: string;
sessionId: string;
- claimToken: string | undefined;
- noncePriv: string | undefined;
+ claimToken?: string;
+ noncePriv?: string;
+}
+
+export type TemplateParams = {
+ amount?: string;
+ summary?: string;
+};
+
+export interface PayTemplateUriResult {
+ type: TalerUriAction.PayTemplate;
+ merchantBaseUrl: string;
+ templateId: string;
}
export interface WithdrawUriResult {
+ type: TalerUriAction.Withdraw;
bankIntegrationApiBaseUrl: string;
withdrawalOperationId: string;
}
export interface RefundUriResult {
+ type: TalerUriAction.Refund;
merchantBaseUrl: string;
orderId: string;
}
-export interface TipUriResult {
- merchantTipId: string;
- merchantBaseUrl: string;
-}
-
export interface PayPushUriResult {
+ type: TalerUriAction.PayPush;
exchangeBaseUrl: string;
contractPriv: string;
}
export interface PayPullUriResult {
+ type: TalerUriAction.PayPull;
exchangeBaseUrl: string;
contractPriv: string;
}
export interface DevExperimentUri {
+ type: TalerUriAction.DevExperiment;
devExperimentId: string;
}
+export interface BackupRestoreUri {
+ type: TalerUriAction.Restore;
+ walletRootPriv: string;
+ providers: Array<string>;
+}
+
+export interface WithdrawExchangeUri {
+ type: TalerUriAction.WithdrawExchange;
+ exchangeBaseUrl: string;
+ amount?: AmountString;
+}
+
+export interface AddExchangeUri {
+ type: TalerUriAction.AddExchange;
+ exchangeBaseUrl: string;
+}
+
/**
* Parse a taler[+http]://withdraw URI.
* Return undefined if not passed a valid URI.
*/
-export function parseWithdrawUri(s: string): WithdrawUriResult | undefined {
- const pi = parseProtoInfo(s, "withdraw");
- if (!pi) {
- return undefined;
+export function parseWithdrawUriWithError(s: string) {
+ const pi = parseProtoInfoWithError(s, "withdraw");
+ if (pi.type === "fail") {
+ return pi;
}
- const parts = pi.rest.split("/");
+ const parts = pi.body.rest.split("/");
if (parts.length < 2) {
- return undefined;
+ return opKnownTalerFailure(TalerErrorCode.WALLET_TALER_URI_MALFORMED, {
+ code: TalerErrorCode.WALLET_TALER_URI_MALFORMED,
+ });
}
const host = parts[0].toLowerCase();
@@ -83,14 +160,78 @@ export function parseWithdrawUri(s: string): WithdrawUriResult | undefined {
const withdrawId = parts[parts.length - 1];
const p = [host, ...pathSegments].join("/");
- return {
- bankIntegrationApiBaseUrl: canonicalizeBaseUrl(`${pi.innerProto}://${p}/`),
+ const result: WithdrawUriResult = {
+ type: TalerUriAction.Withdraw,
+ bankIntegrationApiBaseUrl: canonicalizeBaseUrl(
+ `${pi.body.innerProto}://${p}/`,
+ ),
withdrawalOperationId: withdrawId,
};
+ return opFixedSuccess(result);
+}
+
+/**
+ *
+ * @deprecated use parseWithdrawUriWithError
+ */
+export function parseWithdrawUri(s: string): WithdrawUriResult | undefined {
+ const r = parseWithdrawUriWithError(s);
+ if (r.type === "fail") return undefined;
+ return r.body;
}
+/**
+ * Parse a taler[+http]://withdraw URI.
+ * Return undefined if not passed a valid URI.
+ */
+export function parseAddExchangeUriWithError(s: string) {
+ const pi = parseProtoInfoWithError(s, "add-exchange");
+ if (pi.type === "fail") {
+ return pi;
+ }
+ const parts = pi.body.rest.split("/");
+
+ if (parts.length < 2) {
+ return opKnownTalerFailure(TalerErrorCode.WALLET_TALER_URI_MALFORMED, {
+ code: TalerErrorCode.WALLET_TALER_URI_MALFORMED,
+ });
+ }
+
+ const host = parts[0].toLowerCase();
+ const pathSegments = parts.slice(1, parts.length - 1);
+ /**
+ * The statement below does not tolerate a slash-ended URI.
+ * This results in (1) the withdrawalId being passed as the
+ * empty string, and (2) the bankIntegrationApi ending with the
+ * actual withdrawal operation ID. That can be fixed by
+ * trimming the parts-list. FIXME
+ */
+ const p = [host, ...pathSegments].join("/");
+
+ const result: AddExchangeUri = {
+ type: TalerUriAction.AddExchange,
+ exchangeBaseUrl: canonicalizeBaseUrl(`${pi.body.innerProto}://${p}/`),
+ };
+ return opFixedSuccess(result);
+}
+
+/**
+ *
+ * @deprecated use parseWithdrawUriWithError
+ */
+export function parseAddExchangeUri(s: string): AddExchangeUri | undefined {
+ const r = parseAddExchangeUriWithError(s);
+ if (r.type === "fail") return undefined;
+ return r.body;
+}
+
+/**
+ * @deprecated use TalerUriAction
+ */
export enum TalerUriType {
TalerPay = "taler-pay",
+ TalerTemplate = "taler-template",
+ TalerPayTemplate = "taler-pay-template",
TalerWithdraw = "taler-withdraw",
TalerTip = "taler-tip",
TalerRefund = "taler-refund",
@@ -101,60 +242,17 @@ export enum TalerUriType {
Unknown = "unknown",
}
-const talerActionPayPull = "pay-pull";
-const talerActionPayPush = "pay-push";
-
-/**
- * Classify a taler:// URI.
- */
-export function classifyTalerUri(s: string): TalerUriType {
- const sl = s.toLowerCase();
- if (sl.startsWith("taler://recovery/")) {
- return TalerUriType.TalerRecovery;
- }
- if (sl.startsWith("taler+http://recovery/")) {
- return TalerUriType.TalerRecovery;
- }
- if (sl.startsWith("taler://pay/")) {
- return TalerUriType.TalerPay;
- }
- if (sl.startsWith("taler+http://pay/")) {
- return TalerUriType.TalerPay;
- }
- if (sl.startsWith("taler://tip/")) {
- return TalerUriType.TalerTip;
- }
- if (sl.startsWith("taler+http://tip/")) {
- return TalerUriType.TalerTip;
- }
- if (sl.startsWith("taler://refund/")) {
- return TalerUriType.TalerRefund;
- }
- if (sl.startsWith("taler+http://refund/")) {
- return TalerUriType.TalerRefund;
- }
- if (sl.startsWith("taler://withdraw/")) {
- return TalerUriType.TalerWithdraw;
- }
- if (sl.startsWith("taler+http://withdraw/")) {
- return TalerUriType.TalerWithdraw;
- }
- if (sl.startsWith(`taler://${talerActionPayPush}/`)) {
- return TalerUriType.TalerPayPush;
- }
- if (sl.startsWith(`taler+http://${talerActionPayPush}/`)) {
- return TalerUriType.TalerPayPush;
- }
- if (sl.startsWith(`taler://${talerActionPayPull}/`)) {
- return TalerUriType.TalerPayPull;
- }
- if (sl.startsWith(`taler+http://${talerActionPayPull}/`)) {
- return TalerUriType.TalerPayPull;
- }
- if (sl.startsWith("taler://dev-experiment/")) {
- return TalerUriType.TalerDevExperiment;
- }
- return TalerUriType.Unknown;
+export enum TalerUriAction {
+ Pay = "pay",
+ Withdraw = "withdraw",
+ Refund = "refund",
+ PayPull = "pay-pull",
+ PayPush = "pay-push",
+ PayTemplate = "pay-template",
+ Restore = "restore",
+ DevExperiment = "dev-experiment",
+ WithdrawExchange = "withdraw-exchange",
+ AddExchange = "add-exchange",
}
interface TalerUriProtoInfo {
@@ -183,6 +281,95 @@ function parseProtoInfo(
}
}
+function parseProtoInfoWithError(s: string, action: string) {
+ if (
+ !s.toLowerCase().startsWith("taler://") &&
+ !s.toLowerCase().startsWith("taler+http://")
+ ) {
+ return opKnownTalerFailure(TalerErrorCode.WALLET_TALER_URI_MALFORMED, {
+ code: TalerErrorCode.WALLET_TALER_URI_MALFORMED,
+ });
+ }
+ const pfxPlain = `taler://${action}/`;
+ const pfxHttp = `taler+http://${action}/`;
+ if (s.toLowerCase().startsWith(pfxPlain)) {
+ return opFixedSuccess({
+ innerProto: "https",
+ rest: s.substring(pfxPlain.length),
+ });
+ } else if (s.toLowerCase().startsWith(pfxHttp)) {
+ return opFixedSuccess({
+ innerProto: "http",
+ rest: s.substring(pfxHttp.length),
+ });
+ } else {
+ return opKnownTalerFailure(TalerErrorCode.WALLET_TALER_URI_MALFORMED, {
+ code: TalerErrorCode.WALLET_TALER_URI_MALFORMED,
+ });
+ }
+}
+
+type Parser = (s: string) => TalerUri | undefined;
+const parsers: { [A in TalerUriAction]: Parser } = {
+ [TalerUriAction.Pay]: parsePayUri,
+ [TalerUriAction.PayPull]: parsePayPullUri,
+ [TalerUriAction.PayPush]: parsePayPushUri,
+ [TalerUriAction.PayTemplate]: parsePayTemplateUri,
+ [TalerUriAction.Restore]: parseRestoreUri,
+ [TalerUriAction.Refund]: parseRefundUri,
+ [TalerUriAction.Withdraw]: parseWithdrawUri,
+ [TalerUriAction.DevExperiment]: parseDevExperimentUri,
+ [TalerUriAction.WithdrawExchange]: parseWithdrawExchangeUri,
+ [TalerUriAction.AddExchange]: parseAddExchangeUri,
+};
+
+export function parseTalerUri(string: string): TalerUri | undefined {
+ const https = string.startsWith("taler://");
+ const http = string.startsWith("taler+http://");
+ if (!https && !http) return undefined;
+ const actionStart = https ? 8 : 13;
+ const actionEnd = string.indexOf("/", actionStart + 1);
+ const action = string.substring(actionStart, actionEnd);
+ const found = Object.values(TalerUriAction).find((x) => x === action);
+ if (!found) return undefined;
+ return parsers[found](string);
+}
+
+export function stringifyTalerUri(uri: TalerUri): string {
+ switch (uri.type) {
+ case TalerUriAction.DevExperiment: {
+ return stringifyDevExperimentUri(uri);
+ }
+ case TalerUriAction.Pay: {
+ return stringifyPayUri(uri);
+ }
+ case TalerUriAction.PayPull: {
+ return stringifyPayPullUri(uri);
+ }
+ case TalerUriAction.PayPush: {
+ return stringifyPayPushUri(uri);
+ }
+ case TalerUriAction.PayTemplate: {
+ return stringifyPayTemplateUri(uri);
+ }
+ case TalerUriAction.Restore: {
+ return stringifyRestoreUri(uri);
+ }
+ case TalerUriAction.Refund: {
+ return stringifyRefundUri(uri);
+ }
+ case TalerUriAction.Withdraw: {
+ return stringifyWithdrawUri(uri);
+ }
+ case TalerUriAction.WithdrawExchange: {
+ return stringifyWithdrawExchange(uri);
+ }
+ case TalerUriAction.AddExchange: {
+ return stringifyAddExchange(uri);
+ }
+ }
+}
+
/**
* Parse a taler[+http]://pay URI.
* Return undefined if not passed a valid URI.
@@ -208,33 +395,52 @@ export function parsePayUri(s: string): PayUriResult | undefined {
const merchantBaseUrl = canonicalizeBaseUrl(`${pi.innerProto}://${p}/`);
return {
+ type: TalerUriAction.Pay,
merchantBaseUrl,
orderId,
- sessionId: sessionId,
+ sessionId,
claimToken,
noncePriv,
};
}
-export function constructPayUri(
- merchantBaseUrl: string,
- orderId: string,
- sessionId: string,
- claimToken?: string,
- noncePriv?: string,
-): string {
- const base = canonicalizeBaseUrl(merchantBaseUrl);
- const url = new URL(base);
- const isHttp = base.startsWith("http://");
- let result = isHttp ? `taler+http://pay/` : `taler://pay/`;
- result += `${url.hostname}${url.pathname}${orderId}/${sessionId}?`;
- if (claimToken) result += `c=${claimToken}`;
- if (noncePriv) result += `n=${noncePriv}`;
- return result;
+export function parsePayTemplateUri(
+ uriString: string,
+): PayTemplateUriResult | undefined {
+ const pi = parseProtoInfo(uriString, TalerUriAction.PayTemplate);
+ if (!pi) {
+ return undefined;
+ }
+ const c = pi.rest.split("?");
+
+ const parts = c[0].split("/");
+ if (parts.length < 2) {
+ return undefined;
+ }
+
+ const q = new URLSearchParams(c[1] ?? "");
+ const params: Record<string, string> = {};
+ q.forEach((v, k) => {
+ params[k] = v;
+ });
+
+ const host = parts[0].toLowerCase();
+ const templateId = parts[parts.length - 1];
+ const pathSegments = parts.slice(1, parts.length - 1);
+ const hostAndSegments = [host, ...pathSegments].join("/");
+ const merchantBaseUrl = canonicalizeBaseUrl(
+ `${pi.innerProto}://${hostAndSegments}/`,
+ );
+
+ return {
+ type: TalerUriAction.PayTemplate,
+ merchantBaseUrl,
+ templateId,
+ };
}
export function parsePayPushUri(s: string): PayPushUriResult | undefined {
- const pi = parseProtoInfo(s, talerActionPayPush);
+ const pi = parseProtoInfo(s, TalerUriAction.PayPush);
if (!pi) {
return undefined;
}
@@ -246,17 +452,20 @@ export function parsePayPushUri(s: string): PayPushUriResult | undefined {
const host = parts[0].toLowerCase();
const contractPriv = parts[parts.length - 1];
const pathSegments = parts.slice(1, parts.length - 1);
- const p = [host, ...pathSegments].join("/");
- const exchangeBaseUrl = canonicalizeBaseUrl(`${pi.innerProto}://${p}/`);
+ const hostAndSegments = [host, ...pathSegments].join("/");
+ const exchangeBaseUrl = canonicalizeBaseUrl(
+ `${pi.innerProto}://${hostAndSegments}/`,
+ );
return {
+ type: TalerUriAction.PayPush,
exchangeBaseUrl,
contractPriv,
};
}
export function parsePayPullUri(s: string): PayPullUriResult | undefined {
- const pi = parseProtoInfo(s, talerActionPayPull);
+ const pi = parseProtoInfo(s, TalerUriAction.PayPull);
if (!pi) {
return undefined;
}
@@ -268,38 +477,51 @@ export function parsePayPullUri(s: string): PayPullUriResult | undefined {
const host = parts[0].toLowerCase();
const contractPriv = parts[parts.length - 1];
const pathSegments = parts.slice(1, parts.length - 1);
- const p = [host, ...pathSegments].join("/");
- const exchangeBaseUrl = canonicalizeBaseUrl(`${pi.innerProto}://${p}/`);
+ const hostAndSegments = [host, ...pathSegments].join("/");
+ const exchangeBaseUrl = canonicalizeBaseUrl(
+ `${pi.innerProto}://${hostAndSegments}/`,
+ );
return {
+ type: TalerUriAction.PayPull,
exchangeBaseUrl,
contractPriv,
};
}
-/**
- * Parse a taler[+http]://tip URI.
- * Return undefined if not passed a valid URI.
- */
-export function parseTipUri(s: string): TipUriResult | undefined {
- const pi = parseProtoInfo(s, "tip");
+export function parseWithdrawExchangeUri(
+ s: string,
+): WithdrawExchangeUri | undefined {
+ const pi = parseProtoInfo(s, "withdraw-exchange");
if (!pi) {
return undefined;
}
const c = pi?.rest.split("?");
const parts = c[0].split("/");
- if (parts.length < 2) {
+ if (parts.length < 1) {
return undefined;
}
const host = parts[0].toLowerCase();
- const tipId = parts[parts.length - 1];
+ // Used to be the reserve public key, now it's empty!
+ const lastPathComponent =
+ parts.length > 1 ? parts[parts.length - 1] : undefined;
+
+ if (lastPathComponent) {
+ // invalid taler://withdraw-exchange URI, must end with a slash
+ return undefined;
+ }
const pathSegments = parts.slice(1, parts.length - 1);
- const p = [host, ...pathSegments].join("/");
- const merchantBaseUrl = canonicalizeBaseUrl(`${pi.innerProto}://${p}/`);
+ const hostAndSegments = [host, ...pathSegments].join("/");
+ const exchangeBaseUrl = canonicalizeBaseUrl(
+ `${pi.innerProto}://${hostAndSegments}/`,
+ );
+ const q = new URLSearchParams(c[1] ?? "");
+ const amount = (q.get("a") ?? undefined) as AmountString | undefined;
return {
- merchantBaseUrl,
- merchantTipId: tipId,
+ type: TalerUriAction.WithdrawExchange,
+ exchangeBaseUrl,
+ amount,
};
}
@@ -321,10 +543,13 @@ export function parseRefundUri(s: string): RefundUriResult | undefined {
const sessionId = parts[parts.length - 1];
const orderId = parts[parts.length - 2];
const pathSegments = parts.slice(1, parts.length - 2);
- const p = [host, ...pathSegments].join("/");
- const merchantBaseUrl = canonicalizeBaseUrl(`${pi.innerProto}://${p}/`);
+ const hostAndSegments = [host, ...pathSegments].join("/");
+ const merchantBaseUrl = canonicalizeBaseUrl(
+ `${pi.innerProto}://${hostAndSegments}/`,
+ );
return {
+ type: TalerUriAction.Refund,
merchantBaseUrl,
orderId,
};
@@ -336,89 +561,170 @@ export function parseDevExperimentUri(s: string): DevExperimentUri | undefined {
if (!c) {
return undefined;
}
- // const q = new URLSearchParams(c[1] ?? "");
const parts = c[0].split("/");
return {
+ type: TalerUriAction.DevExperiment,
devExperimentId: parts[0],
};
}
-export function constructPayPushUri(args: {
- exchangeBaseUrl: string;
- contractPriv: string;
-}): string {
- const url = new URL(args.exchangeBaseUrl);
- let proto: string;
- if (url.protocol === "https:") {
- proto = "taler";
- } else if (url.protocol === "http:") {
- proto = "taler+http";
- } else {
- throw Error(`Unsupported exchange URL protocol ${args.exchangeBaseUrl}`);
+export function parseRestoreUri(uri: string): BackupRestoreUri | undefined {
+ const pi = parseProtoInfo(uri, "restore");
+ if (!pi) {
+ return undefined;
}
- if (!url.pathname.endsWith("/")) {
- throw Error(
- `exchange base URL must end with a slash (got ${args.exchangeBaseUrl}instead)`,
- );
+ const c = pi.rest.split("?");
+ const parts = c[0].split("/");
+ if (parts.length < 2) {
+ return undefined;
}
- return `${proto}://pay-push/${url.host}${url.pathname}${args.contractPriv}`;
+
+ const walletRootPriv = parts[0];
+ if (!walletRootPriv) return undefined;
+ const providers = new Array<string>();
+ parts[1].split(",").map((name) => {
+ const url = canonicalizeBaseUrl(
+ `${pi.innerProto}://${decodeURIComponent(name)}/`,
+ );
+ providers.push(url);
+ });
+ return {
+ type: TalerUriAction.Restore,
+ walletRootPriv,
+ providers,
+ };
}
-export function constructPayPullUri(args: {
- exchangeBaseUrl: string;
- contractPriv: string;
-}): string {
- const url = new URL(args.exchangeBaseUrl);
+// ================================================
+// To string functions
+// ================================================
+
+export function stringifyPayUri({
+ merchantBaseUrl,
+ orderId,
+ sessionId,
+ claimToken,
+ noncePriv,
+}: Omit<PayUriResult, "type">): string {
+ const { proto, path, query } = getUrlInfo(merchantBaseUrl, {
+ c: claimToken,
+ n: noncePriv,
+ });
+ return `${proto}://pay/${path}${orderId}/${sessionId}${query}`;
+}
+
+export function stringifyPayPullUri({
+ contractPriv,
+ exchangeBaseUrl,
+}: Omit<PayPullUriResult, "type">): string {
+ const { proto, path } = getUrlInfo(exchangeBaseUrl);
+ return `${proto}://pay-pull/${path}${contractPriv}`;
+}
+
+export function stringifyPayPushUri({
+ contractPriv,
+ exchangeBaseUrl,
+}: Omit<PayPushUriResult, "type">): string {
+ const { proto, path } = getUrlInfo(exchangeBaseUrl);
+
+ return `${proto}://pay-push/${path}${contractPriv}`;
+}
+
+export function stringifyRestoreUri({
+ providers,
+ walletRootPriv,
+}: Omit<BackupRestoreUri, "type">): string {
+ const list = providers
+ .map((url) => `${encodeURIComponent(new URL(url).href)}`)
+ .join(",");
+ return `taler://restore/${walletRootPriv}/${list}`;
+}
+
+export function stringifyWithdrawExchange({
+ exchangeBaseUrl,
+ amount,
+}: Omit<WithdrawExchangeUri, "type">): string {
+ const { proto, path, query } = getUrlInfo(exchangeBaseUrl, {
+ a: amount,
+ });
+ return `${proto}://withdraw-exchange/${path}${query}`;
+}
+
+export function stringifyAddExchange({
+ exchangeBaseUrl,
+}: Omit<AddExchangeUri, "type">): string {
+ const { proto, path } = getUrlInfo(exchangeBaseUrl);
+ return `${proto}://add-exchange/${path}`;
+}
+
+export function stringifyDevExperimentUri({
+ devExperimentId,
+}: Omit<DevExperimentUri, "type">): string {
+ return `taler://dev-experiment/${devExperimentId}`;
+}
+
+export function stringifyPayTemplateUri({
+ merchantBaseUrl,
+ templateId,
+}: Omit<PayTemplateUriResult, "type">): string {
+ const { proto, path, query } = getUrlInfo(merchantBaseUrl);
+ return `${proto}://pay-template/${path}${templateId}${query}`;
+}
+
+export function stringifyRefundUri({
+ merchantBaseUrl,
+ orderId,
+}: Omit<RefundUriResult, "type">): string {
+ const { proto, path } = getUrlInfo(merchantBaseUrl);
+ return `${proto}://refund/${path}${orderId}/`;
+}
+
+export function stringifyWithdrawUri({
+ bankIntegrationApiBaseUrl,
+ withdrawalOperationId,
+}: Omit<WithdrawUriResult, "type">): string {
+ const { proto, path } = getUrlInfo(bankIntegrationApiBaseUrl);
+ return `${proto}://withdraw/${path}${withdrawalOperationId}`;
+}
+
+/**
+ * Use baseUrl to defined http or https
+ * create path using host+port+pathname
+ * use params to create a query parameter string or empty
+ */
+function getUrlInfo(
+ baseUrl: string,
+ params: Record<string, string | undefined> = {},
+): { proto: string; path: string; query: string } {
+ const url = new URL(baseUrl);
let proto: string;
if (url.protocol === "https:") {
proto = "taler";
} else if (url.protocol === "http:") {
proto = "taler+http";
} else {
- throw Error(`Unsupported exchange URL protocol ${args.exchangeBaseUrl}`);
- }
- if (!url.pathname.endsWith("/")) {
- throw Error(
- `exchange base URL must end with a slash (got ${args.exchangeBaseUrl}instead)`,
- );
- }
- return `${proto}://pay-pull/${url.host}${url.pathname}${args.contractPriv}`;
-}
-
-export function constructRecoveryUri(args: BackupRecovery): string {
- const key = args.walletRootPriv;
- //FIXME: name may contain non valid characters
- const urls = args.providers
- .map((p) => `${p.name}=${canonicalizeBaseUrl(p.url)}`)
- .join("&");
-
- return `taler://recovery/${key}?${urls}`;
-}
-export function parseRecoveryUri(uri: string): BackupRecovery | undefined {
- const pi = parseProtoInfo(uri, "recovery");
- if (!pi) {
- return undefined;
+ throw Error(`Unsupported URL protocol in ${baseUrl}`);
}
- const idx = pi.rest.indexOf("?");
- if (idx === -1) {
- return undefined;
+ let path = url.hostname;
+ if (url.port) {
+ path = path + ":" + url.port;
}
- const path = pi.rest.slice(0, idx);
- const params = pi.rest.slice(idx + 1);
- if (!path || !params) {
- return undefined;
+ if (url.pathname) {
+ path = path + url.pathname;
}
- const parts = path.split("/");
- const walletRootPriv = parts[0];
- if (!walletRootPriv) return undefined;
- const providers = new Array<{ name: string; url: string }>();
- const args = params.split("&");
- for (const param in args) {
- const eq = args[param].indexOf("=");
- if (eq === -1) return undefined;
- const name = args[param].slice(0, eq);
- const url = args[param].slice(eq + 1);
- providers.push({ name, url });
+ if (!path.endsWith("/")) {
+ path = path + "/";
}
- return { walletRootPriv, providers };
+
+ const qp = new URLSearchParams();
+ let withParams = false;
+ Object.entries(params).forEach(([name, value]) => {
+ if (value !== undefined) {
+ withParams = true;
+ qp.append(name, value);
+ }
+ });
+ const query = withParams ? "?" + qp.toString() : "";
+
+ return { proto, path, query };
}
diff --git a/packages/taler-util/src/time.test.ts b/packages/taler-util/src/time.test.ts
new file mode 100644
index 000000000..5dd8c7715
--- /dev/null
+++ b/packages/taler-util/src/time.test.ts
@@ -0,0 +1,39 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 GNUnet e.V.
+
+ 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 test from "ava";
+import { AbsoluteTime, Duration } from "./time.js";
+
+test("duration parsing", (t) => {
+ const d1 = Duration.fromPrettyString("1h");
+ t.deepEqual(d1.d_ms, 60 * 60 * 1000);
+
+ const d2 = Duration.fromPrettyString(" 2h 1s 3m");
+ t.deepEqual(d2.d_ms, 2 * 60 * 60 * 1000 + 3 * 60 * 1000 + 1000);
+
+ t.throws(() => {
+ Duration.fromPrettyString("5g");
+ });
+ t.throws(() => {
+ Duration.fromPrettyString("s");
+ });
+ t.throws(() => {
+ Duration.fromPrettyString("s5");
+ });
+ t.throws(() => {
+ Duration.fromPrettyString("5 5 s");
+ });
+});
diff --git a/packages/taler-util/src/time.ts b/packages/taler-util/src/time.ts
index 6ffabd495..95b4911a0 100644
--- a/packages/taler-util/src/time.ts
+++ b/packages/taler-util/src/time.ts
@@ -21,22 +21,85 @@
/**
* Imports.
*/
-import { Codec, renderContext, Context } from "./codec.js";
+import { Codec, Context, renderContext } from "./codec.js";
+declare const flavor_AbsoluteTime: unique symbol;
+declare const flavor_TalerProtocolTimestamp: unique symbol;
+declare const flavor_TalerPreciseTimestamp: unique symbol;
+
+const opaque_AbsoluteTime: unique symbol = Symbol("opaque_AbsoluteTime");
+
+// FIXME: Make this opaque!
export interface AbsoluteTime {
/**
* Timestamp in milliseconds.
*/
readonly t_ms: number | "never";
+
+ readonly _flavor?: typeof flavor_AbsoluteTime;
+
+ // Make the type opaque, we only want our constructors
+ // to able to create an AbsoluteTime value.
+ [opaque_AbsoluteTime]: true;
}
export interface TalerProtocolTimestamp {
+ /**
+ * Seconds (as integer) since epoch.
+ */
+ readonly t_s: number | "never";
+
+ readonly _flavor?: typeof flavor_TalerProtocolTimestamp;
+}
+
+/**
+ * Precise timestamp, typically used in the wallet-core
+ * API but not in other Taler APIs so far.
+ */
+export interface TalerPreciseTimestamp {
+ /**
+ * Seconds (as integer) since epoch.
+ */
readonly t_s: number | "never";
+
+ /**
+ * Optional microsecond offset (non-negative integer).
+ */
+ readonly off_us?: number;
+
+ readonly _flavor?: typeof flavor_TalerPreciseTimestamp;
+}
+
+export namespace TalerPreciseTimestamp {
+ export function now(): TalerPreciseTimestamp {
+ const absNow = AbsoluteTime.now();
+ return AbsoluteTime.toPreciseTimestamp(absNow);
+ }
+
+ export function round(t: TalerPreciseTimestamp): TalerProtocolTimestamp {
+ return {
+ t_s: t.t_s,
+ };
+ }
+
+ export function fromSeconds(s: number): TalerPreciseTimestamp {
+ return {
+ t_s: Math.floor(s),
+ off_us: Math.floor((s - Math.floor(s)) / 1000 / 1000),
+ };
+ }
+
+ export function fromMilliseconds(ms: number): TalerPreciseTimestamp {
+ return {
+ t_s: Math.floor(ms / 1000),
+ off_us: Math.floor((ms - Math.floor(ms / 1000) * 1000) * 1000),
+ };
+ }
}
export namespace TalerProtocolTimestamp {
export function now(): TalerProtocolTimestamp {
- return AbsoluteTime.toTimestamp(AbsoluteTime.now());
+ return AbsoluteTime.toProtocolTimestamp(AbsoluteTime.now());
}
export function zero(): TalerProtocolTimestamp {
@@ -51,11 +114,16 @@ export namespace TalerProtocolTimestamp {
};
}
+ export function isNever(t: TalerProtocolTimestamp): boolean {
+ return t.t_s === "never";
+ }
+
export function fromSeconds(s: number): TalerProtocolTimestamp {
return {
t_s: s,
};
}
+
export function min(
t1: TalerProtocolTimestamp,
t2: TalerProtocolTimestamp,
@@ -64,10 +132,19 @@ export namespace TalerProtocolTimestamp {
return { t_s: t2.t_s };
}
if (t2.t_s === "never") {
- return { t_s: t2.t_s };
+ return { t_s: t1.t_s };
}
return { t_s: Math.min(t1.t_s, t2.t_s) };
}
+ export function max(
+ t1: TalerProtocolTimestamp,
+ t2: TalerProtocolTimestamp,
+ ): TalerProtocolTimestamp {
+ if (t1.t_s === "never" || t2.t_s === "never") {
+ return { t_s: "never" };
+ }
+ return { t_s: Math.max(t1.t_s, t2.t_s) };
+ }
}
export interface Duration {
@@ -81,13 +158,27 @@ export interface TalerProtocolDuration {
readonly d_us: number | "forever";
}
+/**
+ * Timeshift in milliseconds.
+ */
let timeshift = 0;
+/**
+ * Set timetravel offset in milliseconds.
+ *
+ * Use carefully and only for testing.
+ */
export function setDangerousTimetravel(dt: number): void {
timeshift = dt;
}
export namespace Duration {
+ export function toMilliseconds(d: Duration): number {
+ if (d.d_ms === "forever") {
+ return Number.MAX_VALUE;
+ }
+ return d.d_ms;
+ }
export function getRemaining(
deadline: AbsoluteTime,
now = AbsoluteTime.now(),
@@ -104,6 +195,72 @@ export namespace Duration {
return { d_ms: deadline.t_ms - now.t_ms };
}
+ export function fromPrettyString(s: string): Duration {
+ let dMs = 0;
+ let currentNum = "";
+ let parsingNum = true;
+ for (let i = 0; i < s.length; i++) {
+ const cc = s.charCodeAt(i);
+ if (cc >= "0".charCodeAt(0) && cc <= "9".charCodeAt(0)) {
+ if (!parsingNum) {
+ throw Error("invalid duration, unexpected number");
+ }
+ currentNum += s[i];
+ continue;
+ }
+ if (s[i] == " ") {
+ if (currentNum != "") {
+ parsingNum = false;
+ }
+ continue;
+ }
+
+ if (currentNum == "") {
+ throw Error("invalid duration, missing number");
+ }
+
+ if (s[i] === "s") {
+ dMs += 1000 * Number.parseInt(currentNum, 10);
+ } else if (s[i] === "m") {
+ dMs += 60 * 1000 * Number.parseInt(currentNum, 10);
+ } else if (s[i] === "h") {
+ dMs += 60 * 60 * 1000 * Number.parseInt(currentNum, 10);
+ } else if (s[i] === "d") {
+ dMs += 24 * 60 * 60 * 1000 * Number.parseInt(currentNum, 10);
+ } else {
+ throw Error("invalid duration, unsupported unit");
+ }
+ currentNum = "";
+ parsingNum = true;
+ }
+ return {
+ d_ms: dMs,
+ };
+ }
+
+ /**
+ * Compare two durations. Returns 0 when equal, -1 when a < b
+ * and +1 when a > b.
+ */
+ export function cmp(d1: Duration, d2: Duration): 1 | 0 | -1 {
+ if (d1.d_ms === "forever") {
+ if (d2.d_ms === "forever") {
+ return 0;
+ }
+ return 1;
+ }
+ if (d2.d_ms === "forever") {
+ return -1;
+ }
+ if (d1.d_ms == d2.d_ms) {
+ return 0;
+ }
+ if (d1.d_ms > d2.d_ms) {
+ return 1;
+ }
+ return -1;
+ }
+
export function max(d1: Duration, d2: Duration): Duration {
return durationMax(d1, d2);
}
@@ -123,7 +280,23 @@ export namespace Duration {
return Math.ceil(d.d_ms / 1000 / 60 / 60 / 24 / 365);
}
- export const fromSpec = durationFromSpec;
+ export function fromSpec(spec: {
+ seconds?: number;
+ minutes?: number;
+ hours?: number;
+ days?: number;
+ months?: number;
+ years?: number;
+ }): Duration {
+ let d_ms = 0;
+ d_ms += (spec.seconds ?? 0) * SECONDS;
+ d_ms += (spec.minutes ?? 0) * MINUTES;
+ d_ms += (spec.hours ?? 0) * HOURS;
+ d_ms += (spec.days ?? 0) * DAYS;
+ d_ms += (spec.months ?? 0) * MONTHS;
+ d_ms += (spec.years ?? 0) * YEARS;
+ return { d_ms };
+ }
export function getForever(): Duration {
return { d_ms: "forever" };
@@ -142,7 +315,7 @@ export namespace Duration {
};
}
return {
- d_ms: d.d_us / 1000,
+ d_ms: Math.floor(d.d_us / 1000),
};
}
@@ -157,6 +330,12 @@ export namespace Duration {
};
}
+ export function fromMilliseconds(ms: number): Duration {
+ return {
+ d_ms: ms,
+ };
+ }
+
export function clamp(args: {
lower: Duration;
upper: Duration;
@@ -167,15 +346,32 @@ export namespace Duration {
}
export namespace AbsoluteTime {
+ export function getStampMsNow(): number {
+ return new Date().getTime();
+ }
+
+ export function getStampMsNever(): number {
+ return Number.MAX_SAFE_INTEGER;
+ }
+
export function now(): AbsoluteTime {
return {
t_ms: new Date().getTime() + timeshift,
+ [opaque_AbsoluteTime]: true,
};
}
export function never(): AbsoluteTime {
return {
t_ms: "never",
+ [opaque_AbsoluteTime]: true,
+ };
+ }
+
+ export function fromMilliseconds(ms: number): AbsoluteTime {
+ return {
+ t_ms: ms,
+ [opaque_AbsoluteTime]: true,
};
}
@@ -200,22 +396,22 @@ export namespace AbsoluteTime {
export function min(t1: AbsoluteTime, t2: AbsoluteTime): AbsoluteTime {
if (t1.t_ms === "never") {
- return { t_ms: t2.t_ms };
+ return { t_ms: t2.t_ms, [opaque_AbsoluteTime]: true };
}
if (t2.t_ms === "never") {
- return { t_ms: t2.t_ms };
+ return { t_ms: t2.t_ms, [opaque_AbsoluteTime]: true };
}
- return { t_ms: Math.min(t1.t_ms, t2.t_ms) };
+ return { t_ms: Math.min(t1.t_ms, t2.t_ms), [opaque_AbsoluteTime]: true };
}
export function max(t1: AbsoluteTime, t2: AbsoluteTime): AbsoluteTime {
if (t1.t_ms === "never") {
- return { t_ms: "never" };
+ return { t_ms: "never", [opaque_AbsoluteTime]: true };
}
if (t2.t_ms === "never") {
- return { t_ms: "never" };
+ return { t_ms: "never", [opaque_AbsoluteTime]: true };
}
- return { t_ms: Math.max(t1.t_ms, t2.t_ms) };
+ return { t_ms: Math.max(t1.t_ms, t2.t_ms), [opaque_AbsoluteTime]: true };
}
export function difference(t1: AbsoluteTime, t2: AbsoluteTime): Duration {
@@ -232,16 +428,64 @@ export namespace AbsoluteTime {
return cmp(t, now()) <= 0;
}
- export function fromTimestamp(t: TalerProtocolTimestamp): AbsoluteTime {
+ export function isNever(t: AbsoluteTime): boolean {
+ return t.t_ms === "never";
+ }
+
+ export function fromProtocolTimestamp(
+ t: TalerProtocolTimestamp,
+ ): AbsoluteTime {
if (t.t_s === "never") {
- return { t_ms: "never" };
+ return { t_ms: "never", [opaque_AbsoluteTime]: true };
}
return {
t_ms: t.t_s * 1000,
+ [opaque_AbsoluteTime]: true,
};
}
- export function toTimestamp(at: AbsoluteTime): TalerProtocolTimestamp {
+ export function fromStampMs(stampMs: number): AbsoluteTime {
+ return {
+ t_ms: stampMs,
+ [opaque_AbsoluteTime]: true,
+ };
+ }
+
+ export function fromPreciseTimestamp(t: TalerPreciseTimestamp): AbsoluteTime {
+ if (t.t_s === "never") {
+ return { t_ms: "never", [opaque_AbsoluteTime]: true };
+ }
+ const offsetUs = t.off_us ?? 0;
+ return {
+ t_ms: t.t_s * 1000 + Math.floor(offsetUs / 1000),
+ [opaque_AbsoluteTime]: true,
+ };
+ }
+
+ export function toStampMs(at: AbsoluteTime): number {
+ if (at.t_ms === "never") {
+ return Number.MAX_SAFE_INTEGER;
+ }
+ return at.t_ms;
+ }
+
+ export function toPreciseTimestamp(at: AbsoluteTime): TalerPreciseTimestamp {
+ if (at.t_ms == "never") {
+ return {
+ t_s: "never",
+ };
+ }
+ const t_s = Math.floor(at.t_ms / 1000);
+ const off_us = Math.floor(1000 * (at.t_ms - t_s * 1000));
+ return {
+ t_s,
+ off_us,
+ };
+ }
+
+ export function toProtocolTimestamp(
+ at: AbsoluteTime,
+ ): TalerProtocolTimestamp {
if (at.t_ms === "never") {
return { t_s: "never" };
}
@@ -274,9 +518,26 @@ export namespace AbsoluteTime {
export function addDuration(t1: AbsoluteTime, d: Duration): AbsoluteTime {
if (t1.t_ms === "never" || d.d_ms === "forever") {
- return { t_ms: "never" };
+ return { t_ms: "never", [opaque_AbsoluteTime]: true };
+ }
+ return { t_ms: t1.t_ms + d.d_ms, [opaque_AbsoluteTime]: true };
+ }
+
+ /**
+ * Get the remaining duration until {@param t1}.
+ *
+ * If {@param t1} already happened, the remaining duration
+ * is zero.
+ */
+ export function remaining(t1: AbsoluteTime): Duration {
+ if (t1.t_ms === "never") {
+ return Duration.getForever();
}
- return { t_ms: t1.t_ms + d.d_ms };
+ const stampNow = now();
+ if (stampNow.t_ms === "never") {
+ throw Error("invariant violated");
+ }
+ return Duration.fromMilliseconds(Math.max(0, t1.t_ms - stampNow.t_ms));
}
export function subtractDuraction(
@@ -284,12 +545,12 @@ export namespace AbsoluteTime {
d: Duration,
): AbsoluteTime {
if (t1.t_ms === "never") {
- return { t_ms: "never" };
+ return { t_ms: "never", [opaque_AbsoluteTime]: true };
}
if (d.d_ms === "forever") {
- return { t_ms: 0 };
+ return { t_ms: 0, [opaque_AbsoluteTime]: true };
}
- return { t_ms: Math.max(0, t1.t_ms - d.d_ms) };
+ return { t_ms: Math.max(0, t1.t_ms - d.d_ms), [opaque_AbsoluteTime]: true };
}
export function stringify(t: AbsoluteTime): string {
@@ -307,24 +568,6 @@ const DAYS = HOURS * 24;
const MONTHS = DAYS * 30;
const YEARS = DAYS * 365;
-export function durationFromSpec(spec: {
- seconds?: number;
- minutes?: number;
- hours?: number;
- days?: number;
- months?: number;
- years?: number;
-}): Duration {
- let d_ms = 0;
- d_ms += (spec.seconds ?? 0) * SECONDS;
- d_ms += (spec.minutes ?? 0) * MINUTES;
- d_ms += (spec.hours ?? 0) * HOURS;
- d_ms += (spec.days ?? 0) * DAYS;
- d_ms += (spec.months ?? 0) * MONTHS;
- d_ms += (spec.years ?? 0) * YEARS;
- return { d_ms };
-}
-
export function durationMin(d1: Duration, d2: Duration): Duration {
if (d1.d_ms === "forever") {
return { d_ms: d2.d_ms };
@@ -361,13 +604,16 @@ export function durationAdd(d1: Duration, d2: Duration): Duration {
export const codecForAbsoluteTime: Codec<AbsoluteTime> = {
decode(x: any, c?: Context): AbsoluteTime {
+ if (x === undefined) {
+ throw Error(`got undefined and expected absolute time at ${renderContext(c)}`);
+ }
const t_ms = x.t_ms;
if (typeof t_ms === "string") {
if (t_ms === "never") {
- return { t_ms: "never" };
+ return { t_ms: "never", [opaque_AbsoluteTime]: true };
}
} else if (typeof t_ms === "number") {
- return { t_ms };
+ return { t_ms, [opaque_AbsoluteTime]: true };
}
throw Error(`expected timestamp at ${renderContext(c)}`);
},
@@ -376,6 +622,9 @@ export const codecForAbsoluteTime: Codec<AbsoluteTime> = {
export const codecForTimestamp: Codec<TalerProtocolTimestamp> = {
decode(x: any, c?: Context): TalerProtocolTimestamp {
// Compatibility, should be removed soon.
+ if (x === undefined) {
+ throw Error(`got undefined and expected timestamp at ${renderContext(c)}`);
+ }
const t_ms = x.t_ms;
if (typeof t_ms === "string") {
if (t_ms === "never") {
@@ -394,7 +643,21 @@ export const codecForTimestamp: Codec<TalerProtocolTimestamp> = {
if (typeof t_s === "number") {
return { t_s };
}
- throw Error(`expected timestamp at ${renderContext(c)}`);
+ throw Error(`expected protocol timestamp at ${renderContext(c)}`);
+ },
+};
+
+export const codecForPreciseTimestamp: Codec<TalerPreciseTimestamp> = {
+ decode(x: any, c?: Context): TalerPreciseTimestamp {
+ const t_ms = x.t_ms;
+ if (typeof t_ms === "string") {
+ if (t_ms === "never") {
+ return { t_s: "never" };
+ }
+ } else if (typeof t_ms === "number") {
+ return { t_s: Math.floor(t_ms / 1000) };
+ }
+ throw Error(`expected precise timestamp at ${renderContext(c)}`);
},
};
diff --git a/packages/taler-util/src/timer.ts b/packages/taler-util/src/timer.ts
new file mode 100644
index 000000000..8db024512
--- /dev/null
+++ b/packages/taler-util/src/timer.ts
@@ -0,0 +1,213 @@
+/*
+ This file is part of GNU Taler
+ (C) 2017-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 <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Cross-platform timers.
+ *
+ * NodeJS and the browser use slightly different timer API,
+ * this abstracts over these differences.
+ */
+
+/**
+ * Imports.
+ */
+import { Logger, Duration } from "@gnu-taler/taler-util";
+
+const logger = new Logger("timer.ts");
+
+/**
+ * Cancelable timer.
+ */
+export interface TimerHandle {
+ clear(): void;
+
+ /**
+ * Make sure the event loop exits when the timer is the
+ * only event left. Has no effect in the browser.
+ */
+ unref(): void;
+}
+
+class IntervalHandle {
+ constructor(public h: any) {}
+
+ clear(): void {
+ clearInterval(this.h);
+ }
+
+ /**
+ * Make sure the event loop exits when the timer is the
+ * only event left. Has no effect in the browser.
+ */
+ unref(): void {
+ if (typeof this.h === "object" && "unref" in this.h) {
+ this.h.unref();
+ }
+ }
+}
+
+class TimeoutHandle {
+ constructor(public h: any) {}
+
+ clear(): void {
+ clearTimeout(this.h);
+ }
+
+ /**
+ * Make sure the event loop exits when the timer is the
+ * only event left. Has no effect in the browser.
+ */
+ unref(): void {
+ if (typeof this.h === "object" && "unref" in this.h) {
+ this.h.unref();
+ }
+ }
+}
+
+/**
+ * Get a performance counter in nanoseconds.
+ */
+export const performanceNow: () => bigint = (() => {
+ // @ts-ignore
+ if (typeof process !== "undefined" && process.hrtime) {
+ return () => {
+ return process.hrtime.bigint();
+ };
+ }
+
+ // @ts-ignore
+ if (typeof performance !== "undefined") {
+ // @ts-ignore
+ return () => BigInt(Math.floor(performance.now() * 1000)) * BigInt(1000);
+ }
+
+ return () => BigInt(new Date().getTime()) * BigInt(1000) * BigInt(1000);
+})();
+
+const nullTimerHandle = {
+ clear() {
+ // do nothing
+ return;
+ },
+ unref() {
+ // do nothing
+ return;
+ },
+};
+
+/**
+ * Group of timers that can be destroyed at once.
+ */
+export interface TimerAPI {
+ after(delayMs: number, callback: () => void): TimerHandle;
+ every(delayMs: number, callback: () => void): TimerHandle;
+}
+
+export class SetTimeoutTimerAPI implements TimerAPI {
+ /**
+ * Call a function every time the delay given in milliseconds passes.
+ */
+ every(delayMs: number, callback: () => void): TimerHandle {
+ return new IntervalHandle(setInterval(callback, delayMs));
+ }
+
+ /**
+ * Call a function after the delay given in milliseconds passes.
+ */
+ after(delayMs: number, callback: () => void): TimerHandle {
+ return new TimeoutHandle(setTimeout(callback, delayMs));
+ }
+}
+
+export const timer = new SetTimeoutTimerAPI();
+
+/**
+ * Implementation of [[TimerGroup]] using setTimeout
+ */
+export class TimerGroup {
+ private stopped = false;
+
+ private readonly timerMap: { [index: number]: TimerHandle } = {};
+
+ private idGen = 1;
+
+ constructor(public readonly timerApi: TimerAPI) {}
+
+ stopCurrentAndFutureTimers(): void {
+ this.stopped = true;
+ for (const x in this.timerMap) {
+ if (!this.timerMap.hasOwnProperty(x)) {
+ continue;
+ }
+ this.timerMap[x].clear();
+ delete this.timerMap[x];
+ }
+ }
+
+ resolveAfter(delayMs: Duration): Promise<void> {
+ return new Promise<void>((resolve, reject) => {
+ if (delayMs.d_ms !== "forever") {
+ this.after(delayMs.d_ms, () => {
+ resolve();
+ });
+ }
+ });
+ }
+
+ after(delayMs: number, callback: () => void): TimerHandle {
+ if (this.stopped) {
+ logger.warn("dropping timer since timer group is stopped");
+ return nullTimerHandle;
+ }
+ const h = this.timerApi.after(delayMs, callback);
+ const myId = this.idGen++;
+ this.timerMap[myId] = h;
+
+ const tm = this.timerMap;
+
+ return {
+ clear() {
+ h.clear();
+ delete tm[myId];
+ },
+ unref() {
+ h.unref();
+ },
+ };
+ }
+
+ every(delayMs: number, callback: () => void): TimerHandle {
+ if (this.stopped) {
+ logger.warn("dropping timer since timer group is stopped");
+ return nullTimerHandle;
+ }
+ const h = this.timerApi.every(delayMs, callback);
+ const myId = this.idGen++;
+ this.timerMap[myId] = h;
+
+ const tm = this.timerMap;
+
+ return {
+ clear() {
+ h.clear();
+ delete tm[myId];
+ },
+ unref() {
+ h.unref();
+ },
+ };
+ }
+}
diff --git a/packages/taler-util/src/transaction-test-data.ts b/packages/taler-util/src/transaction-test-data.ts
new file mode 100644
index 000000000..378028144
--- /dev/null
+++ b/packages/taler-util/src/transaction-test-data.ts
@@ -0,0 +1,113 @@
+/*
+ This file is part of GNU Taler
+ (C) 2023 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 {
+ TransactionType,
+ PaymentStatus,
+ TransactionMajorState,
+} from "./transactions-types.js";
+import { RefreshReason } from "./wallet-types.js";
+
+/**
+ * Sample transaction list entries.
+ */
+export const sampleWalletCoreTransactions = [
+ {
+ type: TransactionType.Payment,
+ txState: {
+ major: TransactionMajorState.Done,
+ },
+ amountRaw: "KUDOS:10",
+ amountEffective: "KUDOS:10",
+ totalRefundRaw: "KUDOS:0",
+ totalRefundEffective: "KUDOS:0",
+ status: PaymentStatus.Paid,
+ refundPending: undefined,
+ posConfirmation: undefined,
+ pending: false,
+ refunds: [],
+ timestamp: {
+ t_s: 1677166045,
+ },
+ transactionId:
+ "txn:payment:NRRD9KJ8970P5HDAGPW1MBA6HZHB1XMFKF5M3CNR6WA0GT98DHY0",
+ proposalId: "NRRD9KJ8970P5HDAGPW1MBA6HZHB1XMFKF5M3CNR6WA0GT98DHY0",
+ info: {
+ merchant: {
+ name: "woocommerce",
+ website: "woocommerce.demo.taler.net",
+ email: "foo@example.com",
+ address: {},
+ jurisdiction: {},
+ },
+ orderId: "wc_order_KQCRldghIgDRB-100",
+ products: [
+ {
+ description: "Using GCC",
+ quantity: 1,
+ price: "KUDOS:10",
+ product_id: "28",
+ },
+ ],
+ summary: "WooTalerShop #100",
+ contractTermsHash:
+ "A02E1M6ARWKBJ87K2TV4S6WQ4X5YH7BRVR6MYCHCTVAED8MBXTFD6PZ5Q50Y7Z5K18PYBTDA14NQ56XPC1VCQW1EVRWTSB7ZYT65B5G",
+ fulfillmentUrl:
+ "https://woocommerce.demo.taler.net/?wc-api=wc_gnutaler_gateway&order_id=wc_order_KQCRldghIgDRB-100",
+ },
+ refundQueryActive: false,
+ frozen: false,
+ },
+ {
+ type: TransactionType.Refresh,
+ txState: {
+ major: TransactionMajorState.Pending,
+ },
+ refreshReason: RefreshReason.PayMerchant,
+ amountEffective: "KUDOS:0",
+ amountRaw: "KUDOS:0",
+ refreshInputAmount: "KUDOS:1.5",
+ refreshOutputAmount: "KUDOS:1.4",
+ originatingTransactionId:
+ "txn:proposal:ZCGBZFE8KZ1CBYYGSC3ZC8E40KVJWV16VYCTHGC8FFSVZ5HD24BG",
+ pending: true,
+ timestamp: {
+ t_s: 1681376214,
+ },
+ transactionId:
+ "txn:refresh:QQSWHHXCRQ269G0E3RW14JMC6F7NFDYDW26NSFHRTXSKDS6CMCZ0",
+ frozen: false,
+ error: {
+ code: 7029,
+ when: {
+ t_ms: 1681376473665,
+ },
+ hint: "Error (WALLET_REFRESH_GROUP_INCOMPLETE)",
+ numErrors: 1,
+ errors: [
+ {
+ code: 7001,
+ when: {
+ t_ms: 1681376473189,
+ },
+ hint: "unexpected exception (message: exchange wire fee signature invalid)",
+ stack:
+ " at validateWireInfo (../taler-wallet-core-qjs.mjs:23166)\n",
+ },
+ ],
+ },
+ },
+];
diff --git a/packages/taler-util/src/transactions-types.ts b/packages/taler-util/src/transactions-types.ts
index 62caaa055..cee3de9fa 100644
--- a/packages/taler-util/src/transactions-types.ts
+++ b/packages/taler-util/src/transactions-types.ts
@@ -24,41 +24,143 @@
/**
* Imports.
*/
-import { TalerProtocolTimestamp } from "./time.js";
+import {
+ Codec,
+ buildCodecForObject,
+ codecForAny,
+ codecForBoolean,
+ codecForConstString,
+ codecForEither,
+ codecForList,
+ codecForString,
+ codecOptional,
+} from "./codec.js";
import {
AmountString,
- Product,
InternationalizedString,
MerchantInfo,
codecForInternationalizedString,
codecForMerchantInfo,
- codecForProduct,
- Location,
} from "./taler-types.js";
-import {
- Codec,
- buildCodecForObject,
- codecOptional,
- codecForString,
- codecForList,
- codecForAny,
-} from "./codec.js";
+import { TalerPreciseTimestamp, TalerProtocolTimestamp } from "./time.js";
import {
RefreshReason,
+ ScopeInfo,
TalerErrorDetail,
TransactionIdStr,
+ TransactionStateFilter,
+ WithdrawalExchangeAccountDetails,
+ codecForScopeInfo,
} from "./wallet-types.js";
export interface TransactionsRequest {
/**
* return only transactions in the given currency
+ *
+ * it will be removed in next release
+ *
+ * @deprecated use scopeInfo
*/
currency?: string;
/**
+ * return only transactions in the given scopeInfo
+ */
+ scopeInfo?: ScopeInfo;
+
+ /**
* if present, results will be limited to transactions related to the given search string
*/
search?: string;
+
+ /**
+ * Sort order of the transaction items.
+ * By default, items are sorted ascending by their
+ * main timestamp.
+ *
+ * ascending: ascending by timestamp, but pending transactions first
+ * descending: ascending by timestamp, but pending transactions first
+ * stable-ascending: ascending by timestamp, with pending transactions amidst other transactions
+ * (stable in the sense of: pending transactions don't jump around)
+ */
+ sort?: "ascending" | "descending" | "stable-ascending";
+
+ /**
+ * If true, include all refreshes in the transactions list.
+ */
+ includeRefreshes?: boolean;
+
+ filterByState?: TransactionStateFilter;
+}
+
+export interface TransactionState {
+ major: TransactionMajorState;
+ minor?: TransactionMinorState;
+}
+
+export enum TransactionMajorState {
+ // No state, only used when reporting transitions into the initial state
+ None = "none",
+ Pending = "pending",
+ Done = "done",
+ Aborting = "aborting",
+ Aborted = "aborted",
+ Suspended = "suspended",
+ Dialog = "dialog",
+ SuspendedAborting = "suspended-aborting",
+ Failed = "failed",
+ Expired = "expired",
+ // Only used for the notification, never in the transaction history
+ Deleted = "deleted",
+}
+
+export enum TransactionMinorState {
+ // Placeholder until D37 is fully implemented
+ Unknown = "unknown",
+ Deposit = "deposit",
+ KycRequired = "kyc",
+ AmlRequired = "aml",
+ MergeKycRequired = "merge-kyc",
+ Track = "track",
+ SubmitPayment = "submit-payment",
+ RebindSession = "rebind-session",
+ Refresh = "refresh",
+ Pickup = "pickup",
+ AutoRefund = "auto-refund",
+ User = "user",
+ Bank = "bank",
+ Exchange = "exchange",
+ ClaimProposal = "claim-proposal",
+ CheckRefund = "check-refund",
+ CreatePurse = "create-purse",
+ DeletePurse = "delete-purse",
+ RefreshExpired = "refresh-expired",
+ Ready = "ready",
+ Merge = "merge",
+ Repurchase = "repurchase",
+ BankRegisterReserve = "bank-register-reserve",
+ BankConfirmTransfer = "bank-confirm-transfer",
+ WithdrawCoins = "withdraw-coins",
+ ExchangeWaitReserve = "exchange-wait-reserve",
+ AbortingBank = "aborting-bank",
+ Aborting = "aborting",
+ Refused = "refused",
+ Withdraw = "withdraw",
+ MerchantOrderProposed = "merchant-order-proposed",
+ Proposed = "proposed",
+ RefundAvailable = "refund-available",
+ AcceptRefund = "accept-refund",
+ PaidByOther = "paid-by-other",
+ CompletedByOtherWallet = "completed-by-other-wallet",
+}
+
+export enum TransactionAction {
+ Delete = "delete",
+ Suspend = "suspend",
+ Resume = "resume",
+ Abort = "abort",
+ Fail = "fail",
+ Retry = "retry",
}
export interface TransactionsResponse {
@@ -78,18 +180,17 @@ export interface TransactionCommon {
type: TransactionType;
// main timestamp of the transaction
- timestamp: TalerProtocolTimestamp;
+ timestamp: TalerPreciseTimestamp;
- // true if the transaction is still pending, false otherwise
- // If a transaction is not longer pending, its timestamp will be updated,
- // but its transactionId will remain unchanged
- pending: boolean;
+ /**
+ * Transaction state, as per DD37.
+ */
+ txState: TransactionState;
/**
- * True if the transaction encountered a problem that might be
- * permanent. A frozen transaction won't be automatically retried.
+ * Possible transitions based on the current state.
*/
- frozen: boolean;
+ txActions: TransactionAction[];
/**
* Raw amount of the transaction (exclusive of fees or other extra costs).
@@ -102,31 +203,41 @@ export interface TransactionCommon {
amountEffective: AmountString;
error?: TalerErrorDetail;
+
+ /**
+ * If the transaction minor state is in KycRequired this field is going to
+ * have the location where the user need to go to complete KYC information.
+ */
+ kycUrl?: string;
}
export type Transaction =
| TransactionWithdrawal
| TransactionPayment
| TransactionRefund
- | TransactionTip
| TransactionRefresh
| TransactionDeposit
| TransactionPeerPullCredit
| TransactionPeerPullDebit
| TransactionPeerPushCredit
- | TransactionPeerPushDebit;
+ | TransactionPeerPushDebit
+ | TransactionInternalWithdrawal
+ | TransactionRecoup
+ | TransactionDenomLoss;
export enum TransactionType {
Withdrawal = "withdrawal",
+ InternalWithdrawal = "internal-withdrawal",
Payment = "payment",
Refund = "refund",
Refresh = "refresh",
- Tip = "tip",
Deposit = "deposit",
PeerPushDebit = "peer-push-debit",
PeerPushCredit = "peer-push-credit",
PeerPullDebit = "peer-pull-debit",
PeerPullCredit = "peer-pull-credit",
+ Recoup = "recoup",
+ DenomLoss = "denom-loss",
}
export enum WithdrawalType {
@@ -145,11 +256,20 @@ interface WithdrawalDetailsForManualTransfer {
* Payto URIs that the exchange supports.
*
* Already contains the amount and message.
+ *
+ * @deprecated in favor of exchangeCreditAccounts
*/
exchangePaytoUris: string[];
+ exchangeCreditAccountDetails?: WithdrawalExchangeAccountDetails[];
+
// Public key of the reserve
reservePub: string;
+
+ /**
+ * Is the reserve ready for withdrawal?
+ */
+ reserveIsReady: boolean;
}
interface WithdrawalDetailsForTalerBankIntegrationApi {
@@ -170,10 +290,34 @@ interface WithdrawalDetailsForTalerBankIntegrationApi {
// Public key of the reserve
reservePub: string;
+
+ /**
+ * Is the reserve ready for withdrawal?
+ */
+ reserveIsReady: boolean;
+
+ exchangeCreditAccountDetails?: WithdrawalExchangeAccountDetails[];
+}
+
+export enum DenomLossEventType {
+ DenomExpired = "denom-expired",
+ DenomVanished = "denom-vanished",
+ DenomUnoffered = "denom-unoffered",
+}
+
+/**
+ * A transaction to indicate financial loss due to denominations
+ * that became unusable for deposits.
+ */
+export interface TransactionDenomLoss extends TransactionCommon {
+ type: TransactionType.DenomLoss;
+ lossEventType: DenomLossEventType;
+ exchangeBaseUrl: string;
}
-// This should only be used for actual withdrawals
-// and not for tips that have their own transactions type.
+/**
+ * A withdrawal transaction (either bank-integrated or manual).
+ */
export interface TransactionWithdrawal extends TransactionCommon {
type: TransactionType.Withdrawal;
@@ -195,6 +339,40 @@ export interface TransactionWithdrawal extends TransactionCommon {
withdrawalDetails: WithdrawalDetails;
}
+/**
+ * Internal withdrawal operation, only reported on request.
+ *
+ * Some transactions (peer-*-credit) internally do a withdrawal,
+ * but only the peer-*-credit transaction is reported.
+ *
+ * The internal withdrawal transaction allows to access the details of
+ * the underlying withdrawal for testing/debugging.
+ *
+ * It is usually not reported, so that amounts of transactions properly
+ * add up, since the amountEffecive of the withdrawal is already reported
+ * in the peer-*-credit transaction.
+ */
+export interface TransactionInternalWithdrawal extends TransactionCommon {
+ type: TransactionType.InternalWithdrawal;
+
+ /**
+ * Exchange of the withdrawal.
+ */
+ exchangeBaseUrl: string;
+
+ /**
+ * Amount that got subtracted from the reserve balance.
+ */
+ amountRaw: AmountString;
+
+ /**
+ * Amount that actually was (or will be) added to the wallet's balance.
+ */
+ amountEffective: AmountString;
+
+ withdrawalDetails: WithdrawalDetails;
+}
+
export interface PeerInfoShort {
expiration: TalerProtocolTimestamp | undefined;
summary: string | undefined;
@@ -224,8 +402,10 @@ export interface TransactionPeerPullCredit extends TransactionCommon {
/**
* URI to send to the other party.
+ *
+ * Only available in the right state.
*/
- talerUri: string;
+ talerUri: string | undefined;
}
/**
@@ -269,8 +449,11 @@ export interface TransactionPeerPushDebit extends TransactionCommon {
/**
* URI to accept the payment.
+ *
+ * Only present if the transaction is in a state where the other party can
+ * accept the payment.
*/
- talerUri: string;
+ talerUri?: string;
}
/**
@@ -296,6 +479,13 @@ export interface TransactionPeerPushCredit extends TransactionCommon {
amountEffective: AmountString;
}
+/**
+ * The exchange revoked a key and the wallet recoups funds.
+ */
+export interface TransactionRecoup extends TransactionCommon {
+ type: TransactionType.Recoup;
+}
+
export enum PaymentStatus {
/**
* Explicitly aborted after timeout / failure
@@ -333,11 +523,6 @@ export interface TransactionPayment extends TransactionCommon {
proposalId: string;
/**
- * How far did the wallet get with processing the payment?
- */
- status: PaymentStatus;
-
- /**
* Amount that must be paid for the contract
*/
amountRaw: AmountString;
@@ -366,6 +551,16 @@ export interface TransactionPayment extends TransactionCommon {
* Reference to applied refunds
*/
refunds: RefundInfoShort[];
+
+ /**
+ * Is the wallet currently checking for a refund?
+ */
+ refundQueryActive: boolean;
+
+ /**
+ * Does this purchase has an pos validation
+ */
+ posConfirmation: string | undefined;
}
export interface OrderShortInfo {
@@ -395,22 +590,6 @@ export interface OrderShortInfo {
summary_i18n?: InternationalizedString;
/**
- * List of products that are part of the order
- */
- products: Product[] | undefined;
-
- /**
- * Time indicating when the order should be delivered.
- * May be overwritten by individual products.
- */
- delivery_date?: TalerProtocolTimestamp;
-
- /**
- * Delivery location for (all!) products.
- */
- delivery_location?: Location;
-
- /**
* URL of the fulfillment, given by the merchant
*/
fulfillmentUrl?: string;
@@ -434,42 +613,31 @@ export interface RefundInfoShort {
amountRaw: AmountString;
}
-export interface TransactionRefund extends TransactionCommon {
- type: TransactionType.Refund;
-
- // ID for the transaction that is refunded
- refundedTransactionId: string;
-
- // Additional information about the refunded payment
- info: OrderShortInfo;
-
+/**
+ * Summary information about the payment that we got a refund for.
+ */
+export interface RefundPaymentInfo {
+ summary: string;
+ summary_i18n?: InternationalizedString;
/**
- * Amount pending to be picked up
+ * More information about the merchant
*/
- refundPending: AmountString | undefined;
+ merchant: MerchantInfo;
+}
+
+export interface TransactionRefund extends TransactionCommon {
+ type: TransactionType.Refund;
// Amount that has been refunded by the merchant
amountRaw: AmountString;
// Amount will be added to the wallet's balance after fees and refreshing
amountEffective: AmountString;
-}
-export interface TransactionTip extends TransactionCommon {
- type: TransactionType.Tip;
-
- // Raw amount of the tip, without extra fees that apply
- amountRaw: AmountString;
-
- /**
- * More information about the merchant
- */
- // merchant: MerchantInfo;
-
- // Amount will be (or was) added to the wallet's balance after fees and refreshing
- amountEffective: AmountString;
+ // ID for the transaction that is refunded
+ refundedTransactionId: string;
- merchantBaseUrl: string;
+ paymentInfo: RefundPaymentInfo | undefined;
}
/**
@@ -480,11 +648,6 @@ export interface TransactionTip extends TransactionCommon {
export interface TransactionRefresh extends TransactionCommon {
type: TransactionType.Refresh;
- /**
- * Exchange that the coins are refreshed with
- */
- exchangeBaseUrl: string;
-
refreshReason: RefreshReason;
/**
@@ -500,8 +663,26 @@ export interface TransactionRefresh extends TransactionCommon {
/**
* Fees, i.e. the effective, negative effect of the refresh
* on the balance.
+ *
+ * Only applicable for stand-alone refreshes, and zero for
+ * other refreshes where the transaction itself accounts for the
+ * refresh fee.
*/
amountEffective: AmountString;
+
+ refreshInputAmount: AmountString;
+ refreshOutputAmount: AmountString;
+}
+
+export interface DepositTransactionTrackingState {
+ // Raw wire transfer identifier of the deposit.
+ wireTransferId: string;
+ // When was the wire transfer given to the bank.
+ timestampExecuted: TalerProtocolTimestamp;
+ // Total amount transfer for this wtid (including fees)
+ amountRaw: AmountString;
+ // Wire fee amount for this exchange
+ wireFee: AmountString;
}
/**
@@ -529,6 +710,15 @@ export interface TransactionDeposit extends TransactionCommon {
amountEffective: AmountString;
wireTransferDeadline: TalerProtocolTimestamp;
+
+ wireTransferProgress: number;
+
+ /**
+ * Did all the deposit requests succeed?
+ */
+ deposited: boolean;
+
+ trackingState: Array<DepositTransactionTrackingState>;
}
export interface TransactionByIdRequest {
@@ -541,10 +731,32 @@ export const codecForTransactionByIdRequest =
.property("transactionId", codecForString())
.build("TransactionByIdRequest");
+export interface WithdrawalTransactionByURIRequest {
+ talerWithdrawUri: string;
+}
+
+export const codecForWithdrawalTransactionByURIRequest =
+ (): Codec<WithdrawalTransactionByURIRequest> =>
+ buildCodecForObject<WithdrawalTransactionByURIRequest>()
+ .property("talerWithdrawUri", codecForString())
+ .build("WithdrawalTransactionByURIRequest");
+
export const codecForTransactionsRequest = (): Codec<TransactionsRequest> =>
buildCodecForObject<TransactionsRequest>()
.property("currency", codecOptional(codecForString()))
+ .property("scopeInfo", codecOptional(codecForScopeInfo()))
.property("search", codecOptional(codecForString()))
+ .property(
+ "sort",
+ codecOptional(
+ codecForEither(
+ codecForConstString("ascending"),
+ codecForConstString("descending"),
+ codecForConstString("stable-ascending"),
+ ),
+ ),
+ )
+ .property("includeRefreshes", codecOptional(codecForBoolean()))
.build("TransactionsRequest");
// FIXME: do full validation here!
@@ -564,7 +776,20 @@ export const codecForOrderShortInfo = (): Codec<OrderShortInfo> =>
.property("fulfillmentUrl", codecOptional(codecForString()))
.property("merchant", codecForMerchantInfo())
.property("orderId", codecForString())
- .property("products", codecOptional(codecForList(codecForProduct())))
.property("summary", codecForString())
.property("summary_i18n", codecOptional(codecForInternationalizedString()))
.build("OrderShortInfo");
+
+export interface ListAssociatedRefreshesRequest {
+ transactionId: string;
+}
+
+export const codecForListAssociatedRefreshesRequest =
+ (): Codec<ListAssociatedRefreshesRequest> =>
+ buildCodecForObject<ListAssociatedRefreshesRequest>()
+ .property("transactionId", codecForString())
+ .build("ListAssociatedRefreshesRequest");
+
+export interface ListAssociatedRefreshesResponse {
+ transactionIds: string[];
+}
diff --git a/packages/taler-util/src/twrpc-impl.missing.ts b/packages/taler-util/src/twrpc-impl.missing.ts
new file mode 100644
index 000000000..7d7fa84ae
--- /dev/null
+++ b/packages/taler-util/src/twrpc-impl.missing.ts
@@ -0,0 +1,26 @@
+/*
+ This file is part of GNU Taler
+ (C) 2023 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 type { RpcConnectArgs, RpcServerArgs } from "./twrpc.js";
+
+// Not implemented.
+export async function connectRpc<T>(args: RpcConnectArgs<T>): Promise<T> {
+ throw Error("not implemented");
+}
+
+export async function runRpcServer(args: RpcServerArgs): Promise<void> {
+ throw Error("not implemented");
+}
diff --git a/packages/taler-util/src/twrpc-impl.node.ts b/packages/taler-util/src/twrpc-impl.node.ts
new file mode 100644
index 000000000..30e362e5b
--- /dev/null
+++ b/packages/taler-util/src/twrpc-impl.node.ts
@@ -0,0 +1,216 @@
+/*
+ This file is part of GNU Taler
+ (C) 2023 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 * as net from "node:net";
+import * as fs from "node:fs";
+import { Logger } from "./logging.js";
+import { bytesToString, typedArrayConcat } from "./taler-crypto.js";
+import type { RpcConnectArgs, RpcServerArgs } from "./twrpc.js";
+
+interface ReadLinewiseArgs {
+ onLine(lineData: Uint8Array): void;
+ sock: net.Socket;
+}
+
+const logger = new Logger("twrpc-impl.node.ts");
+
+function readStreamLinewise(args: ReadLinewiseArgs): void {
+ let chunks: Uint8Array[] = [];
+ args.sock.on("data", (buf: Uint8Array) => {
+ // Process all newlines in the newly received buffer
+ while (1) {
+ const newlineIdx = buf.indexOf("\n".charCodeAt(0));
+ if (newlineIdx >= 0) {
+ let left = buf.subarray(0, newlineIdx + 1);
+ let right = buf.subarray(newlineIdx + 1);
+ chunks.push(left);
+ const line = typedArrayConcat(chunks);
+ args.onLine(line);
+ chunks = [];
+ buf = right;
+ } else {
+ chunks.push(buf);
+ break;
+ }
+ }
+ });
+}
+
+export async function connectRpc<T>(args: RpcConnectArgs<T>): Promise<T> {
+ let sockFilename = args.socketFilename;
+ return new Promise((resolve, reject) => {
+ const client = net.createConnection(sockFilename);
+ client.on("error", (e) => {
+ reject(e);
+ });
+ client.on("connect", () => {
+ let parsingBody: string | undefined = undefined;
+ let bodyChunks: string[] = [];
+
+ logger.info("connected!");
+ client.write("%hello-from-client\n");
+ const res = args.onEstablished({
+ sendMessage(m) {
+ client.write("%request\n");
+ client.write(JSON.stringify(m));
+ client.write("\n");
+ client.write("%end\n");
+ },
+ close() {
+ client.destroy();
+ },
+ });
+ readStreamLinewise({
+ sock: client,
+ onLine(line) {
+ const lineStr = bytesToString(line);
+ // Are we currently parsing the body of a request?
+ if (!parsingBody) {
+ const strippedLine = lineStr.trim();
+ if (strippedLine == "%message") {
+ parsingBody = "message";
+ } else if (strippedLine == "%hello-from-server") {
+ } else if (strippedLine.startsWith("%error:")) {
+ client.end();
+ res.onDisconnect();
+ } else {
+ logger.warn("got unknown request");
+ client.write("%error: invalid message\n");
+ client.end();
+ }
+ } else if (parsingBody == "message") {
+ const strippedLine = lineStr.trim();
+ if (strippedLine == "%end") {
+ let req = bodyChunks.join("");
+ let reqJson: any = undefined;
+ try {
+ reqJson = JSON.parse(req);
+ } catch (e) {
+ logger.warn("JSON message from server was invalid");
+ logger.info(`message was: ${req}`);
+ }
+ if (reqJson !== undefined) {
+ res.onMessage(reqJson);
+ } else {
+ client.write("%error: invalid JSON");
+ client.end();
+ }
+ bodyChunks = [];
+ parsingBody = undefined;
+ } else {
+ bodyChunks.push(lineStr);
+ }
+ } else {
+ logger.info("invalid parser state");
+ client.write("%error: internal error\n");
+ client.end();
+ }
+ },
+ });
+ client.on("close", () => {
+ res.onDisconnect();
+ });
+ client.on("data", () => {});
+ resolve(res.result);
+ });
+ });
+}
+
+export async function runRpcServer(args: RpcServerArgs): Promise<void> {
+ let sockFilename = args.socketFilename;
+ try {
+ fs.unlinkSync(sockFilename);
+ } catch (e) {
+ // Do nothing!
+ }
+ return new Promise((resolve, reject) => {
+ const server = net.createServer((sock) => {
+ // Are we currently parsing the body of a request?
+ let parsingBody: string | undefined = undefined;
+ let bodyChunks: string[] = [];
+
+ sock.write("%hello-from-server\n");
+ const handlers = args.onConnect({
+ sendResponse(message) {
+ sock.write("%message\n");
+ sock.write(JSON.stringify(message));
+ sock.write("\n");
+ sock.write("%end\n");
+ },
+ });
+
+ sock.on("error", (err) => {
+ logger.error(`connection error: ${err}`);
+ });
+
+ function processLine(line: Uint8Array) {
+ const lineStr = bytesToString(line);
+ if (!parsingBody) {
+ const strippedLine = lineStr.trim();
+ if (strippedLine == "%request") {
+ parsingBody = "request";
+ } else if (strippedLine === "%hello-from-client") {
+ // Nothing to do, ignore hello
+ } else if (strippedLine.startsWith("%error:")) {
+ logger.warn("got error from client");
+ sock.end();
+ handlers.onDisconnect();
+ } else {
+ logger.info("got unknown request");
+ sock.write("%error: invalid request\n");
+ sock.end();
+ }
+ } else if (parsingBody == "request") {
+ const strippedLine = lineStr.trim();
+ if (strippedLine == "%end") {
+ let req = bodyChunks.join("");
+ let reqJson: any = undefined;
+ try {
+ reqJson = JSON.parse(req);
+ } catch (e) {
+ logger.warn("JSON request from client was invalid");
+ }
+ if (reqJson !== undefined) {
+ handlers.onMessage(reqJson);
+ } else {
+ sock.write("%error: invalid JSON");
+ sock.end();
+ }
+ bodyChunks = [];
+ parsingBody = undefined;
+ } else {
+ bodyChunks.push(lineStr);
+ }
+ } else {
+ logger.error("invalid parser state");
+ sock.write("%error: internal error\n");
+ sock.end();
+ }
+ }
+
+ readStreamLinewise({
+ sock,
+ onLine: processLine,
+ });
+
+ sock.on("close", (hadError: boolean) => {
+ logger.trace(`connection closed, hadError=${hadError}`);
+ handlers.onDisconnect();
+ });
+ });
+ server.listen(args.socketFilename);
+ });
+}
diff --git a/packages/taler-util/src/twrpc-impl.qtart.ts b/packages/taler-util/src/twrpc-impl.qtart.ts
new file mode 100644
index 000000000..7d7fa84ae
--- /dev/null
+++ b/packages/taler-util/src/twrpc-impl.qtart.ts
@@ -0,0 +1,26 @@
+/*
+ This file is part of GNU Taler
+ (C) 2023 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 type { RpcConnectArgs, RpcServerArgs } from "./twrpc.js";
+
+// Not implemented.
+export async function connectRpc<T>(args: RpcConnectArgs<T>): Promise<T> {
+ throw Error("not implemented");
+}
+
+export async function runRpcServer(args: RpcServerArgs): Promise<void> {
+ throw Error("not implemented");
+}
diff --git a/packages/taler-util/src/twrpc.ts b/packages/taler-util/src/twrpc.ts
new file mode 100644
index 000000000..d221630d0
--- /dev/null
+++ b/packages/taler-util/src/twrpc.ts
@@ -0,0 +1,63 @@
+/*
+ This file is part of GNU Taler
+ (C) 2023 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 { CoreApiResponse } from "./wallet-types.js";
+
+/**
+ * Implementation for the wallet-core IPC protocol.
+ *
+ * Currently the protocol is completely unstable and only used internally
+ * by the wallet for testing purposes.
+ */
+
+// Platform-specific implementation
+export { connectRpc, runRpcServer } from "#twrpc-impl";
+
+export type JsonMessage =
+ | string
+ | number
+ | boolean
+ | null
+ | JsonMessage[]
+ | { [key: string]: JsonMessage };
+
+export interface RpcServerClientHandlers {
+ onMessage(msg: JsonMessage): void;
+ onDisconnect(): void;
+}
+
+export interface RpcServerClient {
+ sendResponse(message: JsonMessage): void;
+}
+
+export interface RpcServerArgs {
+ socketFilename: string;
+ onConnect(client: RpcServerClient): RpcServerClientHandlers;
+}
+
+export interface RpcClientServerConnection {
+ sendMessage(m: JsonMessage): void;
+ close(): void;
+}
+
+export interface RpcConnectArgs<T> {
+ socketFilename: string;
+ onEstablished(connection: RpcClientServerConnection): {
+ result: T;
+ onDisconnect(): void;
+ onMessage(m: JsonMessage): void;
+ };
+}
diff --git a/packages/taler-util/src/wallet-types.ts b/packages/taler-util/src/wallet-types.ts
index 61f2f3b73..dd8eb29a2 100644
--- a/packages/taler-util/src/wallet-types.ts
+++ b/packages/taler-util/src/wallet-types.ts
@@ -27,16 +27,14 @@
/**
* Imports.
*/
-import {
- AmountJson,
- codecForAmountJson,
- codecForAmountString,
-} from "./amounts.js";
+import { AmountJson, codecForAmountString } from "./amounts.js";
import { BackupRecovery } from "./backup-types.js";
import {
+ Codec,
+ Context,
+ DecodingError,
buildCodecForObject,
buildCodecForUnion,
- Codec,
codecForAny,
codecForBoolean,
codecForConstString,
@@ -46,47 +44,133 @@ import {
codecForNumber,
codecForString,
codecOptional,
+ renderContext,
} from "./codec.js";
+import {
+ CurrencySpecification,
+ TalerMerchantApi,
+ TemplateParams,
+ WithdrawalOperationStatus,
+ canonicalizeBaseUrl,
+} from "./index.js";
import { VersionMatchResult } from "./libtool-version.js";
import { PaytoUri } from "./payto.js";
import { AgeCommitmentProof } from "./taler-crypto.js";
import { TalerErrorCode } from "./taler-error-codes.js";
import {
+ AccountRestriction,
AmountString,
AuditorDenomSig,
- codecForMerchantContractTerms,
CoinEnvelope,
- MerchantContractTerms,
- PeerContractTerms,
- DenominationPubKey,
DenomKeyType,
+ DenominationPubKey,
+ EddsaPrivateKeyString,
ExchangeAuditor,
+ ExchangeWireAccount,
+ InternationalizedString,
+ MerchantContractTerms,
+ MerchantInfo,
+ PeerContractTerms,
UnblindedSignature,
+ codecForExchangeWireAccount,
+ codecForMerchantContractTerms,
codecForPeerContractTerms,
} from "./taler-types.js";
import {
AbsoluteTime,
- codecForAbsoluteTime,
- codecForTimestamp,
+ TalerPreciseTimestamp,
TalerProtocolDuration,
TalerProtocolTimestamp,
+ codecForAbsoluteTime,
+ codecForPreciseTimestamp,
+ codecForTimestamp,
} from "./time.js";
import {
- codecForOrderShortInfo,
OrderShortInfo,
+ TransactionState,
+ TransactionType,
} from "./transactions-types.js";
/**
* Identifier for a transaction in the wallet.
*/
-export type TransactionIdStr = `txn:${string}:${string}`;
+declare const __txId: unique symbol;
+export type TransactionIdStr = `txn:${string}:${string}` & { [__txId]: true };
/**
* Identifier for a pending task in the wallet.
*/
-export type PendingIdStr = `pnd:${string}:${string}`;
-
-export type TombstoneIdStr = `tmb:${string}:${string}`;
+declare const __pndId: unique symbol;
+export type PendingIdStr = `pnd:${string}:${string}` & { [__pndId]: true };
+
+declare const __tmbId: unique symbol;
+export type TombstoneIdStr = `tmb:${string}:${string}` & { [__tmbId]: true };
+
+function codecForTransactionIdStr(): Codec<TransactionIdStr> {
+ return {
+ decode(x: any, c?: Context): TransactionIdStr {
+ if (typeof x === "string" && x.startsWith("txn:")) {
+ return x as TransactionIdStr;
+ }
+ throw new DecodingError(
+ `expected string starting with "txn:" at ${renderContext(
+ c,
+ )} but got ${x}`,
+ );
+ },
+ };
+}
+
+function codecForPendingIdStr(): Codec<PendingIdStr> {
+ return {
+ decode(x: any, c?: Context): PendingIdStr {
+ if (typeof x === "string" && x.startsWith("txn:")) {
+ return x as PendingIdStr;
+ }
+ throw new DecodingError(
+ `expected string starting with "txn:" at ${renderContext(
+ c,
+ )} but got ${x}`,
+ );
+ },
+ };
+}
+
+function codecForTombstoneIdStr(): Codec<TombstoneIdStr> {
+ return {
+ decode(x: any, c?: Context): TombstoneIdStr {
+ if (typeof x === "string" && x.startsWith("tmb:")) {
+ return x as TombstoneIdStr;
+ }
+ throw new DecodingError(
+ `expected string starting with "tmb:" at ${renderContext(
+ c,
+ )} but got ${x}`,
+ );
+ },
+ };
+}
+
+export function codecForCanonBaseUrl(): Codec<string> {
+ return {
+ decode(x: any, c?: Context): string {
+ if (typeof x === "string") {
+ const canon = canonicalizeBaseUrl(x);
+ if (x !== canon) {
+ throw new DecodingError(
+ `expected canonicalized base URL at ${renderContext(
+ c,
+ )} but got value '${x}'`,
+ );
+ }
+ return x;
+ }
+ throw new DecodingError(
+ `expected base URL at ${renderContext(c)} but got type ${typeof x}`,
+ );
+ },
+ };
+}
/**
* Response for the create reserve request to the wallet.
@@ -104,35 +188,399 @@ export class CreateReserveResponse {
reservePub: string;
}
-export interface Balance {
+export interface GetBalanceDetailRequest {
+ currency: string;
+}
+
+export const codecForGetBalanceDetailRequest =
+ (): Codec<GetBalanceDetailRequest> =>
+ buildCodecForObject<GetBalanceDetailRequest>()
+ .property("currency", codecForString())
+ .build("GetBalanceDetailRequest");
+
+/**
+ * How the amount should be interpreted in a transaction
+ * Effective = how the balance is change
+ * Raw = effective amount without fee
+ *
+ * Depending on the transaction, raw can be higher than effective
+ */
+export enum TransactionAmountMode {
+ Effective = "effective",
+ Raw = "raw",
+}
+
+export type GetPlanForOperationRequest =
+ | GetPlanForWithdrawRequest
+ | GetPlanForDepositRequest;
+// | GetPlanForPushDebitRequest
+// | GetPlanForPullCreditRequest
+// | GetPlanForPaymentRequest
+// | GetPlanForTipRequest
+// | GetPlanForRefundRequest
+// | GetPlanForPullDebitRequest
+// | GetPlanForPushCreditRequest;
+
+interface GetPlanForWalletInitiatedOperation {
+ instructedAmount: AmountString;
+ mode: TransactionAmountMode;
+}
+
+export interface ConvertAmountRequest {
+ amount: AmountString;
+ type: TransactionAmountMode;
+}
+
+export const codecForConvertAmountRequest =
+ buildCodecForObject<ConvertAmountRequest>()
+ .property("amount", codecForAmountString())
+ .property(
+ "type",
+ codecForEither(
+ codecForConstString(TransactionAmountMode.Raw),
+ codecForConstString(TransactionAmountMode.Effective),
+ ),
+ )
+ .build("ConvertAmountRequest");
+
+export interface GetAmountRequest {
+ currency: string;
+}
+
+export const codecForGetAmountRequest = buildCodecForObject<GetAmountRequest>()
+ .property("currency", codecForString())
+ .build("GetAmountRequest");
+
+interface GetPlanToCompleteOperation {
+ instructedAmount: AmountString;
+}
+
+const codecForGetPlanForWalletInitiatedOperation = <
+ T extends GetPlanForWalletInitiatedOperation,
+>() =>
+ buildCodecForObject<T>()
+ .property(
+ "mode",
+ codecForEither(
+ codecForConstString(TransactionAmountMode.Raw),
+ codecForConstString(TransactionAmountMode.Effective),
+ ),
+ )
+ .property("instructedAmount", codecForAmountString());
+
+interface GetPlanForWithdrawRequest extends GetPlanForWalletInitiatedOperation {
+ type: TransactionType.Withdrawal;
+ exchangeUrl?: string;
+}
+interface GetPlanForDepositRequest extends GetPlanForWalletInitiatedOperation {
+ type: TransactionType.Deposit;
+ account: string; //payto string
+}
+interface GetPlanForPushDebitRequest
+ extends GetPlanForWalletInitiatedOperation {
+ type: TransactionType.PeerPushDebit;
+}
+
+interface GetPlanForPullCreditRequest
+ extends GetPlanForWalletInitiatedOperation {
+ type: TransactionType.PeerPullCredit;
+ exchangeUrl: string;
+}
+
+const codecForGetPlanForWithdrawRequest =
+ codecForGetPlanForWalletInitiatedOperation<GetPlanForWithdrawRequest>()
+ .property("type", codecForConstString(TransactionType.Withdrawal))
+ .property("exchangeUrl", codecOptional(codecForString()))
+ .build("GetPlanForWithdrawRequest");
+
+const codecForGetPlanForDepositRequest =
+ codecForGetPlanForWalletInitiatedOperation<GetPlanForDepositRequest>()
+ .property("type", codecForConstString(TransactionType.Deposit))
+ .property("account", codecForString())
+ .build("GetPlanForDepositRequest");
+
+const codecForGetPlanForPushDebitRequest =
+ codecForGetPlanForWalletInitiatedOperation<GetPlanForPushDebitRequest>()
+ .property("type", codecForConstString(TransactionType.PeerPushDebit))
+ .build("GetPlanForPushDebitRequest");
+
+const codecForGetPlanForPullCreditRequest =
+ codecForGetPlanForWalletInitiatedOperation<GetPlanForPullCreditRequest>()
+ .property("type", codecForConstString(TransactionType.PeerPullCredit))
+ .property("exchangeUrl", codecForString())
+ .build("GetPlanForPullCreditRequest");
+
+interface GetPlanForPaymentRequest extends GetPlanToCompleteOperation {
+ type: TransactionType.Payment;
+ wireMethod: string;
+ ageRestriction: number;
+ maxDepositFee: AmountString;
+}
+
+interface GetPlanForPullDebitRequest extends GetPlanToCompleteOperation {
+ type: TransactionType.PeerPullDebit;
+}
+
+interface GetPlanForPushCreditRequest extends GetPlanToCompleteOperation {
+ type: TransactionType.PeerPushCredit;
+}
+
+const codecForGetPlanForPaymentRequest =
+ buildCodecForObject<GetPlanForPaymentRequest>()
+ .property("type", codecForConstString(TransactionType.Payment))
+ .property("maxDepositFee", codecForAmountString())
+ .build("GetPlanForPaymentRequest");
+
+const codecForGetPlanForPullDebitRequest =
+ buildCodecForObject<GetPlanForPullDebitRequest>()
+ .property("type", codecForConstString(TransactionType.PeerPullDebit))
+ .build("GetPlanForPullDebitRequest");
+
+const codecForGetPlanForPushCreditRequest =
+ buildCodecForObject<GetPlanForPushCreditRequest>()
+ .property("type", codecForConstString(TransactionType.PeerPushCredit))
+ .build("GetPlanForPushCreditRequest");
+
+export const codecForGetPlanForOperationRequest =
+ (): Codec<GetPlanForOperationRequest> =>
+ buildCodecForUnion<GetPlanForOperationRequest>()
+ .discriminateOn("type")
+ .alternative(
+ TransactionType.Withdrawal,
+ codecForGetPlanForWithdrawRequest,
+ )
+ .alternative(TransactionType.Deposit, codecForGetPlanForDepositRequest)
+ // .alternative(
+ // TransactionType.PeerPushDebit,
+ // codecForGetPlanForPushDebitRequest,
+ // )
+ // .alternative(
+ // TransactionType.PeerPullCredit,
+ // codecForGetPlanForPullCreditRequest,
+ // )
+ // .alternative(TransactionType.Payment, codecForGetPlanForPaymentRequest)
+ // .alternative(
+ // TransactionType.PeerPullDebit,
+ // codecForGetPlanForPullDebitRequest,
+ // )
+ // .alternative(
+ // TransactionType.PeerPushCredit,
+ // codecForGetPlanForPushCreditRequest,
+ // )
+ .build("GetPlanForOperationRequest");
+
+export interface GetPlanForOperationResponse {
+ effectiveAmount: AmountString;
+ rawAmount: AmountString;
+ counterPartyAmount?: AmountString;
+ details: any;
+}
+
+export const codecForGetPlanForOperationResponse =
+ (): Codec<GetPlanForOperationResponse> =>
+ buildCodecForObject<GetPlanForOperationResponse>()
+ .property("effectiveAmount", codecForAmountString())
+ .property("rawAmount", codecForAmountString())
+ .property("details", codecForAny())
+ .property("counterPartyAmount", codecOptional(codecForAmountString()))
+ .build("GetPlanForOperationResponse");
+
+export interface AmountResponse {
+ effectiveAmount: AmountString;
+ rawAmount: AmountString;
+}
+
+export const codecForAmountResponse = (): Codec<AmountResponse> =>
+ buildCodecForObject<AmountResponse>()
+ .property("effectiveAmount", codecForAmountString())
+ .property("rawAmount", codecForAmountString())
+ .build("AmountResponse");
+
+export enum BalanceFlag {
+ IncomingKyc = "incoming-kyc",
+ IncomingAml = "incoming-aml",
+ IncomingConfirmation = "incoming-confirmation",
+ OutgoingKyc = "outgoing-kyc",
+}
+
+export interface WalletBalance {
+ scopeInfo: ScopeInfo;
available: AmountString;
pendingIncoming: AmountString;
pendingOutgoing: AmountString;
- // Does the balance for this currency have a pending
- // transaction?
+ /**
+ * Does the balance for this currency have a pending
+ * transaction?
+ *
+ * @deprecated use flags and pendingIncoming/pendingOutgoing instead
+ */
hasPendingTransactions: boolean;
- // Is there a pending transaction that would affect the balance
- // and requires user input?
+ /**
+ * Is there a transaction that requires user input?
+ *
+ * @deprecated use flags instead
+ */
requiresUserInput: boolean;
+
+ flags: BalanceFlag[];
+}
+
+export const codecForScopeInfoGlobal = (): Codec<ScopeInfoGlobal> =>
+ buildCodecForObject<ScopeInfoGlobal>()
+ .property("currency", codecForString())
+ .property("type", codecForConstString(ScopeType.Global))
+ .build("ScopeInfoGlobal");
+
+export const codecForScopeInfoExchange = (): Codec<ScopeInfoExchange> =>
+ buildCodecForObject<ScopeInfoExchange>()
+ .property("currency", codecForString())
+ .property("type", codecForConstString(ScopeType.Exchange))
+ .property("url", codecForString())
+ .build("ScopeInfoExchange");
+
+export const codecForScopeInfoAuditor = (): Codec<ScopeInfoAuditor> =>
+ buildCodecForObject<ScopeInfoAuditor>()
+ .property("currency", codecForString())
+ .property("type", codecForConstString(ScopeType.Auditor))
+ .property("url", codecForString())
+ .build("ScopeInfoAuditor");
+
+export const codecForScopeInfo = (): Codec<ScopeInfo> =>
+ buildCodecForUnion<ScopeInfo>()
+ .discriminateOn("type")
+ .alternative(ScopeType.Global, codecForScopeInfoGlobal())
+ .alternative(ScopeType.Exchange, codecForScopeInfoExchange())
+ .alternative(ScopeType.Auditor, codecForScopeInfoAuditor())
+ .build("ScopeInfo");
+
+export interface GetCurrencySpecificationRequest {
+ scope: ScopeInfo;
+}
+
+export const codecForGetCurrencyInfoRequest =
+ (): Codec<GetCurrencySpecificationRequest> =>
+ buildCodecForObject<GetCurrencySpecificationRequest>()
+ .property("scope", codecForScopeInfo())
+ .build("GetCurrencySpecificationRequest");
+
+export interface ListExchangesForScopedCurrencyRequest {
+ scope: ScopeInfo;
+}
+
+export const codecForListExchangesForScopedCurrencyRequest =
+ (): Codec<ListExchangesForScopedCurrencyRequest> =>
+ buildCodecForObject<ListExchangesForScopedCurrencyRequest>()
+ .property("scope", codecForScopeInfo())
+ .build("ListExchangesForScopedCurrencyRequest");
+
+export interface GetCurrencySpecificationResponse {
+ currencySpecification: CurrencySpecification;
+}
+
+export interface BuiltinExchange {
+ exchangeBaseUrl: string;
+ currencyHint: string;
+}
+
+export interface PartialWalletRunConfig {
+ builtin?: Partial<WalletRunConfig["builtin"]>;
+ testing?: Partial<WalletRunConfig["testing"]>;
+ features?: Partial<WalletRunConfig["features"]>;
+ lazyTaskLoop?: Partial<WalletRunConfig["lazyTaskLoop"]>;
+}
+
+export interface WalletRunConfig {
+ /**
+ * Initialization values useful for a complete startup.
+ *
+ * These are values may be overridden by different wallets
+ */
+ builtin: {
+ exchanges: BuiltinExchange[];
+ };
+
+ /**
+ * Unsafe options which it should only be used to create
+ * testing environment.
+ */
+ testing: {
+ /**
+ * Allow withdrawal of denominations even though they are about to expire.
+ */
+ denomselAllowLate: boolean;
+ devModeActive: boolean;
+ insecureTrustExchange: boolean;
+ preventThrottling: boolean;
+ skipDefaults: boolean;
+ emitObservabilityEvents?: boolean;
+ };
+
+ /**
+ * Configurations values that may be safe to show to the user
+ */
+ features: {
+ allowHttp: boolean;
+ };
+
+ /**
+ * Start processing tasks only when explicitly required, even after
+ * init has been called.
+ *
+ * Useful when the wallet is started to make single read-only request,
+ * as otherwise wallet-core starts making network request and process
+ * unrelated pending tasks.
+ */
+ lazyTaskLoop: boolean;
+}
+
+export interface InitRequest {
+ config?: PartialWalletRunConfig;
}
+export const codecForInitRequest = (): Codec<InitRequest> =>
+ buildCodecForObject<InitRequest>()
+ .property("config", codecForAny())
+ .build("InitRequest");
+
export interface InitResponse {
versionInfo: WalletCoreVersion;
}
+export enum ScopeType {
+ Global = "global",
+ Exchange = "exchange",
+ Auditor = "auditor",
+}
+
+export type ScopeInfoGlobal = { type: ScopeType.Global; currency: string };
+export type ScopeInfoExchange = {
+ type: ScopeType.Exchange;
+ currency: string;
+ url: string;
+};
+export type ScopeInfoAuditor = {
+ type: ScopeType.Auditor;
+ currency: string;
+ url: string;
+};
+
+export type ScopeInfo = ScopeInfoGlobal | ScopeInfoExchange | ScopeInfoAuditor;
+
export interface BalancesResponse {
- balances: Balance[];
+ balances: WalletBalance[];
}
-export const codecForBalance = (): Codec<Balance> =>
- buildCodecForObject<Balance>()
- .property("available", codecForString())
+export const codecForBalance = (): Codec<WalletBalance> =>
+ buildCodecForObject<WalletBalance>()
+ .property("scopeInfo", codecForAny()) // FIXME
+ .property("available", codecForAmountString())
.property("hasPendingTransactions", codecForBoolean())
- .property("pendingIncoming", codecForString())
- .property("pendingOutgoing", codecForString())
+ .property("pendingIncoming", codecForAmountString())
+ .property("pendingOutgoing", codecForAmountString())
.property("requiresUserInput", codecForBoolean())
+ .property("flags", codecForAny()) // FIXME
.build("Balance");
export const codecForBalancesResponse = (): Codec<BalancesResponse> =>
@@ -161,6 +609,11 @@ export enum CoinStatus {
Fresh = "fresh",
/**
+ * Coin was lost as the denomination is not usable anymore.
+ */
+ DenomLoss = "denom-loss",
+
+ /**
* Fresh, but currently marked as "suspended", thus won't be used
* for spending. Used for testing.
*/
@@ -210,11 +663,11 @@ export interface CoinDumpJson {
withdrawal_reserve_pub: string | undefined;
coin_status: CoinStatus;
spend_allocation:
- | {
- id: string;
- amount: string;
- }
- | undefined;
+ | {
+ id: string;
+ amount: AmountString;
+ }
+ | undefined;
/**
* Information about the age restriction
*/
@@ -233,18 +686,19 @@ export enum ConfirmPayResultType {
export interface ConfirmPayResultDone {
type: ConfirmPayResultType.Done;
contractTerms: MerchantContractTerms;
- transactionId: string;
+ transactionId: TransactionIdStr;
}
export interface ConfirmPayResultPending {
type: ConfirmPayResultType.Pending;
- transactionId: string;
+ transactionId: TransactionIdStr;
lastError: TalerErrorDetail | undefined;
}
export const codecForTalerErrorDetail = (): Codec<TalerErrorDetail> =>
buildCodecForObject<TalerErrorDetail>()
.property("code", codecForNumber())
+ .property("when", codecOptional(codecForAbsoluteTime))
.property("hint", codecOptional(codecForString()))
.build("TalerErrorDetail");
@@ -254,14 +708,14 @@ export const codecForConfirmPayResultPending =
(): Codec<ConfirmPayResultPending> =>
buildCodecForObject<ConfirmPayResultPending>()
.property("lastError", codecOptional(codecForTalerErrorDetail()))
- .property("transactionId", codecForString())
+ .property("transactionId", codecForTransactionIdStr())
.property("type", codecForConstString(ConfirmPayResultType.Pending))
.build("ConfirmPayResultPending");
export const codecForConfirmPayResultDone = (): Codec<ConfirmPayResultDone> =>
buildCodecForObject<ConfirmPayResultDone>()
.property("type", codecForConstString(ConfirmPayResultType.Done))
- .property("transactionId", codecForString())
+ .property("transactionId", codecForTransactionIdStr())
.property("contractTerms", codecForMerchantContractTerms())
.build("ConfirmPayResultDone");
@@ -320,62 +774,6 @@ export interface PrepareRefundResult {
info: OrderShortInfo;
}
-export interface PrepareTipResult {
- /**
- * Unique ID for the tip assigned by the wallet.
- * Typically different from the merchant-generated tip ID.
- */
- walletTipId: string;
-
- /**
- * Has the tip already been accepted?
- */
- accepted: boolean;
-
- /**
- * Amount that the merchant gave.
- */
- tipAmountRaw: AmountString;
-
- /**
- * Amount that arrived at the wallet.
- * Might be lower than the raw amount due to fees.
- */
- tipAmountEffective: AmountString;
-
- /**
- * Base URL of the merchant backend giving then tip.
- */
- merchantBaseUrl: string;
-
- /**
- * Base URL of the exchange that is used to withdraw the tip.
- * Determined by the merchant, the wallet/user has no choice here.
- */
- exchangeBaseUrl: string;
-
- /**
- * Time when the tip will expire. After it expired, it can't be picked
- * up anymore.
- */
- expirationTimestamp: TalerProtocolTimestamp;
-}
-
-export interface AcceptTipResponse {
- transactionId: string;
-}
-
-export const codecForPrepareTipResult = (): Codec<PrepareTipResult> =>
- buildCodecForObject<PrepareTipResult>()
- .property("accepted", codecForBoolean())
- .property("tipAmountRaw", codecForAmountString())
- .property("tipAmountEffective", codecForAmountString())
- .property("exchangeBaseUrl", codecForString())
- .property("merchantBaseUrl", codecForString())
- .property("expirationTimestamp", codecForTimestamp)
- .property("walletTipId", codecForString())
- .build("PrepareTipResult");
-
export interface BenchmarkResult {
time: { [s: string]: number };
repetitions: number;
@@ -393,16 +791,92 @@ export const codecForPreparePayResultPaymentPossible =
.property("amountEffective", codecForAmountString())
.property("amountRaw", codecForAmountString())
.property("contractTerms", codecForMerchantContractTerms())
+ .property("transactionId", codecForTransactionIdStr())
.property("proposalId", codecForString())
.property("contractTermsHash", codecForString())
.property("talerUri", codecForString())
- .property("noncePriv", codecForString())
.property(
"status",
codecForConstString(PreparePayResultType.PaymentPossible),
)
.build("PreparePayResultPaymentPossible");
+export interface BalanceDetails { }
+
+/**
+ * Detailed reason for why the wallet's balance is insufficient.
+ */
+export interface PaymentInsufficientBalanceDetails {
+ /**
+ * Amount requested by the merchant.
+ */
+ amountRequested: AmountString;
+
+ /**
+ * Balance of type "available" (see balance.ts for definition).
+ */
+ balanceAvailable: AmountString;
+
+ /**
+ * Balance of type "material" (see balance.ts for definition).
+ */
+ balanceMaterial: AmountString;
+
+ /**
+ * Balance of type "age-acceptable" (see balance.ts for definition).
+ */
+ balanceAgeAcceptable: AmountString;
+
+ /**
+ * Balance of type "merchant-acceptable" (see balance.ts for definition).
+ */
+ balanceReceiverAcceptable: AmountString;
+
+ /**
+ * Balance of type "merchant-depositable" (see balance.ts for definition).
+ */
+ balanceReceiverDepositable: AmountString;
+
+ balanceExchangeDepositable: AmountString;
+
+ /**
+ * Maximum effective amount that the wallet can spend,
+ * when all fees are paid by the wallet.
+ */
+ maxEffectiveSpendAmount: AmountString;
+
+ perExchange: {
+ [url: string]: {
+ balanceAvailable: AmountString;
+ balanceMaterial: AmountString;
+ balanceExchangeDepositable: AmountString;
+ balanceAgeAcceptable: AmountString;
+ balanceReceiverAcceptable: AmountString;
+ balanceReceiverDepositable: AmountString;
+ maxEffectiveSpendAmount: AmountString;
+ /**
+ * Exchange doesn't have global fees configured for the relevant year,
+ * p2p payments aren't possible.
+ */
+ missingGlobalFees: boolean;
+ };
+ };
+}
+
+export const codecForPayMerchantInsufficientBalanceDetails =
+ (): Codec<PaymentInsufficientBalanceDetails> =>
+ buildCodecForObject<PaymentInsufficientBalanceDetails>()
+ .property("amountRequested", codecForAmountString())
+ .property("balanceAgeAcceptable", codecForAmountString())
+ .property("balanceAvailable", codecForAmountString())
+ .property("balanceMaterial", codecForAmountString())
+ .property("balanceReceiverAcceptable", codecForAmountString())
+ .property("balanceReceiverDepositable", codecForAmountString())
+ .property("balanceExchangeDepositable", codecForAmountString())
+ .property("perExchange", codecForAny())
+ .property("maxEffectiveSpendAmount", codecForAmountString())
+ .build("PayMerchantInsufficientBalanceDetails");
+
export const codecForPreparePayResultInsufficientBalance =
(): Codec<PreparePayResultInsufficientBalance> =>
buildCodecForObject<PreparePayResultInsufficientBalance>()
@@ -410,11 +884,15 @@ export const codecForPreparePayResultInsufficientBalance =
.property("contractTerms", codecForAny())
.property("talerUri", codecForString())
.property("proposalId", codecForString())
- .property("noncePriv", codecForString())
+ .property("transactionId", codecForTransactionIdStr())
.property(
"status",
codecForConstString(PreparePayResultType.InsufficientBalance),
)
+ .property(
+ "balanceDetails",
+ codecForPayMerchantInsufficientBalanceDetails(),
+ )
.build("PreparePayResultInsufficientBalance");
export const codecForPreparePayResultAlreadyConfirmed =
@@ -424,12 +902,13 @@ export const codecForPreparePayResultAlreadyConfirmed =
"status",
codecForConstString(PreparePayResultType.AlreadyConfirmed),
)
- .property("amountEffective", codecForAmountString())
+ .property("amountEffective", codecOptional(codecForAmountString()))
.property("amountRaw", codecForAmountString())
.property("paid", codecForBoolean())
- .property("talerUri", codecOptional(codecForString()))
+ .property("talerUri", codecForString())
.property("contractTerms", codecForAny())
.property("contractTermsHash", codecForString())
+ .property("transactionId", codecForTransactionIdStr())
.property("proposalId", codecForString())
.build("PreparePayResultAlreadyConfirmed");
@@ -463,49 +942,61 @@ export type PreparePayResult =
*/
export interface PreparePayResultPaymentPossible {
status: PreparePayResultType.PaymentPossible;
+ transactionId: TransactionIdStr;
+ /**
+ * @deprecated use transactionId instead
+ */
proposalId: string;
contractTerms: MerchantContractTerms;
contractTermsHash: string;
- amountRaw: string;
- amountEffective: string;
- noncePriv: string;
+ amountRaw: AmountString;
+ amountEffective: AmountString;
talerUri: string;
}
export interface PreparePayResultInsufficientBalance {
status: PreparePayResultType.InsufficientBalance;
+ transactionId: TransactionIdStr;
+ /**
+ * @deprecated use transactionId
+ */
proposalId: string;
contractTerms: MerchantContractTerms;
- amountRaw: string;
- noncePriv: string;
+ amountRaw: AmountString;
talerUri: string;
+ balanceDetails: PaymentInsufficientBalanceDetails;
}
export interface PreparePayResultAlreadyConfirmed {
status: PreparePayResultType.AlreadyConfirmed;
+ transactionId: TransactionIdStr;
contractTerms: MerchantContractTerms;
paid: boolean;
- amountRaw: string;
- amountEffective: string;
+ amountRaw: AmountString;
+ amountEffective: AmountString | undefined;
contractTermsHash: string;
+ /**
+ * @deprecated use transactionId
+ */
proposalId: string;
- talerUri?: string;
+ talerUri: string;
}
export interface BankWithdrawDetails {
- selectionDone: boolean;
- transferDone: boolean;
+ status: WithdrawalOperationStatus;
amount: AmountJson;
senderWire?: string;
suggestedExchange?: string;
confirmTransferUrl?: string;
wireTypes: string[];
+ operationId: string;
+ apiBaseUrl: string;
}
export interface AcceptWithdrawalResponse {
reservePub: string;
confirmTransferUrl?: string;
- transactionId: string;
+ transactionId: TransactionIdStr;
}
/**
@@ -528,6 +1019,7 @@ export interface WalletDiagnostics {
export interface TalerErrorDetail {
code: TalerErrorCode;
+ when?: AbsoluteTime;
hint?: string;
[x: string]: unknown;
}
@@ -535,7 +1027,7 @@ export interface TalerErrorDetail {
/**
* Minimal information needed about a planchet for unblinding a signature.
*
- * Can be a withdrawal/tipping/refresh planchet.
+ * Can be a withdrawal/refresh planchet.
*/
export interface PlanchetUnblindInfo {
denomPub: DenominationPubKey;
@@ -578,6 +1070,9 @@ export enum RefreshReason {
PayPeerPull = "pay-peer-pull",
Refund = "refund",
AbortPay = "abort-pay",
+ AbortDeposit = "abort-deposit",
+ AbortPeerPushDebit = "abort-peer-push-debit",
+ AbortPeerPullDebit = "abort-peer-pull-debit",
Recoup = "recoup",
BackupRestored = "backup-restored",
Scheduled = "scheduled",
@@ -592,13 +1087,6 @@ export interface CoinRefreshRequest {
}
/**
- * Wrapper for refresh group IDs.
- */
-export interface RefreshGroupId {
- readonly refreshGroupId: string;
-}
-
-/**
* Private data required to make a deposit permission.
*/
export interface DepositInfo {
@@ -621,6 +1109,10 @@ export interface DepositInfo {
ageCommitmentProof?: AgeCommitmentProof;
}
+export interface ExchangesShortListResponse {
+ exchanges: ShortExchangeListItem[];
+}
+
export interface ExchangesListResponse {
exchanges: ExchangeListItem[];
}
@@ -630,11 +1122,34 @@ export interface ExchangeDetailedResponse {
}
export interface WalletCoreVersion {
- hash: string | undefined;
+ implementationSemver: string;
+ implementationGitHash: string;
+
+ /**
+ * Wallet-core protocol version supported by this implementation
+ * of the API ("server" version).
+ */
version: string;
exchange: string;
merchant: string;
+
+ bankIntegrationApiRange: string;
+ bankConversionApiRange: string;
+ corebankApiRange: string;
+
+ /**
+ * @deprecated as bank was split into multiple APIs with separate versioning
+ */
bank: string;
+
+ /**
+ * @deprecated
+ */
+ hash: string | undefined;
+
+ /**
+ * @deprecated will be removed
+ */
devMode: boolean;
}
@@ -649,13 +1164,6 @@ export interface KnownBankAccounts {
accounts: KnownBankAccountsInfo[];
}
-export interface ExchangeTosStatusDetails {
- acceptedVersion?: string;
- currentVersion?: string;
- contentType?: string;
- content?: string;
-}
-
/**
* Wire fee for one wire method
*/
@@ -686,19 +1194,11 @@ export interface WireFee {
sig: string;
}
-/**
- * Information about one of the exchange's bank accounts.
- */
-export interface ExchangeAccount {
- payto_uri: string;
- master_sig: string;
-}
-
export type WireFeeMap = { [wireMethod: string]: WireFee[] };
export interface WireInfo {
feesForType: WireFeeMap;
- accounts: ExchangeAccount[];
+ accounts: ExchangeWireAccount[];
}
export interface ExchangeGlobalFees {
@@ -717,12 +1217,6 @@ export interface ExchangeGlobalFees {
signature: string;
}
-const codecForExchangeAccount = (): Codec<ExchangeAccount> =>
- buildCodecForObject<ExchangeAccount>()
- .property("payto_uri", codecForString())
- .property("master_sig", codecForString())
- .build("codecForExchangeAccount");
-
const codecForWireFee = (): Codec<WireFee> =>
buildCodecForObject<WireFee>()
.property("sig", codecForString())
@@ -735,7 +1229,7 @@ const codecForWireFee = (): Codec<WireFee> =>
const codecForWireInfo = (): Codec<WireInfo> =>
buildCodecForObject<WireInfo>()
.property("feesForType", codecForMap(codecForList(codecForWireFee())))
- .property("accounts", codecForList(codecForExchangeAccount()))
+ .property("accounts", codecForList(codecForExchangeWireAccount()))
.build("codecForWireInfo");
export interface DenominationInfo {
@@ -826,7 +1320,6 @@ export interface ExchangeFullDetails {
exchangeBaseUrl: string;
currency: string;
paytoUris: string[];
- tos: ExchangeTosStatusDetails;
auditors: ExchangeAuditor[];
wireInfo: WireInfo;
denomFees: DenomOperationMap<FeeDescription[]>;
@@ -835,36 +1328,61 @@ export interface ExchangeFullDetails {
}
export enum ExchangeTosStatus {
- New = "new",
+ Pending = "pending",
+ Proposed = "proposed",
Accepted = "accepted",
- Changed = "changed",
- NotFound = "not-found",
- Unknown = "unknown",
}
export enum ExchangeEntryStatus {
- Unknown = "unknown",
- Outdated = "outdated",
- Ok = "ok",
+ Preset = "preset",
+ Ephemeral = "ephemeral",
+ Used = "used",
+}
+
+export enum ExchangeUpdateStatus {
+ Initial = "initial",
+ InitialUpdate = "initial-update",
+ Suspended = "suspended",
+ UnavailableUpdate = "unavailable-update",
+ Ready = "ready",
+ ReadyUpdate = "ready-update",
}
export interface OperationErrorInfo {
error: TalerErrorDetail;
}
-// FIXME: This should probably include some error status.
+export interface ShortExchangeListItem {
+ exchangeBaseUrl: string;
+}
+
+/**
+ * Info about an exchange entry in the wallet.
+ */
export interface ExchangeListItem {
exchangeBaseUrl: string;
- currency: string | undefined;
+ masterPub: string | undefined;
+ currency: string;
paytoUris: string[];
tosStatus: ExchangeTosStatus;
- exchangeStatus: ExchangeEntryStatus;
+ exchangeEntryStatus: ExchangeEntryStatus;
+ exchangeUpdateStatus: ExchangeUpdateStatus;
ageRestrictionOptions: number[];
+
/**
- * Permanently added to the wallet, as opposed to just
- * temporarily queried.
+ * P2P payments are disabled with this exchange
+ * (e.g. because no global fees are configured).
*/
- permanent: boolean;
+ peerPaymentsDisabled: boolean;
+
+ /**
+ * Set to true if this exchange doesn't charge any fees.
+ */
+ noFees: boolean;
+
+ scopeInfo: ScopeInfo;
+
+ lastUpdateTimestamp: TalerPreciseTimestamp | undefined;
/**
* Information about the last error that occurred when trying
@@ -886,14 +1404,6 @@ const codecForExchangeAuditor = (): Codec<ExchangeAuditor> =>
.property("denomination_keys", codecForList(codecForAuditorDenomSig()))
.build("codecForExchangeAuditor");
-const codecForExchangeTos = (): Codec<ExchangeTosStatusDetails> =>
- buildCodecForObject<ExchangeTosStatusDetails>()
- .property("acceptedVersion", codecOptional(codecForString()))
- .property("currentVersion", codecOptional(codecForString()))
- .property("contentType", codecOptional(codecForString()))
- .property("content", codecOptional(codecForString()))
- .build("ExchangeTos");
-
export const codecForFeeDescriptionPair = (): Codec<FeeDescriptionPair> =>
buildCodecForObject<FeeDescriptionPair>()
.property("group", codecForString())
@@ -924,9 +1434,8 @@ export const codecForFeesByOperations = (): Codec<
export const codecForExchangeFullDetails = (): Codec<ExchangeFullDetails> =>
buildCodecForObject<ExchangeFullDetails>()
.property("currency", codecForString())
- .property("exchangeBaseUrl", codecForString())
+ .property("exchangeBaseUrl", codecForCanonBaseUrl())
.property("paytoUris", codecForList(codecForString()))
- .property("tos", codecForExchangeTos())
.property("auditors", codecForList(codecForExchangeAuditor()))
.property("wireInfo", codecForWireInfo())
.property("denomFees", codecForFeesByOperations())
@@ -940,12 +1449,18 @@ export const codecForExchangeFullDetails = (): Codec<ExchangeFullDetails> =>
export const codecForExchangeListItem = (): Codec<ExchangeListItem> =>
buildCodecForObject<ExchangeListItem>()
.property("currency", codecForString())
- .property("exchangeBaseUrl", codecForString())
+ .property("exchangeBaseUrl", codecForCanonBaseUrl())
+ .property("masterPub", codecOptional(codecForString()))
.property("paytoUris", codecForList(codecForString()))
.property("tosStatus", codecForAny())
- .property("exchangeStatus", codecForAny())
- .property("permanent", codecForBoolean())
+ .property("exchangeEntryStatus", codecForAny())
+ .property("exchangeUpdateStatus", codecForAny())
.property("ageRestrictionOptions", codecForList(codecForNumber()))
+ .property("scopeInfo", codecForScopeInfo())
+ .property("lastUpdateErrorInfo", codecForAny())
+ .property("lastUpdateTimestamp", codecOptional(codecForPreciseTimestamp))
+ .property("noFees", codecForBoolean())
+ .property("peerPaymentsDisabled", codecForBoolean())
.build("ExchangeListItem");
export const codecForExchangesListResponse = (): Codec<ExchangesListResponse> =>
@@ -956,6 +1471,8 @@ export const codecForExchangesListResponse = (): Codec<ExchangesListResponse> =>
export interface AcceptManualWithdrawalResult {
/**
* Payto URIs that can be used to fund the withdrawal.
+ *
+ * @deprecated in favor of withdrawalAccountsList
*/
exchangePaytoUris: string[];
@@ -964,13 +1481,17 @@ export interface AcceptManualWithdrawalResult {
*/
reservePub: string;
- transactionId: string;
+ withdrawalAccountsList: WithdrawalExchangeAccountDetails[];
+
+ transactionId: TransactionIdStr;
}
-export interface ManualWithdrawalDetails {
+export interface WithdrawalDetailsForAmount {
/**
* Did the user accept the current version of the exchange's
* terms of service?
+ *
+ * @deprecated the client should query the exchange entry instead
*/
tosAccepted: boolean;
@@ -985,15 +1506,44 @@ export interface ManualWithdrawalDetails {
amountEffective: AmountString;
/**
+ * Number of coins that would be used for withdrawal.
+ *
+ * The UIs should warn if this number is too high (roughly at >100).
+ */
+ numCoins: number;
+
+ /**
* Ways to pay the exchange.
+ *
+ * @deprecated in favor of withdrawalAccountsList
*/
paytoUris: string[];
/**
+ * Ways to pay the exchange, including accounts that require currency conversion.
+ */
+ withdrawalAccountsList: WithdrawalExchangeAccountDetails[];
+
+ /**
* If the exchange supports age-restricted coins it will return
* the array of ages.
*/
ageRestrictionOptions?: number[];
+
+ /**
+ * Scope info of the currency withdrawn.
+ */
+ scopeInfo: ScopeInfo;
+}
+
+export interface DenomSelItem {
+ denomPubHash: string;
+ count: number;
+ /**
+ * Number of denoms/planchets to skip, because
+ * a re-denomination effectively deleted them.
+ */
+ skip?: number;
}
/**
@@ -1002,10 +1552,9 @@ export interface ManualWithdrawalDetails {
export interface DenomSelectionState {
totalCoinValue: AmountString;
totalWithdrawCost: AmountString;
- selectedDenoms: {
- denomPubHash: string;
- count: number;
- }[];
+ selectedDenoms: DenomSelItem[];
+ earliestDepositExpiration: TalerProtocolTimestamp;
+ hasDenomWithAgeRestriction: boolean;
}
/**
@@ -1021,43 +1570,24 @@ export interface ExchangeWithdrawalDetails {
*/
exchangeWireAccounts: string[];
+ exchangeCreditAccountDetails: WithdrawalExchangeAccountDetails[];
+
/**
* Selected denominations for withdraw.
*/
selectedDenoms: DenomSelectionState;
/**
- * Does the wallet know about an auditor for
- * the exchange that the reserve.
- */
- isAudited: boolean;
-
- /**
* Did the user already accept the current terms of service for the exchange?
*/
termsOfServiceAccepted: boolean;
/**
- * The exchange is trusted directly.
- */
- isTrusted: boolean;
-
- /**
* The earliest deposit expiration of the selected coins.
*/
earliestDepositExpiration: TalerProtocolTimestamp;
/**
- * Number of currently offered denominations.
- */
- numOfferedDenoms: number;
-
- /**
- * Public keys of trusted auditors for the currency we're withdrawing.
- */
- trustedAuditorPubs: string[];
-
- /**
* Result of checking the wallet's version
* against the exchange's version.
*
@@ -1092,6 +1622,8 @@ export interface ExchangeWithdrawalDetails {
*
*/
ageRestrictionOptions?: number[];
+
+ scopeInfo: ScopeInfo;
}
export interface GetExchangeTosResult {
@@ -1116,58 +1648,136 @@ export interface GetExchangeTosResult {
*/
contentType: string;
+ /**
+ * Language of the returned content.
+ *
+ * If missing, language is unknown.
+ */
+ contentLanguage: string | undefined;
+
+ /**
+ * Available languages as advertised by the exchange.
+ */
+ tosAvailableLanguages: string[];
+
tosStatus: ExchangeTosStatus;
}
export interface TestPayArgs {
merchantBaseUrl: string;
merchantAuthToken?: string;
- amount: string;
+ amount: AmountString;
summary: string;
forcedCoinSel?: ForcedCoinSel;
}
export const codecForTestPayArgs = (): Codec<TestPayArgs> =>
buildCodecForObject<TestPayArgs>()
- .property("merchantBaseUrl", codecForString())
+ .property("merchantBaseUrl", codecForCanonBaseUrl())
.property("merchantAuthToken", codecOptional(codecForString()))
- .property("amount", codecForString())
+ .property("amount", codecForAmountString())
.property("summary", codecForString())
.property("forcedCoinSel", codecForAny())
.build("TestPayArgs");
export interface IntegrationTestArgs {
exchangeBaseUrl: string;
- bankBaseUrl: string;
- bankAccessApiBaseUrl?: string;
+ corebankApiBaseUrl: string;
merchantBaseUrl: string;
merchantAuthToken?: string;
- amountToWithdraw: string;
- amountToSpend: string;
+ amountToWithdraw: AmountString;
+ amountToSpend: AmountString;
}
export const codecForIntegrationTestArgs = (): Codec<IntegrationTestArgs> =>
buildCodecForObject<IntegrationTestArgs>()
- .property("exchangeBaseUrl", codecForString())
- .property("bankBaseUrl", codecForString())
- .property("merchantBaseUrl", codecForString())
+ .property("exchangeBaseUrl", codecForCanonBaseUrl())
+ .property("merchantBaseUrl", codecForCanonBaseUrl())
.property("merchantAuthToken", codecOptional(codecForString()))
.property("amountToSpend", codecForAmountString())
.property("amountToWithdraw", codecForAmountString())
- .property("bankAccessApiBaseUrl", codecOptional(codecForAmountString()))
+ .property("corebankApiBaseUrl", codecForCanonBaseUrl())
.build("IntegrationTestArgs");
+export interface IntegrationTestV2Args {
+ exchangeBaseUrl: string;
+ corebankApiBaseUrl: string;
+ merchantBaseUrl: string;
+ merchantAuthToken?: string;
+}
+
+export const codecForIntegrationTestV2Args = (): Codec<IntegrationTestV2Args> =>
+ buildCodecForObject<IntegrationTestV2Args>()
+ .property("exchangeBaseUrl", codecForCanonBaseUrl())
+ .property("merchantBaseUrl", codecForCanonBaseUrl())
+ .property("merchantAuthToken", codecOptional(codecForString()))
+ .property("corebankApiBaseUrl", codecForCanonBaseUrl())
+ .build("IntegrationTestV2Args");
+
+export interface GetExchangeEntryByUrlRequest {
+ exchangeBaseUrl: string;
+}
+
+export const codecForGetExchangeEntryByUrlRequest =
+ (): Codec<GetExchangeEntryByUrlRequest> =>
+ buildCodecForObject<GetExchangeEntryByUrlRequest>()
+ .property("exchangeBaseUrl", codecForCanonBaseUrl())
+ .build("GetExchangeEntryByUrlRequest");
+
+export type GetExchangeEntryByUrlResponse = ExchangeListItem;
+
export interface AddExchangeRequest {
exchangeBaseUrl: string;
+
+ /**
+ * @deprecated use a separate API call to start a forced exchange update instead
+ */
forceUpdate?: boolean;
}
export const codecForAddExchangeRequest = (): Codec<AddExchangeRequest> =>
buildCodecForObject<AddExchangeRequest>()
- .property("exchangeBaseUrl", codecForString())
+ .property("exchangeBaseUrl", codecForCanonBaseUrl())
.property("forceUpdate", codecOptional(codecForBoolean()))
.build("AddExchangeRequest");
+export interface UpdateExchangeEntryRequest {
+ exchangeBaseUrl: string;
+ force?: boolean;
+}
+
+export const codecForUpdateExchangeEntryRequest =
+ (): Codec<UpdateExchangeEntryRequest> =>
+ buildCodecForObject<UpdateExchangeEntryRequest>()
+ .property("exchangeBaseUrl", codecForCanonBaseUrl())
+ .property("force", codecOptional(codecForBoolean()))
+ .build("UpdateExchangeEntryRequest");
+
+export interface GetExchangeResourcesRequest {
+ exchangeBaseUrl: string;
+}
+
+export const codecForGetExchangeResourcesRequest =
+ (): Codec<GetExchangeResourcesRequest> =>
+ buildCodecForObject<GetExchangeResourcesRequest>()
+ .property("exchangeBaseUrl", codecForCanonBaseUrl())
+ .build("GetExchangeResourcesRequest");
+
+export interface GetExchangeResourcesResponse {
+ hasResources: boolean;
+}
+
+export interface DeleteExchangeRequest {
+ exchangeBaseUrl: string;
+ purge?: boolean;
+}
+
+export const codecForDeleteExchangeRequest = (): Codec<DeleteExchangeRequest> =>
+ buildCodecForObject<DeleteExchangeRequest>()
+ .property("exchangeBaseUrl", codecForCanonBaseUrl())
+ .property("purge", codecOptional(codecForBoolean()))
+ .build("DeleteExchangeRequest");
+
export interface ForceExchangeUpdateRequest {
exchangeBaseUrl: string;
}
@@ -1175,40 +1785,100 @@ export interface ForceExchangeUpdateRequest {
export const codecForForceExchangeUpdateRequest =
(): Codec<AddExchangeRequest> =>
buildCodecForObject<AddExchangeRequest>()
- .property("exchangeBaseUrl", codecForString())
+ .property("exchangeBaseUrl", codecForCanonBaseUrl())
.build("AddExchangeRequest");
export interface GetExchangeTosRequest {
exchangeBaseUrl: string;
acceptedFormat?: string[];
+ acceptLanguage?: string;
}
export const codecForGetExchangeTosRequest = (): Codec<GetExchangeTosRequest> =>
buildCodecForObject<GetExchangeTosRequest>()
- .property("exchangeBaseUrl", codecForString())
+ .property("exchangeBaseUrl", codecForCanonBaseUrl())
.property("acceptedFormat", codecOptional(codecForList(codecForString())))
+ .property("acceptLanguage", codecOptional(codecForString()))
.build("GetExchangeTosRequest");
export interface AcceptManualWithdrawalRequest {
exchangeBaseUrl: string;
- amount: string;
+ amount: AmountString;
restrictAge?: number;
+
+ /**
+ * Instead of generating a fresh, random reserve key pair,
+ * use the provided reserve private key.
+ *
+ * Use with caution. Usage of this field may be restricted
+ * to developer mode.
+ */
+ forceReservePriv?: EddsaPrivateKeyString;
}
-export const codecForAcceptManualWithdrawalRequet =
+export const codecForAcceptManualWithdrawalRequest =
(): Codec<AcceptManualWithdrawalRequest> =>
buildCodecForObject<AcceptManualWithdrawalRequest>()
- .property("exchangeBaseUrl", codecForString())
- .property("amount", codecForString())
+ .property("exchangeBaseUrl", codecForCanonBaseUrl())
+ .property("amount", codecForAmountString())
.property("restrictAge", codecOptional(codecForNumber()))
+ .property("forceReservePriv", codecOptional(codecForString()))
.build("AcceptManualWithdrawalRequest");
export interface GetWithdrawalDetailsForAmountRequest {
exchangeBaseUrl: string;
- amount: string;
+ amount: AmountString;
restrictAge?: number;
+
+ /**
+ * ID provided by the client to cancel the request.
+ *
+ * If the same request is made again with the same clientCancellationId,
+ * all previous requests are cancelled.
+ *
+ * The cancelled request will receive an error response with
+ * an error code that indicates the cancellation.
+ *
+ * The cancellation is best-effort, responses might still arrive.
+ */
+ clientCancellationId?: string;
}
+export interface PrepareBankIntegratedWithdrawalRequest {
+ talerWithdrawUri: string;
+ selectedExchange?: string;
+}
+
+export const codecForPrepareBankIntegratedWithdrawalRequest =
+ (): Codec<PrepareBankIntegratedWithdrawalRequest> =>
+ buildCodecForObject<PrepareBankIntegratedWithdrawalRequest>()
+ .property("talerWithdrawUri", codecForString())
+ .property("selectedExchange", codecOptional(codecForString()))
+ .build("PrepareBankIntegratedWithdrawalRequest");
+
+export interface PrepareBankIntegratedWithdrawalResponse {
+ transactionId?: string;
+ info: WithdrawUriInfoResponse;
+}
+
+export interface ConfirmWithdrawalRequest {
+ transactionId: string;
+ exchangeBaseUrl: string;
+ amount: AmountString;
+ forcedDenomSel?: ForcedDenomSel;
+ restrictAge?: number;
+}
+
+export const codecForConfirmWithdrawalRequestRequest =
+ (): Codec<ConfirmWithdrawalRequest> =>
+ buildCodecForObject<ConfirmWithdrawalRequest>()
+ .property("transactionId", codecForString())
+ .property("amount", codecForAmountString())
+ .property("exchangeBaseUrl", codecForCanonBaseUrl())
+ .property("forcedDenomSel", codecForAny())
+ .property("restrictAge", codecOptional(codecForNumber()))
+ .build("ConfirmWithdrawalRequest");
+
export interface AcceptBankIntegratedWithdrawalRequest {
talerWithdrawUri: string;
exchangeBaseUrl: string;
@@ -1219,7 +1889,7 @@ export interface AcceptBankIntegratedWithdrawalRequest {
export const codecForAcceptBankIntegratedWithdrawalRequest =
(): Codec<AcceptBankIntegratedWithdrawalRequest> =>
buildCodecForObject<AcceptBankIntegratedWithdrawalRequest>()
- .property("exchangeBaseUrl", codecForString())
+ .property("exchangeBaseUrl", codecForCanonBaseUrl())
.property("talerWithdrawUri", codecForString())
.property("forcedDenomSel", codecForAny())
.property("restrictAge", codecOptional(codecForNumber()))
@@ -1228,31 +1898,40 @@ export const codecForAcceptBankIntegratedWithdrawalRequest =
export const codecForGetWithdrawalDetailsForAmountRequest =
(): Codec<GetWithdrawalDetailsForAmountRequest> =>
buildCodecForObject<GetWithdrawalDetailsForAmountRequest>()
- .property("exchangeBaseUrl", codecForString())
- .property("amount", codecForString())
+ .property("exchangeBaseUrl", codecForCanonBaseUrl())
+ .property("amount", codecForAmountString())
.property("restrictAge", codecOptional(codecForNumber()))
+ .property("clientCancellationId", codecOptional(codecForString()))
.build("GetWithdrawalDetailsForAmountRequest");
export interface AcceptExchangeTosRequest {
exchangeBaseUrl: string;
- etag: string | undefined;
}
export const codecForAcceptExchangeTosRequest =
(): Codec<AcceptExchangeTosRequest> =>
buildCodecForObject<AcceptExchangeTosRequest>()
- .property("exchangeBaseUrl", codecForString())
- .property("etag", codecOptional(codecForString()))
+ .property("exchangeBaseUrl", codecForCanonBaseUrl())
.build("AcceptExchangeTosRequest");
-export interface ApplyRefundRequest {
- talerRefundUri: string;
+export interface ForgetExchangeTosRequest {
+ exchangeBaseUrl: string;
}
-export const codecForApplyRefundRequest = (): Codec<ApplyRefundRequest> =>
- buildCodecForObject<ApplyRefundRequest>()
- .property("talerRefundUri", codecForString())
- .build("ApplyRefundRequest");
+export const codecForForgetExchangeTosRequest =
+ (): Codec<ForgetExchangeTosRequest> =>
+ buildCodecForObject<ForgetExchangeTosRequest>()
+ .property("exchangeBaseUrl", codecForCanonBaseUrl())
+ .build("ForgetExchangeTosRequest");
+
+export interface AcceptRefundRequest {
+ transactionId: TransactionIdStr;
+}
+
+export const codecForApplyRefundRequest = (): Codec<AcceptRefundRequest> =>
+ buildCodecForObject<AcceptRefundRequest>()
+ .property("transactionId", codecForTransactionIdStr())
+ .build("AcceptRefundRequest");
export interface ApplyRefundFromPurchaseIdRequest {
purchaseId: string;
@@ -1266,6 +1945,9 @@ export const codecForApplyRefundFromPurchaseIdRequest =
export interface GetWithdrawalDetailsForUriRequest {
talerWithdrawUri: string;
+ /**
+ * @deprecated not used
+ */
restrictAge?: number;
}
@@ -1319,13 +2001,16 @@ export const codecForAbortProposalRequest = (): Codec<AbortProposalRequest> =>
.build("AbortProposalRequest");
export interface GetContractTermsDetailsRequest {
- proposalId: string;
+ // @deprecated use transaction id
+ proposalId?: string;
+ transactionId?: string;
}
export const codecForGetContractTermsDetails =
(): Codec<GetContractTermsDetailsRequest> =>
buildCodecForObject<GetContractTermsDetailsRequest>()
- .property("proposalId", codecForString())
+ .property("proposalId", codecOptional(codecForString()))
+ .property("transactionId", codecOptional(codecForString()))
.build("GetContractTermsDetails");
export interface PreparePayRequest {
@@ -1337,22 +2022,77 @@ export const codecForPreparePayRequest = (): Codec<PreparePayRequest> =>
.property("talerPayUri", codecForString())
.build("PreparePay");
+export interface SharePaymentRequest {
+ merchantBaseUrl: string;
+ orderId: string;
+}
+export const codecForSharePaymentRequest = (): Codec<SharePaymentRequest> =>
+ buildCodecForObject<SharePaymentRequest>()
+ .property("merchantBaseUrl", codecForCanonBaseUrl())
+ .property("orderId", codecForString())
+ .build("SharePaymentRequest");
+
+export interface SharePaymentResult {
+ privatePayUri: string;
+}
+export const codecForSharePaymentResult = (): Codec<SharePaymentResult> =>
+ buildCodecForObject<SharePaymentResult>()
+ .property("privatePayUri", codecForString())
+ .build("SharePaymentResult");
+
+export interface CheckPayTemplateRequest {
+ talerPayTemplateUri: string;
+}
+
+export type CheckPayTemplateReponse = TalerMerchantApi.WalletTemplateDetails & {
+ supportedCurrencies: string[];
+}
+
+export const codecForCheckPayTemplateRequest =
+ (): Codec<CheckPayTemplateRequest> =>
+ buildCodecForObject<CheckPayTemplateRequest>()
+ .property("talerPayTemplateUri", codecForString())
+ .build("CheckPayTemplateRequest");
+
+export interface PreparePayTemplateRequest {
+ talerPayTemplateUri: string;
+ templateParams?: TemplateParams;
+}
+
+export const codecForPreparePayTemplateRequest =
+ (): Codec<PreparePayTemplateRequest> =>
+ buildCodecForObject<PreparePayTemplateRequest>()
+ .property("talerPayTemplateUri", codecForString())
+ .property("templateParams", codecForAny())
+ .build("PreparePayTemplate");
+
export interface ConfirmPayRequest {
- proposalId: string;
+ /**
+ * @deprecated use transactionId instead
+ */
+ proposalId?: string;
+ transactionId?: TransactionIdStr;
sessionId?: string;
forcedCoinSel?: ForcedCoinSel;
}
export const codecForConfirmPayRequest = (): Codec<ConfirmPayRequest> =>
buildCodecForObject<ConfirmPayRequest>()
- .property("proposalId", codecForString())
+ .property("proposalId", codecOptional(codecForString()))
+ .property("transactionId", codecOptional(codecForTransactionIdStr()))
.property("sessionId", codecOptional(codecForString()))
.property("forcedCoinSel", codecForAny())
.build("ConfirmPay");
+export interface CoreApiRequestEnvelope {
+ id: string;
+ operation: string;
+ args: unknown;
+}
+
export type CoreApiResponse = CoreApiResponseSuccess | CoreApiResponseError;
-export type CoreApiEnvelope = CoreApiResponse | CoreApiNotification;
+export type CoreApiMessageEnvelope = CoreApiResponse | CoreApiNotification;
export interface CoreApiNotification {
type: "notification";
@@ -1376,22 +2116,15 @@ export interface CoreApiResponseError {
}
export interface WithdrawTestBalanceRequest {
- amount: string;
- bankBaseUrl: string;
+ amount: AmountString;
/**
- * Bank access API base URL. Defaults to the bankBaseUrl.
+ * Corebank API base URL.
*/
- bankAccessApiBaseUrl?: string;
+ corebankApiBaseUrl: string;
exchangeBaseUrl: string;
forcedDenomSel?: ForcedDenomSel;
}
-export const withdrawTestBalanceDefaults = {
- amount: "TESTKUDOS:10",
- bankBaseUrl: "https://bank.test.taler.net/",
- exchangeBaseUrl: "https://exchange.test.taler.net/",
-};
-
/**
* Request to the crypto worker to make a sync signature.
*/
@@ -1457,43 +2190,12 @@ export interface RecoveryLoadRequest {
export const codecForWithdrawTestBalance =
(): Codec<WithdrawTestBalanceRequest> =>
buildCodecForObject<WithdrawTestBalanceRequest>()
- .property("amount", codecForString())
- .property("bankBaseUrl", codecForString())
- .property("exchangeBaseUrl", codecForString())
+ .property("amount", codecForAmountString())
+ .property("exchangeBaseUrl", codecForCanonBaseUrl())
.property("forcedDenomSel", codecForAny())
- .property("bankAccessApiBaseUrl", codecOptional(codecForString()))
+ .property("corebankApiBaseUrl", codecForCanonBaseUrl())
.build("WithdrawTestBalanceRequest");
-export interface ApplyRefundResponse {
- contractTermsHash: string;
-
- transactionId: string;
-
- proposalId: string;
-
- amountEffectivePaid: AmountString;
-
- amountRefundGranted: AmountString;
-
- amountRefundGone: AmountString;
-
- pendingAtExchange: boolean;
-
- info: OrderShortInfo;
-}
-
-export const codecForApplyRefundResponse = (): Codec<ApplyRefundResponse> =>
- buildCodecForObject<ApplyRefundResponse>()
- .property("amountEffectivePaid", codecForAmountString())
- .property("amountRefundGone", codecForAmountString())
- .property("amountRefundGranted", codecForAmountString())
- .property("contractTermsHash", codecForString())
- .property("pendingAtExchange", codecForBoolean())
- .property("proposalId", codecForString())
- .property("transactionId", codecForString())
- .property("info", codecForOrderShortInfo())
- .build("ApplyRefundResponse");
-
export interface SetCoinSuspendedRequest {
coinPub: string;
suspended: boolean;
@@ -1506,57 +2208,94 @@ export const codecForSetCoinSuspendedRequest =
.property("suspended", codecForBoolean())
.build("SetCoinSuspendedRequest");
+export interface RefreshCoinSpec {
+ coinPub: string;
+ amount?: AmountString;
+}
+
+export const codecForRefreshCoinSpec = (): Codec<RefreshCoinSpec> =>
+ buildCodecForObject<RefreshCoinSpec>()
+ .property("amount", codecForAmountString())
+ .property("coinPub", codecForString())
+ .build("ForceRefreshRequest");
+
export interface ForceRefreshRequest {
- coinPubList: string[];
+ refreshCoinSpecs: RefreshCoinSpec[];
}
export const codecForForceRefreshRequest = (): Codec<ForceRefreshRequest> =>
buildCodecForObject<ForceRefreshRequest>()
- .property("coinPubList", codecForList(codecForString()))
+ .property("refreshCoinSpecs", codecForList(codecForRefreshCoinSpec()))
.build("ForceRefreshRequest");
export interface PrepareRefundRequest {
talerRefundUri: string;
}
+export interface StartRefundQueryForUriResponse {
+ /**
+ * Transaction id of the *payment* where the refund query was started.
+ */
+ transactionId: TransactionIdStr;
+}
+
export const codecForPrepareRefundRequest = (): Codec<PrepareRefundRequest> =>
buildCodecForObject<PrepareRefundRequest>()
.property("talerRefundUri", codecForString())
.build("PrepareRefundRequest");
-export interface PrepareTipRequest {
- talerTipUri: string;
+export interface StartRefundQueryRequest {
+ transactionId: TransactionIdStr;
}
-export const codecForPrepareTipRequest = (): Codec<PrepareTipRequest> =>
- buildCodecForObject<PrepareTipRequest>()
- .property("talerTipUri", codecForString())
- .build("PrepareTipRequest");
+export const codecForStartRefundQueryRequest =
+ (): Codec<StartRefundQueryRequest> =>
+ buildCodecForObject<StartRefundQueryRequest>()
+ .property("transactionId", codecForTransactionIdStr())
+ .build("StartRefundQueryRequest");
-export interface AcceptTipRequest {
- walletTipId: string;
+export interface FailTransactionRequest {
+ transactionId: TransactionIdStr;
}
-export const codecForAcceptTipRequest = (): Codec<AcceptTipRequest> =>
- buildCodecForObject<AcceptTipRequest>()
- .property("walletTipId", codecForString())
- .build("AcceptTipRequest");
+export const codecForFailTransactionRequest =
+ (): Codec<FailTransactionRequest> =>
+ buildCodecForObject<FailTransactionRequest>()
+ .property("transactionId", codecForTransactionIdStr())
+ .build("FailTransactionRequest");
-export interface AbortPayWithRefundRequest {
- proposalId: string;
+export interface SuspendTransactionRequest {
+ transactionId: TransactionIdStr;
}
-export const codecForAbortPayWithRefundRequest =
- (): Codec<AbortPayWithRefundRequest> =>
- buildCodecForObject<AbortPayWithRefundRequest>()
- .property("proposalId", codecForString())
- .build("AbortPayWithRefundRequest");
+export const codecForSuspendTransaction =
+ (): Codec<SuspendTransactionRequest> =>
+ buildCodecForObject<AbortTransactionRequest>()
+ .property("transactionId", codecForTransactionIdStr())
+ .build("SuspendTransactionRequest");
-export interface GetFeeForDepositRequest {
- depositPaytoUri: string;
- amount: AmountString;
+export interface ResumeTransactionRequest {
+ transactionId: TransactionIdStr;
}
+export const codecForResumeTransaction = (): Codec<ResumeTransactionRequest> =>
+ buildCodecForObject<ResumeTransactionRequest>()
+ .property("transactionId", codecForTransactionIdStr())
+ .build("ResumeTransactionRequest");
+
+export interface AbortTransactionRequest {
+ transactionId: TransactionIdStr;
+}
+
+export interface FailTransactionRequest {
+ transactionId: TransactionIdStr;
+}
+
+export const codecForAbortTransaction = (): Codec<AbortTransactionRequest> =>
+ buildCodecForObject<AbortTransactionRequest>()
+ .property("transactionId", codecForTransactionIdStr())
+ .build("AbortTransactionRequest");
+
export interface DepositGroupFees {
coin: AmountString;
wire: AmountString;
@@ -1564,16 +2303,17 @@ export interface DepositGroupFees {
}
export interface CreateDepositGroupRequest {
+ /**
+ * Pre-allocated transaction ID.
+ * Allows clients to easily handle notifications
+ * that occur while the operation has been created but
+ * before the creation request has returned.
+ */
+ transactionId?: TransactionIdStr;
depositPaytoUri: string;
amount: AmountString;
}
-export const codecForGetFeeForDeposit = (): Codec<GetFeeForDepositRequest> =>
- buildCodecForObject<GetFeeForDepositRequest>()
- .property("amount", codecForAmountString())
- .property("depositPaytoUri", codecForString())
- .build("GetFeeForDepositRequest");
-
export interface PrepareDepositRequest {
depositPaytoUri: string;
amount: AmountString;
@@ -1587,6 +2327,7 @@ export const codecForPrepareDepositRequest = (): Codec<PrepareDepositRequest> =>
export interface PrepareDepositResponse {
totalDepositCost: AmountString;
effectiveDepositAmount: AmountString;
+ fees: DepositGroupFees;
}
export const codecForCreateDepositGroupRequest =
@@ -1594,31 +2335,22 @@ export const codecForCreateDepositGroupRequest =
buildCodecForObject<CreateDepositGroupRequest>()
.property("amount", codecForAmountString())
.property("depositPaytoUri", codecForString())
+ .property("transactionId", codecOptional(codecForTransactionIdStr()))
.build("CreateDepositGroupRequest");
export interface CreateDepositGroupResponse {
depositGroupId: string;
- transactionId: string;
-}
-
-export interface TrackDepositGroupRequest {
- depositGroupId: string;
+ transactionId: TransactionIdStr;
}
-export interface TrackDepositGroupResponse {
- responses: {
- status: number;
- body: any;
- }[];
+export interface TxIdResponse {
+ transactionId: TransactionIdStr;
}
-export const codecForTrackDepositGroupRequest =
- (): Codec<TrackDepositGroupRequest> =>
- buildCodecForObject<TrackDepositGroupRequest>()
- .property("depositGroupId", codecForAmountString())
- .build("TrackDepositGroupRequest");
-
export interface WithdrawUriInfoResponse {
+ operationId: string;
+ status: WithdrawalOperationStatus;
+ confirmTransferUrl?: string;
amount: AmountString;
defaultExchangeBaseUrl?: string;
possibleExchanges: ExchangeListItem[];
@@ -1627,8 +2359,19 @@ export interface WithdrawUriInfoResponse {
export const codecForWithdrawUriInfoResponse =
(): Codec<WithdrawUriInfoResponse> =>
buildCodecForObject<WithdrawUriInfoResponse>()
+ .property("operationId", codecForString())
+ .property("confirmTransferUrl", codecOptional(codecForString()))
+ .property(
+ "status",
+ codecForEither(
+ codecForConstString("pending"),
+ codecForConstString("selected"),
+ codecForConstString("aborted"),
+ codecForConstString("confirmed"),
+ ),
+ )
.property("amount", codecForAmountString())
- .property("defaultExchangeBaseUrl", codecOptional(codecForString()))
+ .property("defaultExchangeBaseUrl", codecOptional(codecForCanonBaseUrl()))
.property("possibleExchanges", codecForList(codecForExchangeListItem()))
.build("WithdrawUriInfoResponse");
@@ -1645,24 +2388,38 @@ export interface WalletCurrencyInfo {
}[];
}
+export interface TestingListTasksForTransactionRequest {
+ transactionId: TransactionIdStr;
+}
+
+export interface TestingListTasksForTransactionsResponse {
+ taskIdList: string[];
+}
+
+export const codecForTestingListTasksForTransactionRequest =
+ (): Codec<TestingListTasksForTransactionRequest> =>
+ buildCodecForObject<TestingListTasksForTransactionRequest>()
+ .property("transactionId", codecForTransactionIdStr())
+ .build("TestingListTasksForTransactionRequest");
+
export interface DeleteTransactionRequest {
- transactionId: string;
+ transactionId: TransactionIdStr;
}
export interface RetryTransactionRequest {
- transactionId: string;
+ transactionId: TransactionIdStr;
}
export const codecForDeleteTransactionRequest =
(): Codec<DeleteTransactionRequest> =>
buildCodecForObject<DeleteTransactionRequest>()
- .property("transactionId", codecForString())
+ .property("transactionId", codecForTransactionIdStr())
.build("DeleteTransactionRequest");
export const codecForRetryTransactionRequest =
(): Codec<RetryTransactionRequest> =>
buildCodecForObject<RetryTransactionRequest>()
- .property("transactionId", codecForString())
+ .property("transactionId", codecForTransactionIdStr())
.build("RetryTransactionRequest");
export interface SetWalletDeviceIdRequest {
@@ -1791,12 +2548,12 @@ interface AttentionBackupUnpaid {
interface AttentionMerchantRefund {
type: AttentionType.MerchantRefund;
- transactionId: string;
+ transactionId: TransactionIdStr;
}
interface AttentionKycWithdrawal {
type: AttentionType.KycWithdrawal;
- transactionId: string;
+ transactionId: TransactionIdStr;
}
interface AttentionExchangeTosChanged {
@@ -1826,17 +2583,17 @@ interface AttentionAuditorDenominationExpires {
}
interface AttentionPullPaymentPaid {
type: AttentionType.PullPaymentPaid;
- transactionId: string;
+ transactionId: TransactionIdStr;
}
interface AttentionPushPaymentReceived {
type: AttentionType.PushPaymentReceived;
- transactionId: string;
+ transactionId: TransactionIdStr;
}
export type UserAttentionUnreadList = Array<{
info: AttentionInfo;
- when: AbsoluteTime;
+ when: TalerPreciseTimestamp;
read: boolean;
}>;
@@ -1856,12 +2613,40 @@ export const codecForWithdrawFakebankRequest =
.property("exchange", codecForString())
.build("WithdrawFakebankRequest");
-export interface ImportDb {
+export interface ActiveTask {
+ taskId: string;
+ transaction: TransactionIdStr | undefined;
+ firstTry: AbsoluteTime | undefined;
+ nextTry: AbsoluteTime | undefined;
+ retryCounter: number | undefined;
+ lastError: TalerErrorDetail | undefined;
+}
+
+export interface GetActiveTasksResponse {
+ tasks: ActiveTask[];
+}
+
+export const codecForActiveTask = (): Codec<ActiveTask> =>
+ buildCodecForObject<ActiveTask>()
+ .property("taskId", codecForString())
+ .property("transaction", codecOptional(codecForTransactionIdStr()))
+ .property("retryCounter", codecOptional(codecForNumber()))
+ .property("firstTry", codecOptional(codecForAbsoluteTime))
+ .property("nextTry", codecOptional(codecForAbsoluteTime))
+ .property("lastError", codecOptional(codecForTalerErrorDetail()))
+ .build("ActiveTask");
+
+export const codecForGetActiveTasks = (): Codec<GetActiveTasksResponse> =>
+ buildCodecForObject<GetActiveTasksResponse>()
+ .property("tasks", codecForList(codecForActiveTask()))
+ .build("GetActiveTasks");
+
+export interface ImportDbRequest {
dump: any;
}
-export const codecForImportDbRequest = (): Codec<ImportDb> =>
- buildCodecForObject<ImportDb>()
+export const codecForImportDbRequest = (): Codec<ImportDbRequest> =>
+ buildCodecForObject<ImportDbRequest>()
.property("dump", codecForAny())
.build("ImportDbRequest");
@@ -1883,7 +2668,23 @@ export interface ForcedCoinSel {
}
export interface TestPayResult {
- payCoinSelection: PayCoinSelection;
+ /**
+ * Number of coins used for the payment.
+ */
+ numCoins: number;
+}
+
+export interface SelectedCoin {
+ denomPubHash: string;
+ coinPub: string;
+ contribution: AmountString;
+ exchangeBaseUrl: string;
+}
+
+export interface SelectedProspectiveCoin {
+ denomPubHash: string;
+ contribution: AmountString;
+ exchangeBaseUrl: string;
}
/**
@@ -1891,20 +2692,21 @@ export interface TestPayResult {
* coins with their denomination.
*/
export interface PayCoinSelection {
- /**
- * Amount requested by the merchant.
- */
- paymentAmount: AmountString;
+ coins: SelectedCoin[];
/**
- * Public keys of the coins that were selected.
+ * How much of the wire fees is the customer paying?
*/
- coinPubs: string[];
+ customerWireFees: AmountString;
/**
- * Amount that each coin contributes.
+ * How much of the deposit fees is the customer paying?
*/
- coinContributions: AmountString[];
+ customerDepositFees: AmountString;
+}
+
+export interface ProspectivePayCoinSelection {
+ prospectiveCoins: SelectedProspectiveCoin[];
/**
* How much of the wire fees is the customer paying?
@@ -1917,111 +2719,138 @@ export interface PayCoinSelection {
customerDepositFees: AmountString;
}
-export interface PreparePeerPushPaymentRequest {
+export interface CheckPeerPushDebitRequest {
+ /**
+ * Preferred exchange to use for the p2p payment.
+ */
exchangeBaseUrl?: string;
+
+ /**
+ * Instructed amount.
+ *
+ * FIXME: Allow specifying the instructed amount type.
+ */
amount: AmountString;
}
-export const codecForPreparePeerPushPaymentRequest =
- (): Codec<PreparePeerPushPaymentRequest> =>
- buildCodecForObject<PreparePeerPushPaymentRequest>()
- .property("exchangeBaseUrl", codecOptional(codecForString()))
+export const codecForCheckPeerPushDebitRequest =
+ (): Codec<CheckPeerPushDebitRequest> =>
+ buildCodecForObject<CheckPeerPushDebitRequest>()
+ .property("exchangeBaseUrl", codecOptional(codecForCanonBaseUrl()))
.property("amount", codecForAmountString())
- .build("InitiatePeerPushPaymentRequest");
+ .build("CheckPeerPushDebitRequest");
-export interface PreparePeerPushPaymentResponse {
+export interface CheckPeerPushDebitResponse {
amountRaw: AmountString;
amountEffective: AmountString;
+ exchangeBaseUrl: string;
+ /**
+ * Maximum expiration date, based on how close the coins
+ * used for the payment are to expiry.
+ *
+ * The value is based on when the wallet would typically
+ * automatically refresh the coins on its own, leaving enough
+ * time to get a refund for the push payment and refresh the
+ * coin.
+ */
+ maxExpirationDate: TalerProtocolTimestamp;
}
-export interface InitiatePeerPushPaymentRequest {
+export interface InitiatePeerPushDebitRequest {
exchangeBaseUrl?: string;
partialContractTerms: PeerContractTerms;
}
-export interface InitiatePeerPushPaymentResponse {
+export interface InitiatePeerPushDebitResponse {
exchangeBaseUrl: string;
pursePub: string;
mergePriv: string;
contractPriv: string;
- talerUri: string;
- transactionId: string;
+ transactionId: TransactionIdStr;
}
-export const codecForInitiatePeerPushPaymentRequest =
- (): Codec<InitiatePeerPushPaymentRequest> =>
- buildCodecForObject<InitiatePeerPushPaymentRequest>()
+export const codecForInitiatePeerPushDebitRequest =
+ (): Codec<InitiatePeerPushDebitRequest> =>
+ buildCodecForObject<InitiatePeerPushDebitRequest>()
.property("partialContractTerms", codecForPeerContractTerms())
- .build("InitiatePeerPushPaymentRequest");
+ .build("InitiatePeerPushDebitRequest");
-export interface CheckPeerPushPaymentRequest {
+export interface PreparePeerPushCreditRequest {
talerUri: string;
}
-export interface CheckPeerPullPaymentRequest {
+export interface PreparePeerPullDebitRequest {
talerUri: string;
}
-export interface CheckPeerPushPaymentResponse {
+export interface PreparePeerPushCreditResponse {
contractTerms: PeerContractTerms;
+ amountRaw: AmountString;
+ amountEffective: AmountString;
+
+ transactionId: TransactionIdStr;
+
+ exchangeBaseUrl: string;
+
+ /**
+ * @deprecated use transaction ID instead.
+ */
+ peerPushCreditId: string;
+
+ /**
+ * @deprecated
+ */
amount: AmountString;
- peerPushPaymentIncomingId: string;
}
-export interface CheckPeerPullPaymentResponse {
+export interface PreparePeerPullDebitResponse {
contractTerms: PeerContractTerms;
+ /**
+ * @deprecated Redundant field with bad name, will be removed soon.
+ */
amount: AmountString;
- peerPullPaymentIncomingId: string;
+
+ amountRaw: AmountString;
+ amountEffective: AmountString;
+
+ peerPullDebitId: string;
+
+ transactionId: TransactionIdStr;
}
-export const codecForCheckPeerPushPaymentRequest =
- (): Codec<CheckPeerPushPaymentRequest> =>
- buildCodecForObject<CheckPeerPushPaymentRequest>()
+export const codecForPreparePeerPushCreditRequest =
+ (): Codec<PreparePeerPushCreditRequest> =>
+ buildCodecForObject<PreparePeerPushCreditRequest>()
.property("talerUri", codecForString())
.build("CheckPeerPushPaymentRequest");
export const codecForCheckPeerPullPaymentRequest =
- (): Codec<CheckPeerPullPaymentRequest> =>
- buildCodecForObject<CheckPeerPullPaymentRequest>()
+ (): Codec<PreparePeerPullDebitRequest> =>
+ buildCodecForObject<PreparePeerPullDebitRequest>()
.property("talerUri", codecForString())
- .build("CheckPeerPullPaymentRequest");
+ .build("PreparePeerPullDebitRequest");
-export interface AcceptPeerPushPaymentRequest {
- /**
- * Transparent identifier of the incoming peer push payment.
- */
- peerPushPaymentIncomingId: string;
+export interface ConfirmPeerPushCreditRequest {
+ transactionId: string;
}
export interface AcceptPeerPushPaymentResponse {
- transactionId: string;
+ transactionId: TransactionIdStr;
}
export interface AcceptPeerPullPaymentResponse {
- transactionId: string;
+ transactionId: TransactionIdStr;
}
-export const codecForAcceptPeerPushPaymentRequest =
- (): Codec<AcceptPeerPushPaymentRequest> =>
- buildCodecForObject<AcceptPeerPushPaymentRequest>()
- .property("peerPushPaymentIncomingId", codecForString())
- .build("AcceptPeerPushPaymentRequest");
-
-export interface AcceptPeerPullPaymentRequest {
- /**
- * Transparent identifier of the incoming peer pull payment.
- */
- peerPullPaymentIncomingId: string;
-}
+export const codecForConfirmPeerPushPaymentRequest =
+ (): Codec<ConfirmPeerPushCreditRequest> =>
+ buildCodecForObject<ConfirmPeerPushCreditRequest>()
+ .property("transactionId", codecForString())
+ .build("ConfirmPeerPushCreditRequest");
-export interface SetDevModeRequest {
- devModeEnabled: boolean;
+export interface ConfirmPeerPullDebitRequest {
+ transactionId: TransactionIdStr;
}
-export const codecForSetDevModeRequest = (): Codec<SetDevModeRequest> =>
- buildCodecForObject<SetDevModeRequest>()
- .property("devModeEnabled", codecForBoolean())
- .build("SetDevModeRequest");
-
export interface ApplyDevExperimentRequest {
devExperimentUri: string;
}
@@ -2033,47 +2862,488 @@ export const codecForApplyDevExperiment =
.build("ApplyDevExperimentRequest");
export const codecForAcceptPeerPullPaymentRequest =
- (): Codec<AcceptPeerPullPaymentRequest> =>
- buildCodecForObject<AcceptPeerPullPaymentRequest>()
- .property("peerPullPaymentIncomingId", codecForString())
- .build("AcceptPeerPllPaymentRequest");
+ (): Codec<ConfirmPeerPullDebitRequest> =>
+ buildCodecForObject<ConfirmPeerPullDebitRequest>()
+ .property("transactionId", codecForTransactionIdStr())
+ .build("ConfirmPeerPullDebitRequest");
-export interface PreparePeerPullPaymentRequest {
- exchangeBaseUrl: string;
+export interface CheckPeerPullCreditRequest {
+ exchangeBaseUrl?: string;
amount: AmountString;
}
export const codecForPreparePeerPullPaymentRequest =
- (): Codec<PreparePeerPullPaymentRequest> =>
- buildCodecForObject<PreparePeerPullPaymentRequest>()
+ (): Codec<CheckPeerPullCreditRequest> =>
+ buildCodecForObject<CheckPeerPullCreditRequest>()
.property("amount", codecForAmountString())
- .property("exchangeBaseUrl", codecForString())
- .build("PreparePeerPullPaymentRequest");
+ .property("exchangeBaseUrl", codecOptional(codecForCanonBaseUrl()))
+ .build("CheckPeerPullCreditRequest");
-export interface PreparePeerPullPaymentResponse {
+export interface CheckPeerPullCreditResponse {
+ exchangeBaseUrl: string;
amountRaw: AmountString;
amountEffective: AmountString;
-}
-export interface InitiatePeerPullPaymentRequest {
+
/**
- * FIXME: Make this optional?
+ * Number of coins that will be used,
+ * can be used by the UI to warn if excessively large.
*/
- exchangeBaseUrl: string;
+ numCoins: number;
+}
+export interface InitiatePeerPullCreditRequest {
+ exchangeBaseUrl?: string;
partialContractTerms: PeerContractTerms;
}
export const codecForInitiatePeerPullPaymentRequest =
- (): Codec<InitiatePeerPullPaymentRequest> =>
- buildCodecForObject<InitiatePeerPullPaymentRequest>()
+ (): Codec<InitiatePeerPullCreditRequest> =>
+ buildCodecForObject<InitiatePeerPullCreditRequest>()
.property("partialContractTerms", codecForPeerContractTerms())
- .property("exchangeBaseUrl", codecForString())
- .build("InitiatePeerPullPaymentRequest");
+ .property("exchangeBaseUrl", codecOptional(codecForCanonBaseUrl()))
+ .build("InitiatePeerPullCreditRequest");
-export interface InitiatePeerPullPaymentResponse {
+export interface InitiatePeerPullCreditResponse {
/**
* Taler URI for the other party to make the payment
* that was requested.
+ *
+ * @deprecated since it's not necessarily valid yet until the tx is in the right state
*/
talerUri: string;
- transactionId: string;
+ transactionId: TransactionIdStr;
+}
+
+export interface CanonicalizeBaseUrlRequest {
+ url: string;
+}
+
+export const codecForCanonicalizeBaseUrlRequest =
+ (): Codec<CanonicalizeBaseUrlRequest> =>
+ buildCodecForObject<CanonicalizeBaseUrlRequest>()
+ .property("url", codecForString())
+ .build("CanonicalizeBaseUrlRequest");
+
+export interface CanonicalizeBaseUrlResponse {
+ url: string;
+}
+
+export interface ValidateIbanRequest {
+ iban: string;
+}
+
+export const codecForValidateIbanRequest = (): Codec<ValidateIbanRequest> =>
+ buildCodecForObject<ValidateIbanRequest>()
+ .property("iban", codecForString())
+ .build("ValidateIbanRequest");
+
+export interface ValidateIbanResponse {
+ valid: boolean;
+}
+
+export const codecForValidateIbanResponse = (): Codec<ValidateIbanResponse> =>
+ buildCodecForObject<ValidateIbanResponse>()
+ .property("valid", codecForBoolean())
+ .build("ValidateIbanResponse");
+
+export type TransactionStateFilter = "nonfinal";
+
+export interface TransactionRecordFilter {
+ onlyState?: TransactionStateFilter;
+ onlyCurrency?: string;
+}
+
+export interface StoredBackupList {
+ storedBackups: {
+ name: string;
+ }[];
+}
+
+export interface CreateStoredBackupResponse {
+ name: string;
+}
+
+export interface RecoverStoredBackupRequest {
+ name: string;
+}
+
+export interface DeleteStoredBackupRequest {
+ name: string;
+}
+
+export const codecForDeleteStoredBackupRequest =
+ (): Codec<DeleteStoredBackupRequest> =>
+ buildCodecForObject<DeleteStoredBackupRequest>()
+ .property("name", codecForString())
+ .build("DeleteStoredBackupRequest");
+
+export const codecForRecoverStoredBackupRequest =
+ (): Codec<RecoverStoredBackupRequest> =>
+ buildCodecForObject<RecoverStoredBackupRequest>()
+ .property("name", codecForString())
+ .build("RecoverStoredBackupRequest");
+
+export interface TestingSetTimetravelRequest {
+ offsetMs: number;
+}
+
+export const codecForTestingSetTimetravelRequest =
+ (): Codec<TestingSetTimetravelRequest> =>
+ buildCodecForObject<TestingSetTimetravelRequest>()
+ .property("offsetMs", codecForNumber())
+ .build("TestingSetTimetravelRequest");
+
+export interface AllowedAuditorInfo {
+ auditorBaseUrl: string;
+ auditorPub: string;
+}
+
+export interface AllowedExchangeInfo {
+ exchangeBaseUrl: string;
+ exchangePub: string;
+}
+
+/**
+ * Data extracted from the contract terms that is relevant for payment
+ * processing in the wallet.
+ */
+export interface WalletContractData {
+ /**
+ * Fulfillment URL, or the empty string if the order has no fulfillment URL.
+ *
+ * Stored as a non-nullable string as we use this field for IndexedDB indexing.
+ */
+ fulfillmentUrl: string;
+
+ contractTermsHash: string;
+ fulfillmentMessage?: string;
+ fulfillmentMessageI18n?: InternationalizedString;
+ merchantSig: string;
+ merchantPub: string;
+ merchant: MerchantInfo;
+ amount: AmountString;
+ orderId: string;
+ merchantBaseUrl: string;
+ summary: string;
+ summaryI18n: { [lang_tag: string]: string } | undefined;
+ autoRefund: TalerProtocolDuration | undefined;
+ payDeadline: TalerProtocolTimestamp;
+ refundDeadline: TalerProtocolTimestamp;
+ allowedExchanges: AllowedExchangeInfo[];
+ timestamp: TalerProtocolTimestamp;
+ wireMethod: string;
+ wireInfoHash: string;
+ maxDepositFee: AmountString;
+ minimumAge?: number;
+}
+
+export interface TestingWaitTransactionRequest {
+ transactionId: TransactionIdStr;
+ txState: TransactionState;
+}
+
+export interface TestingGetReserveHistoryRequest {
+ reservePub: string;
+ exchangeBaseUrl: string;
+}
+
+export const codecForTestingGetReserveHistoryRequest =
+ (): Codec<TestingGetReserveHistoryRequest> =>
+ buildCodecForObject<TestingGetReserveHistoryRequest>()
+ .property("reservePub", codecForString())
+ .property("exchangeBaseUrl", codecForString())
+ .build("TestingGetReserveHistoryRequest");
+
+export interface TestingGetDenomStatsRequest {
+ exchangeBaseUrl: string;
+}
+
+export interface TestingGetDenomStatsResponse {
+ numKnown: number;
+ numOffered: number;
+ numLost: number;
+}
+
+export const codecForTestingGetDenomStatsRequest =
+ (): Codec<TestingGetDenomStatsRequest> =>
+ buildCodecForObject<TestingGetDenomStatsRequest>()
+ .property("exchangeBaseUrl", codecForCanonBaseUrl())
+ .build("TestingGetDenomStatsRequest");
+
+export interface WithdrawalExchangeAccountDetails {
+ /**
+ * Payto URI to credit the exchange.
+ *
+ * Depending on whether the (manual!) withdrawal is accepted or just
+ * being checked, this already includes the subject with the
+ * reserve public key.
+ */
+ paytoUri: string;
+
+ /**
+ * Status that indicates whether the account can be used
+ * by the user to send funds for a withdrawal.
+ *
+ * ok: account should be shown to the user
+ * error: account should not be shown to the user, UIs might render the error (in conversionError),
+ * especially in dev mode.
+ */
+ status: "ok" | "error";
+
+ /**
+ * Transfer amount. Might be in a different currency than the requested
+ * amount for withdrawal.
+ *
+ * Absent if this is a conversion account and the conversion failed.
+ */
+ transferAmount?: AmountString;
+
+ /**
+ * Currency specification for the external currency.
+ *
+ * Only included if this account requires a currency conversion.
+ */
+ currencySpecification?: CurrencySpecification;
+
+ /**
+ * Further restrictions for sending money to the
+ * exchange.
+ */
+ creditRestrictions?: AccountRestriction[];
+
+ /**
+ * Label given to the account or the account's bank by the exchange.
+ */
+ bankLabel?: string;
+
+ /*
+ * Display priority assigned to this bank account by the exchange.
+ */
+ priority?: number;
+
+ /**
+ * Error that happened when attempting to request the conversion rate.
+ */
+ conversionError?: TalerErrorDetail;
+}
+
+export interface PrepareWithdrawExchangeRequest {
+ /**
+ * A taler://withdraw-exchange URI.
+ */
+ talerUri: string;
+}
+
+export const codecForPrepareWithdrawExchangeRequest =
+ (): Codec<PrepareWithdrawExchangeRequest> =>
+ buildCodecForObject<PrepareWithdrawExchangeRequest>()
+ .property("talerUri", codecForString())
+ .build("PrepareWithdrawExchangeRequest");
+
+export interface PrepareWithdrawExchangeResponse {
+ /**
+ * Base URL of the exchange that already existed
+ * or was ephemerally added as an exchange entry to
+ * the wallet.
+ */
+ exchangeBaseUrl: string;
+
+ /**
+ * Amount from the taler://withdraw-exchange URI.
+ * Only present if specified in the URI.
+ */
+ amount?: AmountString;
+}
+
+export interface ExchangeEntryState {
+ tosStatus: ExchangeTosStatus;
+ exchangeEntryStatus: ExchangeEntryStatus;
+ exchangeUpdateStatus: ExchangeUpdateStatus;
+}
+
+export interface ListGlobalCurrencyAuditorsResponse {
+ auditors: {
+ currency: string;
+ auditorBaseUrl: string;
+ auditorPub: string;
+ }[];
}
+
+export interface ListGlobalCurrencyExchangesResponse {
+ exchanges: {
+ currency: string;
+ exchangeBaseUrl: string;
+ exchangeMasterPub: string;
+ }[];
+}
+
+export interface AddGlobalCurrencyExchangeRequest {
+ currency: string;
+ exchangeBaseUrl: string;
+ exchangeMasterPub: string;
+}
+
+export const codecForAddGlobalCurrencyExchangeRequest =
+ (): Codec<AddGlobalCurrencyExchangeRequest> =>
+ buildCodecForObject<AddGlobalCurrencyExchangeRequest>()
+ .property("currency", codecForString())
+ .property("exchangeBaseUrl", codecForCanonBaseUrl())
+ .property("exchangeMasterPub", codecForString())
+ .build("AddGlobalCurrencyExchangeRequest");
+
+export interface RemoveGlobalCurrencyExchangeRequest {
+ currency: string;
+ exchangeBaseUrl: string;
+ exchangeMasterPub: string;
+}
+
+export const codecForRemoveGlobalCurrencyExchangeRequest =
+ (): Codec<RemoveGlobalCurrencyExchangeRequest> =>
+ buildCodecForObject<RemoveGlobalCurrencyExchangeRequest>()
+ .property("currency", codecForString())
+ .property("exchangeBaseUrl", codecForCanonBaseUrl())
+ .property("exchangeMasterPub", codecForString())
+ .build("RemoveGlobalCurrencyExchangeRequest");
+
+export interface AddGlobalCurrencyAuditorRequest {
+ currency: string;
+ auditorBaseUrl: string;
+ auditorPub: string;
+}
+
+export const codecForAddGlobalCurrencyAuditorRequest =
+ (): Codec<AddGlobalCurrencyAuditorRequest> =>
+ buildCodecForObject<AddGlobalCurrencyAuditorRequest>()
+ .property("currency", codecForString())
+ .property("auditorBaseUrl", codecForCanonBaseUrl())
+ .property("auditorPub", codecForString())
+ .build("AddGlobalCurrencyAuditorRequest");
+
+export interface RemoveGlobalCurrencyAuditorRequest {
+ currency: string;
+ auditorBaseUrl: string;
+ auditorPub: string;
+}
+
+export const codecForRemoveGlobalCurrencyAuditorRequest =
+ (): Codec<RemoveGlobalCurrencyAuditorRequest> =>
+ buildCodecForObject<RemoveGlobalCurrencyAuditorRequest>()
+ .property("currency", codecForString())
+ .property("auditorBaseUrl", codecForCanonBaseUrl())
+ .property("auditorPub", codecForString())
+ .build("RemoveGlobalCurrencyAuditorRequest");
+
+/**
+ * Information about one provider.
+ *
+ * We don't store the account key here,
+ * as that's derived from the wallet root key.
+ */
+export interface ProviderInfo {
+ active: boolean;
+ syncProviderBaseUrl: string;
+ name: string;
+ terms?: BackupProviderTerms;
+ /**
+ * Last communication issue with the provider.
+ */
+ lastError?: TalerErrorDetail;
+ lastSuccessfulBackupTimestamp?: TalerPreciseTimestamp;
+ lastAttemptedBackupTimestamp?: TalerPreciseTimestamp;
+ paymentProposalIds: string[];
+ backupProblem?: BackupProblem;
+ paymentStatus: ProviderPaymentStatus;
+}
+
+export interface BackupProviderTerms {
+ supportedProtocolVersion: string;
+ annualFee: AmountString;
+ storageLimitInMegabytes: number;
+}
+
+export type BackupProblem =
+ | BackupUnreadableProblem
+ | BackupConflictingDeviceProblem;
+
+export interface BackupUnreadableProblem {
+ type: "backup-unreadable";
+}
+
+export interface BackupConflictingDeviceProblem {
+ type: "backup-conflicting-device";
+ otherDeviceId: string;
+ myDeviceId: string;
+ backupTimestamp: AbsoluteTime;
+}
+
+export type ProviderPaymentStatus =
+ | ProviderPaymentTermsChanged
+ | ProviderPaymentPaid
+ | ProviderPaymentInsufficientBalance
+ | ProviderPaymentUnpaid
+ | ProviderPaymentPending;
+
+export enum ProviderPaymentType {
+ Unpaid = "unpaid",
+ Pending = "pending",
+ InsufficientBalance = "insufficient-balance",
+ Paid = "paid",
+ TermsChanged = "terms-changed",
+}
+
+export interface ProviderPaymentUnpaid {
+ type: ProviderPaymentType.Unpaid;
+}
+
+export interface ProviderPaymentInsufficientBalance {
+ type: ProviderPaymentType.InsufficientBalance;
+ amount: AmountString;
+}
+
+export interface ProviderPaymentPending {
+ type: ProviderPaymentType.Pending;
+ talerUri?: string;
+}
+
+export interface ProviderPaymentPaid {
+ type: ProviderPaymentType.Paid;
+ paidUntil: AbsoluteTime;
+}
+
+export interface ProviderPaymentTermsChanged {
+ type: ProviderPaymentType.TermsChanged;
+ paidUntil: AbsoluteTime;
+ oldTerms: BackupProviderTerms;
+ newTerms: BackupProviderTerms;
+}
+
+// FIXME: Does not really belong here, move to sync API
+export interface SyncTermsOfServiceResponse {
+ // maximum backup size supported
+ storage_limit_in_megabytes: number;
+
+ // Fee for an account, per year.
+ annual_fee: AmountString;
+
+ // protocol version supported by the server,
+ // for now always "0.0".
+ version: string;
+}
+
+// FIXME: Does not really belong here, move to sync API
+export const codecForSyncTermsOfServiceResponse =
+ (): Codec<SyncTermsOfServiceResponse> =>
+ buildCodecForObject<SyncTermsOfServiceResponse>()
+ .property("storage_limit_in_megabytes", codecForNumber())
+ .property("annual_fee", codecForAmountString())
+ .property("version", codecForString())
+ .build("SyncTermsOfServiceResponse");
+
+export interface HintNetworkAvailabilityRequest {
+ isNetworkAvailable: boolean;
+}
+
+export const codecForHintNetworkAvailabilityRequest =
+ (): Codec<HintNetworkAvailabilityRequest> =>
+ buildCodecForObject<HintNetworkAvailabilityRequest>()
+ .property("isNetworkAvailable", codecForBoolean())
+ .build("HintNetworkAvailabilityRequest");
diff --git a/packages/taler-util/src/whatwg-url.ts b/packages/taler-util/src/whatwg-url.ts
index 991528ae6..13abf5397 100644
--- a/packages/taler-util/src/whatwg-url.ts
+++ b/packages/taler-util/src/whatwg-url.ts
@@ -1908,15 +1908,22 @@ function parseURL(
}
export class URLImpl {
- constructor(url: string, base?: string) {
+ //Include URL type for "url" and "base" params.
+ constructor(url: string | URL, base?: string | URL) {
let parsedBase = null;
if (base !== undefined) {
+ if (base instanceof URL) {
+ base = base.href;
+ }
parsedBase = basicURLParse(base);
if (parsedBase === null) {
throw new TypeError(`Invalid base URL: ${base}`);
}
}
+ if (url instanceof URL) {
+ url = url.href;
+ }
const parsedURL = basicURLParse(url, { baseURL: parsedBase });
if (parsedURL === null) {
throw new TypeError(`Invalid URL: ${url}`);