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/CancellationToken.ts2
-rw-r--r--packages/taler-util/src/MerchantApiClient.ts380
-rw-r--r--packages/taler-util/src/RequestThrottler.ts1
-rw-r--r--packages/taler-util/src/ReserveStatus.ts16
-rw-r--r--packages/taler-util/src/ReserveTransaction.ts15
-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.ts357
-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.ts42
-rw-r--r--packages/taler-util/src/backupTypes.ts1285
-rw-r--r--packages/taler-util/src/bank-api-client.ts440
-rw-r--r--packages/taler-util/src/base64.ts64
-rw-r--r--packages/taler-util/src/bech32.ts14
-rw-r--r--packages/taler-util/src/bitcoin.test.ts89
-rw-r--r--packages/taler-util/src/bitcoin.ts16
-rw-r--r--packages/taler-util/src/clk.test.ts39
-rw-r--r--packages/taler-util/src/clk.ts101
-rw-r--r--packages/taler-util/src/codec.ts97
-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.test.ts127
-rw-r--r--packages/taler-util/src/contract-terms.ts231
-rw-r--r--packages/taler-util/src/errors.ts329
-rw-r--r--packages/taler-util/src/fnutils.ts2
-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.ts2343
-rw-r--r--packages/taler-util/src/http-client/officer-account.ts105
-rw-r--r--packages/taler-util/src/http-client/types.ts5290
-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.ts37
-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.ts69
-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.d.ts24
-rw-r--r--packages/taler-util/src/kdf.js95
-rw-r--r--packages/taler-util/src/kdf.ts47
-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/merchant-api-types.ts352
-rw-r--r--packages/taler-util/src/notifications.ts457
-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.ts160
-rw-r--r--packages/taler-util/src/promises.ts112
-rw-r--r--packages/taler-util/src/punycode.ts468
-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/segwit_addr.ts32
-rw-r--r--packages/taler-util/src/sha256.ts77
-rw-r--r--packages/taler-util/src/taler-crypto.test.ts (renamed from packages/taler-util/src/talerCrypto.test.ts)79
-rw-r--r--packages/taler-util/src/taler-crypto.ts (renamed from packages/taler-util/src/talerCrypto.ts)757
-rw-r--r--packages/taler-util/src/taler-error-codes.ts2115
-rw-r--r--packages/taler-util/src/taler-types.ts (renamed from packages/taler-util/src/talerTypes.ts)1444
-rw-r--r--packages/taler-util/src/talerconfig.ts439
-rw-r--r--packages/taler-util/src/taleruri.test.ts479
-rw-r--r--packages/taler-util/src/taleruri.ts625
-rw-r--r--packages/taler-util/src/time.test.ts39
-rw-r--r--packages/taler-util/src/time.ts389
-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.ts795
-rw-r--r--packages/taler-util/src/transactionsTypes.ts376
-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/types-test.ts2
-rw-r--r--packages/taler-util/src/url.ts30
-rw-r--r--packages/taler-util/src/wallet-types.ts3296
-rw-r--r--packages/taler-util/src/walletTypes.ts1207
-rw-r--r--packages/taler-util/src/whatwg-url.ts2126
97 files changed, 30591 insertions, 4501 deletions
diff --git a/packages/taler-util/src/CancellationToken.ts b/packages/taler-util/src/CancellationToken.ts
index 134805274..3aa576d77 100644
--- a/packages/taler-util/src/CancellationToken.ts
+++ b/packages/taler-util/src/CancellationToken.ts
@@ -260,7 +260,7 @@ namespace CancellationToken {
cancel(reason?: any): void;
/**
- * Dipose of the token and this source and release memory.
+ * Dispose of the token and this source and release memory.
*/
dispose(): void;
}
diff --git a/packages/taler-util/src/MerchantApiClient.ts b/packages/taler-util/src/MerchantApiClient.ts
new file mode 100644
index 000000000..c27f1d582
--- /dev/null
+++ b/packages/taler-util/src/MerchantApiClient.ts
@@ -0,0 +1,380 @@
+/*
+ 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,
+} 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 {
+ MerchantInstancesResponse,
+ MerchantPostOrderRequest,
+ MerchantPostOrderResponse,
+ MerchantTemplateAddDetails,
+ codecForMerchantPostOrderResponse,
+} from "./merchant-api-types.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<MerchantInstancesResponse> {
+ 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: MerchantPostOrderRequest,
+ ): Promise<MerchantPostOrderResponse> {
+ 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,
+ codecForMerchantPostOrderResponse(),
+ );
+ }
+
+ 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: 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/RequestThrottler.ts b/packages/taler-util/src/RequestThrottler.ts
index a151cc634..2f59612de 100644
--- a/packages/taler-util/src/RequestThrottler.ts
+++ b/packages/taler-util/src/RequestThrottler.ts
@@ -21,7 +21,6 @@ import { AbsoluteTime } from "./time.js";
* Implementation of token bucket throttling.
*/
-
const logger = new Logger("RequestThrottler.ts");
/**
diff --git a/packages/taler-util/src/ReserveStatus.ts b/packages/taler-util/src/ReserveStatus.ts
index eb147da2d..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 { AmountString } from "./talerTypes.js";
-import {
- ReserveTransaction,
- codecForReserveTransaction,
-} from "./ReserveTransaction.js";
+import { codecForAmountString } from "./amounts.js";
+import { Codec, buildCodecForObject } from "./codec.js";
+import { AmountString } from "./taler-types.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 50610483f..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,
@@ -37,7 +38,7 @@ import {
EddsaSignatureString,
EddsaPublicKeyString,
CoinPublicKeyString,
-} from "./talerTypes";
+} from "./taler-types.js";
import {
AbsoluteTime,
codecForTimestamp,
@@ -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 98cd4ad62..82a3d3b68 100644
--- a/packages/taler-util/src/amounts.ts
+++ b/packages/taler-util/src/amounts.ts
@@ -22,12 +22,16 @@
* Imports.
*/
import {
+ Codec,
+ Context,
+ DecodingError,
buildCodecForObject,
- codecForString,
codecForNumber,
- Codec,
+ codecForString,
+ renderContext,
} from "./codec.js";
-import { AmountString } from "./talerTypes.js";
+import { CurrencySpecification } from "./index.js";
+import { AmountString } from "./taler-types.js";
/**
* Number of fractional units that one value unit represents.
@@ -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.
@@ -103,10 +175,24 @@ export class Amounts {
throw Error("not instantiable");
}
+ static currencyOf(amount: AmountLike) {
+ const amt = Amounts.parseOrThrow(amount);
+ return amt.currency;
+ }
+
+ static zeroOfAmount(amount: AmountLike): AmountJson {
+ const amt = Amounts.parseOrThrow(amount);
+ return {
+ currency: amt.currency,
+ fraction: 0,
+ value: 0,
+ };
+ }
+
/**
* Get an amount that represents zero units of a currency.
*/
- static getZero(currency: string): AmountJson {
+ static zeroOfCurrency(currency: string): AmountJson {
return {
currency,
fraction: 0,
@@ -118,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");
@@ -132,7 +246,7 @@ export class Amounts {
static sumOrZero(currency: string, amounts: AmountLike[]): Result {
if (amounts.length <= 0) {
return {
- amount: Amounts.getZero(currency),
+ amount: Amounts.zeroOfCurrency(currency),
saturated: false,
};
}
@@ -147,9 +261,11 @@ export class Amounts {
*
* Throws when currencies don't match.
*/
- static add(first: AmountJson, ...rest: AmountJson[]): Result {
- const currency = first.currency;
- let value = first.value + Math.floor(first.fraction / amountFractionalBase);
+ static add(first: AmountLike, ...rest: AmountLike[]): Result {
+ const firstJ = Amounts.jsonifyAmount(first);
+ const currency = firstJ.currency;
+ let value =
+ firstJ.value + Math.floor(firstJ.fraction / amountFractionalBase);
if (value > amountMaxValue) {
return {
amount: {
@@ -160,17 +276,18 @@ export class Amounts {
saturated: true,
};
}
- let fraction = first.fraction % amountFractionalBase;
+ let fraction = firstJ.fraction % amountFractionalBase;
for (const x of rest) {
- if (x.currency.toUpperCase() !== currency.toUpperCase()) {
- throw Error(`Mismatched currency: ${x.currency} and ${currency}`);
+ const xJ = Amounts.jsonifyAmount(x);
+ if (xJ.currency.toUpperCase() !== currency.toUpperCase()) {
+ throw Error(`Mismatched currency: ${xJ.currency} and ${currency}`);
}
value =
value +
- x.value +
- Math.floor((fraction + x.fraction) / amountFractionalBase);
- fraction = Math.floor((fraction + x.fraction) % amountFractionalBase);
+ xJ.value +
+ Math.floor((fraction + xJ.fraction) / amountFractionalBase);
+ fraction = Math.floor((fraction + xJ.fraction) % amountFractionalBase);
if (value > amountMaxValue) {
return {
amount: {
@@ -192,16 +309,18 @@ export class Amounts {
*
* Throws when currencies don't match.
*/
- static sub(a: AmountJson, ...rest: AmountJson[]): Result {
- const currency = a.currency;
- let value = a.value;
- let fraction = a.fraction;
+ static sub(a: AmountLike, ...rest: AmountLike[]): Result {
+ const aJ = Amounts.jsonifyAmount(a);
+ const currency = aJ.currency;
+ let value = aJ.value;
+ let fraction = aJ.fraction;
for (const b of rest) {
- if (b.currency.toUpperCase() !== a.currency.toUpperCase()) {
- throw Error(`Mismatched currency: ${b.currency} and ${currency}`);
+ const bJ = Amounts.jsonifyAmount(b);
+ if (bJ.currency.toUpperCase() !== aJ.currency.toUpperCase()) {
+ throw Error(`Mismatched currency: ${bJ.currency} and ${currency}`);
}
- if (fraction < b.fraction) {
+ if (fraction < bJ.fraction) {
if (value < 1) {
return {
amount: { currency, value: 0, fraction: 0 },
@@ -211,12 +330,12 @@ export class Amounts {
value--;
fraction += amountFractionalBase;
}
- console.assert(fraction >= b.fraction);
- fraction -= b.fraction;
- if (value < b.value) {
+ console.assert(fraction >= bJ.fraction);
+ fraction -= bJ.fraction;
+ if (value < bJ.value) {
return { amount: { currency, value: 0, fraction: 0 }, saturated: true };
}
- value -= b.value;
+ value -= bJ.value;
}
return { amount: { currency, value, fraction }, saturated: false };
@@ -284,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;
}
@@ -294,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;
}
@@ -320,26 +450,30 @@ export class Amounts {
* Parse amount in standard string form (like 'EUR:20.5'),
* throw if the input is not a valid amount.
*/
- static parseOrThrow(s: string): AmountJson {
- const res = Amounts.parse(s);
- if (!res) {
- throw Error(`Can't parse amount: "${s}"`);
+ 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");
+ }
+ if (typeof s.value !== "number") {
+ throw Error("invalid amount object");
+ }
+ if (typeof s.fraction !== "number") {
+ throw Error("invalid amount object");
+ }
+ return { currency: s.currency, value: s.value, fraction: s.fraction };
+ } else if (typeof s === "string") {
+ const res = Amounts.parse(s);
+ if (!res) {
+ throw Error(`Can't parse amount: "${s}"`);
+ }
+ return res;
+ } else {
+ throw Error("invalid amount (illegal type)");
}
- return res;
- }
-
- /**
- * 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 {
@@ -363,16 +497,19 @@ 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");
}
if (n == 0) {
- return { amount: Amounts.getZero(a.currency), saturated: false };
+ return {
+ amount: Amounts.zeroOfCurrency(a.currency),
+ saturated: false,
+ };
}
let x = a;
- let acc = Amounts.getZero(a.currency);
+ let acc = Amounts.zeroOfCurrency(a.currency);
while (n > 1) {
if (n % 2 == 0) {
n = n / 2;
@@ -412,26 +549,31 @@ 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 stringifyValue(a: AmountJson, minFractional = 0): string {
- const av = a.value + Math.floor(a.fraction / amountFractionalBase);
- const af = a.fraction % amountFractionalBase;
+ 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);
+ const af = aJ.fraction % amountFractionalBase;
let s = av.toString();
- if (af) {
- s = s + ".";
+ if (af || minFractional) {
+ s = s + FRAC_SEPARATOR;
let n = af;
for (let i = 0; i < amountFractionalLength; i++) {
if (!n && i >= minFractional) {
@@ -444,4 +586,99 @@ export class Amounts {
return s;
}
+
+ /**
+ * Number of fractional digits needed to fully represent the amount
+ * @param a amount
+ * @returns
+ */
+ static maxFractionalDigits(a: AmountJson): number {
+ if (a.fraction === 0) return 0;
+ if (a.fraction < 0) {
+ console.error("amount fraction can not be negative", a);
+ return 0;
+ }
+ let i = 0;
+ let check = true;
+ let rest = a.fraction;
+ while (rest > 0 && check) {
+ check = rest % 10 === 0;
+ rest = rest / 10;
+ i++;
+ }
+ 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
new file mode 100644
index 000000000..8c38b70a6
--- /dev/null
+++ b/packages/taler-util/src/backup-types.ts
@@ -0,0 +1,42 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 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 { AmountString } from "./taler-types.js";
+
+export interface BackupRecovery {
+ walletRootPriv: string;
+ providers: {
+ name: string;
+ url: string;
+ }[];
+}
+
+export class BackupBackupProviderTerms {
+ /**
+ * Last known supported protocol version.
+ */
+ supported_protocol_version: string;
+
+ /**
+ * Last known annual fee.
+ */
+ annual_fee: AmountString;
+
+ /**
+ * Last known storage limit.
+ */
+ storage_limit_in_megabytes: number;
+}
diff --git a/packages/taler-util/src/backupTypes.ts b/packages/taler-util/src/backupTypes.ts
deleted file mode 100644
index b31a83831..000000000
--- a/packages/taler-util/src/backupTypes.ts
+++ /dev/null
@@ -1,1285 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2020 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/>
- */
-
-/**
- * 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).
- *
- * @author Florian Dold <dold@taler.net>
- */
-
-/**
- * Imports.
- */
-import { DenominationPubKey, UnblindedSignature } from "./talerTypes.js";
-import { TalerProtocolDuration, TalerProtocolTimestamp } from "./time.js";
-
-/**
- * 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: "gnu-taler-wallet-backup-content";
-
- /**
- * Version of the schema.
- */
- schema_version: 1;
-
- /**
- * 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[];
-
- /**
- * Grouped refresh sessions.
- *
- * Sorted by the refresh group ID.
- */
- refresh_groups: BackupRefreshGroup[];
-
- /**
- * Tips.
- *
- * Sorted by the wallet tip ID.
- */
- tips: BackupTip[];
-
- /**
- * Proposals from merchants. The proposal may
- * be deleted as soon as it has been accepted (and thus
- * turned into a purchase).
- *
- * Sorted by the proposal ID.
- */
- proposals: BackupProposal[];
-
- /**
- * 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[];
-
- /**
- * 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[];
-}
-
-/**
- * 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 class BackupBackupProviderTerms {
- /**
- * Last known supported protocol version.
- */
- supported_protocol_version: string;
-
- /**
- * Last known annual fee.
- */
- annual_fee: BackupAmountString;
-
- /**
- * 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;
- finish_is_failure?: boolean;
-
- /**
- * Information about each coin being recouped.
- */
- coins: {
- coin_pub: string;
- recoup_finished: boolean;
- old_amount: BackupAmountString;
- }[];
-}
-
-/**
- * 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;
-}
-
-/**
- * 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;
-
- /**
- * Amount that's left on the coin.
- */
- current_amount: BackupAmountString;
-
- /**
- * 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;
-}
-
-/**
- * Backup information for a withdrawal group.
- *
- * Always part of a BackupReserve.
- */
-export interface BackupWithdrawalGroup {
- withdrawal_group_id: string;
-
- /**
- * Secret seed to derive the planchets.
- */
- secret_seed: string;
-
- /**
- * When was the withdrawal operation started started?
- * Timestamp in milliseconds.
- */
- timestamp_created: TalerProtocolTimestamp;
-
- timestamp_finish?: TalerProtocolTimestamp;
- finish_is_failure?: boolean;
-
- /**
- * 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;
-
- /**
- * Multiset of denominations selected for withdrawal.
- */
- selected_denoms: BackupDenomSel;
-
- selected_denoms_id: OperationUid;
-}
-
-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;
-
-export interface BackupPurchase {
- /**
- * Proposal ID for this purchase. Uniquely identifies the
- * purchase and the proposal.
- */
- proposal_id: string;
-
- /**
- * Contract terms we got from the merchant.
- */
- contract_terms_raw: RawContractTerms;
-
- /**
- * Signature on the contract terms.
- */
- merchant_sig: string;
-
- /**
- * Private key for the nonce. Might eventually be used
- * to prove ownership of the contract.
- */
- nonce_priv: string;
-
- 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;
-
- /**
- * 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;
-
- /**
- * When was the purchase made?
- * Refers to the time that the user accepted.
- */
- timestamp_accept: TalerProtocolTimestamp;
-
- /**
- * Pending refunds for the purchase. A refund is pending
- * when the merchant reports a transient error from the exchange.
- */
- refunds: BackupRefundItem[];
-
- /**
- * Abort status of the payment.
- */
- abort_status?: "abort-refund" | "abort-finished";
-
- /**
- * 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;
-}[];
-
-export interface BackupReserve {
- /**
- * The reserve private key.
- */
- reserve_priv: string;
-
- /**
- * Time when the reserve was created.
- */
- timestamp_created: TalerProtocolTimestamp;
-
- /**
- * Timestamp of the last observed activity.
- *
- * Used to compute when to give up querying the exchange.
- */
- timestamp_last_activity: TalerProtocolTimestamp;
-
- /**
- * Timestamp of when the reserve closed.
- *
- * Note that the last activity can be after the closing time
- * due to recouping.
- */
- timestamp_closed?: TalerProtocolTimestamp;
-
- /**
- * Wire information (as payto URI) for the bank account that
- * transferred funds for this reserve.
- */
- sender_wire?: string;
-
- /**
- * Amount that was sent by the user to fund the reserve.
- */
- instructed_amount: BackupAmountString;
-
- /**
- * Extra state for when this is a withdrawal involving
- * a Taler-integrated bank.
- */
- bank_info?: {
- /**
- * Status URL that the wallet will use to query the status
- * of the Taler withdrawal operation on the bank's side.
- */
- status_url: string;
-
- /**
- * URL that the user should be instructed to navigate to
- * in order to confirm the transfer (or show instructions/help
- * on how to do that at a PoS terminal).
- */
- 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.
- */
- timestamp_reserve_info_posted: TalerProtocolTimestamp | undefined;
-
- /**
- * Time when the reserve was confirmed by the bank.
- *
- * Set to undefined if not confirmed yet.
- */
- timestamp_bank_confirmed: TalerProtocolTimestamp | undefined;
- };
-
- /**
- * Pre-allocated withdrawal group ID that will be
- * used for the first withdrawal.
- *
- * (Already created so it can be referenced in the transactions list
- * before it really exists, as there'll be an entry for the withdrawal
- * even before the withdrawal group really has been created).
- */
- initial_withdrawal_group_id: string;
-
- /**
- * Denominations selected for the initial withdrawal.
- * Stored here to show costs before withdrawal has begun.
- */
- initial_selected_denoms: BackupDenomSel;
-
- /**
- * Groups of withdrawal operations for this reserve. Typically just one.
- */
- withdrawal_groups: BackupWithdrawalGroup[];
-}
-
-/**
- * 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;
-
- wad_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;
-}
-
-/**
- * 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;
-
- protocol_version_range: 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[];
-
- /**
- * Reserves at the exchange.
- */
- reserves: BackupReserve[];
-
- /**
- * 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[];
-
- /**
- * 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",
-}
-
-/**
- * Proposal by a merchant.
- */
-export interface BackupProposal {
- /**
- * Base URL of the merchant that proposed the purchase.
- */
- merchant_base_url: string;
-
- /**
- * Downloaded data from the merchant.
- */
- contract_terms_raw?: RawContractTerms;
-
- /**
- * Signature on the contract terms.
- *
- * Must be present if contract_terms_raw is present.
- */
- merchant_sig?: string;
-
- /**
- * Unique ID when the order is stored in the wallet DB.
- */
- proposal_id: string;
-
- /**
- * Merchant-assigned order ID of the proposal.
- */
- order_id: string;
-
- /**
- * Timestamp of when the record
- * was created.
- */
- timestamp: TalerProtocolTimestamp;
-
- /**
- * Private key for the nonce.
- */
- nonce_priv: string;
-
- /**
- * Claim token initially given by the merchant.
- */
- claim_token: string | undefined;
-
- /**
- * 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;
-}
-
-export interface BackupRecovery {
- walletRootPriv: string;
- providers: {
- 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..51359129d
--- /dev/null
+++ b/packages/taler-util/src/bank-api-client.ts
@@ -0,0 +1,440 @@
+/*
+ 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,
+ HttpRequestLibrary,
+ readSuccessResponseJsonOrThrow,
+ 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(
+ private 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());
+ }
+}
diff --git a/packages/taler-util/src/base64.ts b/packages/taler-util/src/base64.ts
new file mode 100644
index 000000000..5d39ee581
--- /dev/null
+++ b/packages/taler-util/src/base64.ts
@@ -0,0 +1,64 @@
+// Converts an ArrayBuffer directly to base64, without any intermediate 'convert to string then
+// use window.btoa' step. According to my tests, this appears to be a faster approach:
+// http://jsperf.com/encoding-xhr-image-data/5
+
+/*
+MIT LICENSE
+Copyright 2011 Jon Leighton
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+
+export function base64FromArrayBuffer(arrayBuffer: ArrayBuffer): string {
+ var base64 = "";
+ var encodings =
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+
+ var bytes = new Uint8Array(arrayBuffer);
+ var byteLength = bytes.byteLength;
+ var byteRemainder = byteLength % 3;
+ var mainLength = byteLength - byteRemainder;
+
+ var a, b, c, d;
+ var chunk;
+
+ // Main loop deals with bytes in chunks of 3
+ for (var i = 0; i < mainLength; i = i + 3) {
+ // Combine the three bytes into a single integer
+ chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2];
+
+ // Use bitmasks to extract 6-bit segments from the triplet
+ a = (chunk & 16515072) >> 18; // 16515072 = (2^6 - 1) << 18
+ b = (chunk & 258048) >> 12; // 258048 = (2^6 - 1) << 12
+ c = (chunk & 4032) >> 6; // 4032 = (2^6 - 1) << 6
+ d = chunk & 63; // 63 = 2^6 - 1
+
+ // Convert the raw binary segments to the appropriate ASCII encoding
+ base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d];
+ }
+
+ // Deal with the remaining bytes and padding
+ if (byteRemainder == 1) {
+ chunk = bytes[mainLength];
+
+ a = (chunk & 252) >> 2; // 252 = (2^6 - 1) << 2
+
+ // Set the 4 least significant bits to zero
+ b = (chunk & 3) << 4; // 3 = 2^2 - 1
+
+ base64 += encodings[a] + encodings[b] + "==";
+ } else if (byteRemainder == 2) {
+ chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1];
+
+ a = (chunk & 64512) >> 10; // 64512 = (2^6 - 1) << 10
+ b = (chunk & 1008) >> 4; // 1008 = (2^6 - 1) << 4
+
+ // Set the 2 least significant bits to zero
+ c = (chunk & 15) << 2; // 15 = 2^4 - 1
+
+ base64 += encodings[a] + encodings[b] + encodings[c] + "=";
+ }
+
+ return base64;
+}
diff --git a/packages/taler-util/src/bech32.ts b/packages/taler-util/src/bech32.ts
index 03c24e807..e48e9ac3e 100644
--- a/packages/taler-util/src/bech32.ts
+++ b/packages/taler-util/src/bech32.ts
@@ -18,7 +18,7 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
-var CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l';
+var CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
var GENERATOR = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];
const encodings: any = {
@@ -38,7 +38,7 @@ function getEncodingConst(enc: any) {
} else if (enc == encodings.BECH32M) {
return 0x2bc830a3;
} else {
- throw new Error('unknown encoding')
+ throw new Error("unknown encoding");
}
}
@@ -46,7 +46,7 @@ function polymod(values: any) {
var chk = 1;
for (var p = 0; p < values.length; ++p) {
var top = chk >> 25;
- chk = (chk & 0x1ffffff) << 5 ^ values[p];
+ chk = ((chk & 0x1ffffff) << 5) ^ values[p];
for (var i = 0; i < 5; ++i) {
if ((top >> i) & 1) {
chk ^= GENERATOR[i];
@@ -78,14 +78,14 @@ function createChecksum(hrp: any, data: any, enc: any) {
var mod = polymod(values) ^ getEncodingConst(enc);
var ret = [];
for (var p = 0; p < 6; ++p) {
- ret.push((mod >> 5 * (5 - p)) & 31);
+ ret.push((mod >> (5 * (5 - p))) & 31);
}
return ret;
}
function encode(hrp: any, data: any, enc: any): string {
var combined = data.concat(createChecksum(hrp, data, enc));
- var ret = hrp + '1';
+ var ret = hrp + "1";
for (var p = 0; p < combined.length; ++p) {
ret += CHARSET.charAt(combined[p]);
}
@@ -111,7 +111,7 @@ function decode(bechString: any, enc: any) {
return null;
}
bechString = bechString.toLowerCase();
- var pos = bechString.lastIndexOf('1');
+ var pos = bechString.lastIndexOf("1");
if (pos < 1 || pos + 7 > bechString.length || bechString.length > 90) {
return null;
}
@@ -128,4 +128,4 @@ function decode(bechString: any, enc: any) {
return null;
}
return { hrp: hrp, data: data.slice(0, data.length - 6) };
-} \ No newline at end of file
+}
diff --git a/packages/taler-util/src/bitcoin.test.ts b/packages/taler-util/src/bitcoin.test.ts
index 3070a8ab5..fe8de89c3 100644
--- a/packages/taler-util/src/bitcoin.test.ts
+++ b/packages/taler-util/src/bitcoin.test.ts
@@ -19,59 +19,90 @@
*/
import test from "ava";
-import {
- generateFakeSegwitAddress,
-} from "./bitcoin.js";
+import { generateFakeSegwitAddress } from "./bitcoin.js";
test("generate testnet", (t) => {
- const [addr1, addr2] = generateFakeSegwitAddress("B9E0EXNDKGJX7WFAEVZCZXM0R661T66YWD71N7NRFDEWQEV10XSG", "tb1qhxrhccqexg0dv4nltgkuw4fg2ce7muplmjsn0v")
+ const [addr1, addr2] = generateFakeSegwitAddress(
+ "B9E0EXNDKGJX7WFAEVZCZXM0R661T66YWD71N7NRFDEWQEV10XSG",
+ "tb1qhxrhccqexg0dv4nltgkuw4fg2ce7muplmjsn0v",
+ );
t.assert(addr1 === "tb1qtfwqwaj6tsrhdtvuyhflr6nklm8ldqxpf0lfjw");
t.assert(addr2 === "tb1qmfwqwa5vr5vdac6wr20ts76aewakzpmns40yuf");
});
test("generate mainnet", (t) => {
- const [addr1, addr2] = generateFakeSegwitAddress("B9E0EXNDKGJX7WFAEVZCZXM0R661T66YWD71N7NRFDEWQEV10XSG", "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq")
+ const [addr1, addr2] = generateFakeSegwitAddress(
+ "B9E0EXNDKGJX7WFAEVZCZXM0R661T66YWD71N7NRFDEWQEV10XSG",
+ "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq",
+ );
//bc
t.assert(addr1 === "bc1qtfwqwaj6tsrhdtvuyhflr6nklm8ldqxprfy6fa");
t.assert(addr2 === "bc1qmfwqwa5vr5vdac6wr20ts76aewakzpmn6n5h86");
});
test("generate Regtest", (t) => {
- const [addr1, addr2] = generateFakeSegwitAddress("B9E0EXNDKGJX7WFAEVZCZXM0R661T66YWD71N7NRFDEWQEV10XSG", "bcrtqhxrhccqexg0dv4nltgkuw4fg2ce7muplmjsn0v")
+ const [addr1, addr2] = generateFakeSegwitAddress(
+ "B9E0EXNDKGJX7WFAEVZCZXM0R661T66YWD71N7NRFDEWQEV10XSG",
+ "bcrtqhxrhccqexg0dv4nltgkuw4fg2ce7muplmjsn0v",
+ );
t.assert(addr1 === "bcrt1qtfwqwaj6tsrhdtvuyhflr6nklm8ldqxptxxy98");
t.assert(addr2 === "bcrt1qmfwqwa5vr5vdac6wr20ts76aewakzpmnjukftq");
});
-
test("unknown net", (t) => {
t.throws(() => {
- generateFakeSegwitAddress("B9E0EXNDKGJX7WFAEVZCZXM0R661T66YWD71N7NRFDEWQEV10XSG", "abqhxrhccqexg0dv4nltgkuw4fg2ce7muplmjsn0v")
- })
-
+ generateFakeSegwitAddress(
+ "B9E0EXNDKGJX7WFAEVZCZXM0R661T66YWD71N7NRFDEWQEV10XSG",
+ "abqhxrhccqexg0dv4nltgkuw4fg2ce7muplmjsn0v",
+ );
+ });
});
test("invalid or no reserve", (t) => {
let result = undefined;
- // empty
- result = generateFakeSegwitAddress("", "bcrtqhxrhccqexg0dv4nltgkuw4fg2ce7muplmjsn0v")
- t.deepEqual(result, [])
- // small
- result = generateFakeSegwitAddress("s", "bcrtqhxrhccqexg0dv4nltgkuw4fg2ce7muplmjsn0v")
- t.deepEqual(result, [])
- result = generateFakeSegwitAddress("asdsad", "bcrtqhxrhccqexg0dv4nltgkuw4fg2ce7muplmjsn0v")
- t.deepEqual(result, [])
- result = generateFakeSegwitAddress("asdasdasdasdasdasd", "bcrtqhxrhccqexg0dv4nltgkuw4fg2ce7muplmjsn0v")
- t.deepEqual(result, [])
- result = generateFakeSegwitAddress("B9E0EXNDKGJX7WFAEVZCZXM0R661T66YWD71N7NRFDEWQEV10XS", "bcrtqhxrhccqexg0dv4nltgkuw4fg2ce7muplmjsn0v")
- t.deepEqual(result, [])
- result = generateFakeSegwitAddress("B9E0EXNDKGJX7WFAEVZCZXM0R661T66YWD71N7NRFDEWQEV10XSSSS", "bcrtqhxrhccqexg0dv4nltgkuw4fg2ce7muplmjsn0v")
- t.deepEqual(result, [])
+ // empty
+ result = generateFakeSegwitAddress(
+ "",
+ "bcrtqhxrhccqexg0dv4nltgkuw4fg2ce7muplmjsn0v",
+ );
+ t.deepEqual(result, []);
+ // small
+ result = generateFakeSegwitAddress(
+ "s",
+ "bcrtqhxrhccqexg0dv4nltgkuw4fg2ce7muplmjsn0v",
+ );
+ t.deepEqual(result, []);
+ result = generateFakeSegwitAddress(
+ "asdsad",
+ "bcrtqhxrhccqexg0dv4nltgkuw4fg2ce7muplmjsn0v",
+ );
+ t.deepEqual(result, []);
+ result = generateFakeSegwitAddress(
+ "asdasdasdasdasdasd",
+ "bcrtqhxrhccqexg0dv4nltgkuw4fg2ce7muplmjsn0v",
+ );
+ t.deepEqual(result, []);
+ result = generateFakeSegwitAddress(
+ "B9E0EXNDKGJX7WFAEVZCZXM0R661T66YWD71N7NRFDEWQEV10XS",
+ "bcrtqhxrhccqexg0dv4nltgkuw4fg2ce7muplmjsn0v",
+ );
+ t.deepEqual(result, []);
+ result = generateFakeSegwitAddress(
+ "B9E0EXNDKGJX7WFAEVZCZXM0R661T66YWD71N7NRFDEWQEV10XSSSS",
+ "bcrtqhxrhccqexg0dv4nltgkuw4fg2ce7muplmjsn0v",
+ );
+ t.deepEqual(result, []);
// no reserve
- result = generateFakeSegwitAddress(undefined, "bcrtqhxrhccqexg0dv4nltgkuw4fg2ce7muplmjsn0v")
- t.deepEqual(result, [])
- result = generateFakeSegwitAddress("B9E0EXNDKGJX7WFAEVZCZXM0R661T66YWD71N7NRFDEWQEV10XS-", "bcrtqhxrhccqexg0dv4nltgkuw4fg2ce7muplmjsn0v")
- t.deepEqual(result, [])
+ result = generateFakeSegwitAddress(
+ undefined,
+ "bcrtqhxrhccqexg0dv4nltgkuw4fg2ce7muplmjsn0v",
+ );
+ t.deepEqual(result, []);
+ result = generateFakeSegwitAddress(
+ "B9E0EXNDKGJX7WFAEVZCZXM0R661T66YWD71N7NRFDEWQEV10XS-",
+ "bcrtqhxrhccqexg0dv4nltgkuw4fg2ce7muplmjsn0v",
+ );
+ t.deepEqual(result, []);
});
-
diff --git a/packages/taler-util/src/bitcoin.ts b/packages/taler-util/src/bitcoin.ts
index 822652a8a..37b7ae6b9 100644
--- a/packages/taler-util/src/bitcoin.ts
+++ b/packages/taler-util/src/bitcoin.ts
@@ -23,10 +23,9 @@
* Imports.
*/
import { AmountJson, Amounts } from "./amounts.js";
-import { decodeCrock } from "./talerCrypto.js";
+import { decodeCrock } from "./taler-crypto.js";
import * as segwit from "./segwit_addr.js";
-
function buf2hex(buffer: Uint8Array) {
// buffer is an ArrayBuffer
return [...new Uint8Array(buffer)]
@@ -35,24 +34,23 @@ function buf2hex(buffer: Uint8Array) {
}
const hext2buf = (hexString: string) =>
- new Uint8Array(hexString.match(/.{1,2}/g)!.map(byte => parseInt(byte, 16)));
-
+ new Uint8Array(hexString.match(/.{1,2}/g)!.map((byte) => parseInt(byte, 16)));
export function generateFakeSegwitAddress(
reservePub: string | undefined,
- addr: string
+ addr: string,
): string[] {
- if (!reservePub) return []
+ if (!reservePub) return [];
let pub;
try {
pub = decodeCrock(reservePub);
} catch {
// pub = new Uint8Array(0)
}
- if (!pub || pub.length !== 32) return []
+ if (!pub || pub.length !== 32) return [];
const first_rnd = new Uint8Array(4);
- first_rnd.set(pub.subarray(0, 4))
+ first_rnd.set(pub.subarray(0, 4));
const second_rnd = new Uint8Array(4);
second_rnd.set(pub.subarray(0, 4));
@@ -80,7 +78,7 @@ export function generateFakeSegwitAddress(
const addr1 = segwit.default.encode(prefix, 0, first_part);
const addr2 = segwit.default.encode(prefix, 0, second_part);
- return [addr1, addr2]
+ return [addr1, addr2];
}
// https://github.com/bitcoin/bitcoin/blob/master/src/policy/policy.cpp
diff --git a/packages/taler-util/src/clk.test.ts b/packages/taler-util/src/clk.test.ts
new file mode 100644
index 000000000..9077f07de
--- /dev/null
+++ b/packages/taler-util/src/clk.test.ts
@@ -0,0 +1,39 @@
+/*
+ This file is part of GNU Taler
+ (C) 2018-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/>
+ */
+
+/**
+ * Type-safe codecs for converting from/to JSON.
+ */
+
+import test from "ava";
+import { clk } from "./clk.js";
+
+test("bla", (t) => {
+ const prog = clk.program("foo", {
+ help: "Hello",
+ });
+
+ let success = false;
+
+ prog.maybeOption("opt1", ["-o", "--opt1"], clk.INT).action((args) => {
+ success = true;
+ t.deepEqual(args.foo.opt1, 42);
+ });
+
+ prog.run(["bla", "-o", "42"]);
+
+ t.true(success);
+});
diff --git a/packages/taler-util/src/clk.ts b/packages/taler-util/src/clk.ts
index d172eed48..60969af69 100644
--- a/packages/taler-util/src/clk.ts
+++ b/packages/taler-util/src/clk.ts
@@ -17,15 +17,20 @@
/**
* Imports.
*/
-import process from "process";
-import path from "path";
-import readline from "readline";
+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;
@@ -329,6 +334,22 @@ export namespace clk {
const myArgs: any = (parsedArgs[this.argKey] = {});
const foundOptions: { [name: string]: boolean } = {};
const currentName = this.name ?? progname;
+ const storeOption = (def: OptionDef, value: string) => {
+ foundOptions[def.name] = true;
+ if (def.conv === INT) {
+ 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");
+ }
+ };
+ const storeFlag = (def: OptionDef, value: boolean) => {
+ foundOptions[def.name] = true;
+ myArgs[def.name] = value;
+ };
for (i = 0; i < unparsedArgs.length; i++) {
const argVal = unparsedArgs[i];
if (argsTerminated == false) {
@@ -344,30 +365,28 @@ 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");
}
- foundOptions[d.name] = true;
- myArgs[d.name] = true;
+ storeFlag(d, true);
} else {
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");
}
- myArgs[d.name] = unparsedArgs[i + 1];
+ storeOption(d, unparsedArgs[i + 1]);
i++;
} else {
- myArgs[d.name] = r.value;
+ storeOption(d, r.value);
}
- foundOptions[d.name] = true;
}
continue;
}
@@ -378,25 +397,23 @@ 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) {
- myArgs[opt.name] = true;
- foundOptions[opt.name] = true;
+ storeFlag(opt, true);
} else {
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 {
- myArgs[opt.name] = unparsedArgs[i + 1];
+ storeOption(opt, unparsedArgs[i + 1]);
i++;
}
} else {
- myArgs[opt.name] = optShort.substring(si + 1);
+ storeOption(opt, optShort.substring(si + 1));
}
- foundOptions[opt.name] = true;
break;
}
}
@@ -407,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;
@@ -416,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];
@@ -426,7 +443,7 @@ export namespace clk {
if (parsedArgs[this.argKey].help) {
this.printHelp(progname, parents);
- process.exit(0);
+ processExit(0);
throw Error("not reached");
}
@@ -439,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");
}
}
@@ -453,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");
}
}
@@ -481,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");
}
}
@@ -508,16 +525,21 @@ export namespace clk {
});
}
- run(): void {
- const args = process.argv;
- if (args.length < 2) {
+ run(cmdlineArgs?: string[]): void {
+ let args: string[];
+ if (cmdlineArgs) {
+ args = cmdlineArgs;
+ } else {
+ 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[1]);
- const rest = args.slice(2);
+ const progname = pathBasename(args[0]);
+ const rest = args.slice(1);
this.mainCommand.run(progname, [], rest, {});
}
@@ -595,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,
@@ -606,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 2ea64a249..54d450d82 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.
*/
@@ -134,7 +139,7 @@ class UnionCodecBuilder<
TargetType,
TagPropertyLabel extends keyof TargetType,
CommonBaseType,
- PartialTargetType
+ PartialTargetType,
> {
private alternatives = new Map<any, Alternative>();
@@ -186,7 +191,7 @@ class UnionCodecBuilder<
throw new DecodingError(
`expected tag for ${objectDisplayName} at ${renderContext(
c,
- )}.${discriminator}`,
+ )}.${String(discriminator)}`,
);
}
const alt = alternatives.get(d);
@@ -194,7 +199,7 @@ class UnionCodecBuilder<
throw new DecodingError(
`unknown tag for ${objectDisplayName} ${d} at ${renderContext(
c,
- )}.${discriminator}`,
+ )}.${String(discriminator)}`,
);
}
const altDecoded = alt.codec.decode(x);
@@ -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,19 @@ export function codecOptional<V>(innerCodec: Codec<V>): Codec<V | undefined> {
};
}
+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 +518,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.test.ts b/packages/taler-util/src/contract-terms.test.ts
new file mode 100644
index 000000000..fc0920501
--- /dev/null
+++ b/packages/taler-util/src/contract-terms.test.ts
@@ -0,0 +1,127 @@
+/*
+ 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/>
+ */
+
+/**
+ * Imports.
+ */
+import test from "ava";
+import { initNodePrng } from "./prng-node.js";
+import { ContractTermsUtil } from "./contract-terms.js";
+
+// Since we import nacl-fast directly (and not via index.node.ts), we need to
+// init the PRNG manually.
+initNodePrng();
+
+test("contract terms canon hashing", (t) => {
+ const cReq = {
+ foo: 42,
+ bar: "hello",
+ $forgettable: {
+ foo: true,
+ },
+ };
+
+ const c1 = ContractTermsUtil.saltForgettable(cReq);
+ const c2 = ContractTermsUtil.saltForgettable(cReq);
+ t.assert(typeof cReq.$forgettable.foo === "boolean");
+ t.assert(typeof c1.$forgettable.foo === "string");
+ t.assert(c1.$forgettable.foo !== c2.$forgettable.foo);
+
+ const h1 = ContractTermsUtil.hashContractTerms(c1);
+
+ const c3 = ContractTermsUtil.scrub(JSON.parse(JSON.stringify(c1)));
+
+ t.assert(c3.foo === undefined);
+ t.assert(c3.bar === cReq.bar);
+
+ const h2 = ContractTermsUtil.hashContractTerms(c3);
+
+ t.deepEqual(h1, h2);
+});
+
+test("contract terms canon hashing (nested)", (t) => {
+ const cReq = {
+ foo: 42,
+ bar: {
+ prop1: "hello, world",
+ $forgettable: {
+ prop1: true,
+ },
+ },
+ $forgettable: {
+ bar: true,
+ },
+ };
+
+ const c1 = ContractTermsUtil.saltForgettable(cReq);
+
+ t.is(typeof c1.$forgettable.bar, "string");
+ t.is(typeof c1.bar.$forgettable.prop1, "string");
+
+ const forgetPath = (x: any, s: string) =>
+ ContractTermsUtil.forgetAll(x, (p) => p.join(".") === s);
+
+ // Forget bar first
+ const c2 = forgetPath(c1, "bar");
+
+ // Forget bar.prop1 first
+ const c3 = forgetPath(forgetPath(c1, "bar.prop1"), "bar");
+
+ // Forget everything
+ const c4 = ContractTermsUtil.scrub(c1);
+
+ const h1 = ContractTermsUtil.hashContractTerms(c1);
+ const h2 = ContractTermsUtil.hashContractTerms(c2);
+ const h3 = ContractTermsUtil.hashContractTerms(c3);
+ const h4 = ContractTermsUtil.hashContractTerms(c4);
+
+ t.is(h1, h2);
+ t.is(h1, h3);
+ t.is(h1, h4);
+
+ // Doesn't contain salt
+ t.false(ContractTermsUtil.validateForgettable(cReq));
+
+ t.true(ContractTermsUtil.validateForgettable(c1));
+ t.true(ContractTermsUtil.validateForgettable(c2));
+ t.true(ContractTermsUtil.validateForgettable(c3));
+ t.true(ContractTermsUtil.validateForgettable(c4));
+});
+
+test("contract terms reference vector", (t) => {
+ const j = {
+ k1: 1,
+ $forgettable: {
+ k1: "SALT",
+ },
+ k2: {
+ n1: true,
+ $forgettable: {
+ n1: "salt",
+ },
+ },
+ k3: {
+ n1: "string",
+ },
+ };
+
+ const h = ContractTermsUtil.hashContractTerms(j);
+
+ t.deepEqual(
+ h,
+ "VDE8JPX0AEEE3EX1K8E11RYEWSZQKGGZCV6BWTE4ST1C8711P7H850Z7F2Q2HSSYETX87ERC2JNHWB7GTDWTDWMM716VKPSRBXD7SRR",
+ );
+});
diff --git a/packages/taler-util/src/contract-terms.ts b/packages/taler-util/src/contract-terms.ts
new file mode 100644
index 000000000..b906a1d7f
--- /dev/null
+++ b/packages/taler-util/src/contract-terms.ts
@@ -0,0 +1,231 @@
+/*
+ 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 { canonicalJson } from "./helpers.js";
+import { Logger } from "./logging.js";
+import {
+ decodeCrock,
+ encodeCrock,
+ getRandomBytes,
+ hash,
+ kdf,
+ stringToBytes,
+} from "./taler-crypto.js";
+
+const logger = new Logger("contractTerms.ts");
+
+export namespace ContractTermsUtil {
+ export function forgetAllImpl(
+ anyJson: any,
+ path: string[],
+ pred: PathPredicate,
+ ): any {
+ const dup = JSON.parse(JSON.stringify(anyJson));
+ if (Array.isArray(dup)) {
+ for (let i = 0; i < dup.length; i++) {
+ dup[i] = forgetAllImpl(dup[i], [...path, `${i}`], pred);
+ }
+ } else if (typeof dup === "object" && dup != null) {
+ if (typeof dup.$forgettable === "object") {
+ for (const x of Object.keys(dup.$forgettable)) {
+ if (!pred([...path, x])) {
+ continue;
+ }
+ if (!dup.$forgotten) {
+ dup.$forgotten = {};
+ }
+ if (!dup.$forgotten[x]) {
+ const membValCanon = stringToBytes(
+ canonicalJson(scrub(dup[x])) + "\0",
+ );
+ const membSalt = stringToBytes(dup.$forgettable[x] + "\0");
+ const h = kdf(64, membValCanon, membSalt, new Uint8Array([]));
+ dup.$forgotten[x] = encodeCrock(h);
+ }
+ delete dup[x];
+ delete dup.$forgettable[x];
+ }
+ if (Object.keys(dup.$forgettable).length === 0) {
+ delete dup.$forgettable;
+ }
+ }
+ for (const x of Object.keys(dup)) {
+ if (x.startsWith("$")) {
+ continue;
+ }
+ dup[x] = forgetAllImpl(dup[x], [...path, x], pred);
+ }
+ }
+ return dup;
+ }
+
+ export type PathPredicate = (path: string[]) => boolean;
+
+ /**
+ * Scrub all forgettable members from an object.
+ */
+ export function scrub(anyJson: any): any {
+ return forgetAllImpl(anyJson, [], () => true);
+ }
+
+ /**
+ * Recursively forget all forgettable members of an object,
+ * where the path matches a predicate.
+ */
+ export function forgetAll(anyJson: any, pred: PathPredicate): any {
+ return forgetAllImpl(anyJson, [], pred);
+ }
+
+ /**
+ * Generate a salt for all members marked as forgettable,
+ * but which don't have an actual salt yet.
+ */
+ export function saltForgettable(anyJson: any): any {
+ const dup = JSON.parse(JSON.stringify(anyJson));
+ if (Array.isArray(dup)) {
+ for (let i = 0; i < dup.length; i++) {
+ dup[i] = saltForgettable(dup[i]);
+ }
+ } else if (typeof dup === "object" && dup !== null) {
+ if (typeof dup.$forgettable === "object") {
+ for (const k of Object.keys(dup.$forgettable)) {
+ if (dup.$forgettable[k] === true) {
+ dup.$forgettable[k] = encodeCrock(getRandomBytes(32));
+ }
+ }
+ }
+ for (const x of Object.keys(dup)) {
+ if (x.startsWith("$")) {
+ continue;
+ }
+ dup[x] = saltForgettable(dup[x]);
+ }
+ }
+ return dup;
+ }
+
+ const nameRegex = /^[0-9A-Za-z_]+$/;
+
+ /**
+ * Check that the given JSON object is well-formed with regards
+ * to forgettable fields and other restrictions for forgettable JSON.
+ */
+ export function validateForgettable(anyJson: any): boolean {
+ if (typeof anyJson === "string") {
+ return true;
+ }
+ if (typeof anyJson === "number") {
+ return (
+ Number.isInteger(anyJson) &&
+ anyJson >= Number.MIN_SAFE_INTEGER &&
+ anyJson <= Number.MAX_SAFE_INTEGER
+ );
+ }
+ if (typeof anyJson === "boolean") {
+ return true;
+ }
+ if (anyJson === null) {
+ return true;
+ }
+ if (Array.isArray(anyJson)) {
+ return anyJson.every((x) => validateForgettable(x));
+ }
+ if (typeof anyJson === "object") {
+ for (const k of Object.keys(anyJson)) {
+ if (k.match(nameRegex)) {
+ if (validateForgettable(anyJson[k])) {
+ continue;
+ } else {
+ return false;
+ }
+ }
+ if (k === "$forgettable") {
+ const fga = anyJson.$forgettable;
+ if (!fga || typeof fga !== "object") {
+ return false;
+ }
+ for (const fk of Object.keys(fga)) {
+ if (!fk.match(nameRegex)) {
+ return false;
+ }
+ if (!(fk in anyJson)) {
+ return false;
+ }
+ const fv = anyJson.$forgettable[fk];
+ if (typeof fv !== "string") {
+ return false;
+ }
+ }
+ } else if (k === "$forgotten") {
+ const fgo = anyJson.$forgotten;
+ if (!fgo || typeof fgo !== "object") {
+ return false;
+ }
+ for (const fk of Object.keys(fgo)) {
+ if (!fk.match(nameRegex)) {
+ return false;
+ }
+ // Check that the value has actually been forgotten.
+ if (fk in anyJson) {
+ return false;
+ }
+ const fv = anyJson.$forgotten[fk];
+ if (typeof fv !== "string") {
+ return false;
+ }
+ try {
+ const decFv = decodeCrock(fv);
+ if (decFv.length != 64) {
+ return false;
+ }
+ } catch (e) {
+ return false;
+ }
+ // Check that salt has been deleted after forgetting.
+ if (anyJson.$forgettable?.[k] !== undefined) {
+ return false;
+ }
+ }
+ } else {
+ return false;
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Check that no forgettable information has been forgotten.
+ *
+ * Must only be called on an object already validated with validateForgettable.
+ */
+ export function validateNothingForgotten(contractTerms: any): boolean {
+ throw Error("not implemented yet");
+ }
+
+ /**
+ * Hash a contract terms object. Forgettable fields
+ * are scrubbed and JSON canonicalization is applied
+ * before hashing.
+ */
+ export function hashContractTerms(contractTerms: unknown): string {
+ const cleaned = scrub(contractTerms);
+ const canon = canonicalJson(cleaned) + "\0";
+ const bytes = stringToBytes(canon);
+ return encodeCrock(hash(bytes));
+ }
+}
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/fnutils.ts b/packages/taler-util/src/fnutils.ts
index 85fac6680..ff309f694 100644
--- a/packages/taler-util/src/fnutils.ts
+++ b/packages/taler-util/src/fnutils.ts
@@ -35,4 +35,4 @@ export namespace fnutil {
}
return false;
}
-} \ No newline at end of file
+}
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..d682dcfa0
--- /dev/null
+++ b/packages/taler-util/src/http-client/merchant.ts
@@ -0,0 +1,2343 @@
+/*
+ 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,
+ 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 = "10:0:6";
+
+ 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-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..c0004a218
--- /dev/null
+++ b/packages/taler-util/src/http-client/types.ts
@@ -0,0 +1,5290 @@
+import { deprecate } from "util";
+import { codecForAmountString } from "../amounts.js";
+import {
+ Codec,
+ buildCodecForObject,
+ buildCodecForUnion,
+ codecForAny,
+ codecForBoolean,
+ codecForConstNumber,
+ codecForConstString,
+ codecForEither,
+ codecForList,
+ codecForMap,
+ codecForNumber,
+ codecForString,
+ codecOptional,
+} from "../codec.js";
+import { PaytoString, codecForPaytoString } from "../payto.js";
+import {
+ AmountString,
+ InternationalizedString,
+ codecForInternationalizedString,
+ codecForLocation,
+} from "../taler-types.js";
+import { TalerUriString, codecForTalerUriString } from "../taleruri.js";
+import {
+ AbsoluteTime,
+ TalerProtocolDuration,
+ TalerProtocolTimestamp,
+ codecForAbsoluteTime,
+ 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", 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", codecForString())
+ .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 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", codecForList(codecForTax()))
+ .property("address", codecForLocation())
+ .property("next_restock", 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()))
+ .property("pay_duration", codecOptional(codecForDuration))
+ .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");
+
+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 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;
+ }
+
+ type Order = MinimalOrderDetail | ContractTerms;
+
+ 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;
+ }
+
+ 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?: AmountString;
+
+ minimum_age?: Integer;
+
+ pay_duration?: RelativeTime;
+ }
+ 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 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 7b3ec55d1..f43f543ea 100644
--- a/packages/taler-util/src/i18n.ts
+++ b/packages/taler-util/src/i18n.ts
@@ -10,11 +10,11 @@ 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]) {
- strings[lang] = {}
+ strings[lang] = {};
// logger.warn(`language ${lang} not found, defaulting to source strings`);
}
jed = new jedLib.Jed(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,13 +42,16 @@ function toI18nString(stringSeq: ReadonlyArray<string>): string {
s += `%${i + 1}$s`;
}
}
- return s;
+ return s as TranslatedString;
}
/**
* Internationalize a string template with arbitrary serialized values.
*/
-export function singular(stringSeq: TemplateStringsArray, ...values: any[]): string {
+export function singular(
+ stringSeq: TemplateStringsArray,
+ ...values: any[]
+): TranslatedString {
const s = toI18nString(stringSeq);
const tr = jed
.translate(s)
@@ -60,23 +66,29 @@ export function singular(stringSeq: TemplateStringsArray, ...values: any[]): str
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);
}
/**
* Internationalize a string template without serializing
*/
-export function Translate({ children, debug, }: { children: any, debug?: boolean }): any {
+export function Translate({
+ children,
+ debug,
+}: {
+ children: any;
+ debug?: boolean;
+}): any {
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)
+ console.log("looking for ", s, "got", translation);
}
return replacePlaceholderWithValues(translation, c);
}
@@ -95,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/);
@@ -148,4 +160,3 @@ export const i18n = {
Translate,
translate,
};
-
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 199218d69..24d6e9950 100644
--- a/packages/taler-util/src/index.ts
+++ b/packages/taler-util/src/index.ts
@@ -2,33 +2,62 @@ 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 "./backupTypes.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 "./talerTypes.js";
-export * from "./taleruri.js";
-export * from "./time.js";
-export * from "./transactionsTypes.js";
-export * from "./walletTypes.js";
+export * from "./http-client/bank-conversion.js";
+export * from "./http-client/authentication.js";
+export * from "./http-client/bank-core.js";
+export * from "./http-client/merchant.js";
+export * from "./http-client/challenger.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/exchange.js";
+export { CacheEvictor } from "./http-client/utils.js";
+export * from "./http-client/officer-account.js";
+export * from "./http-client/types.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 "./talerCrypto.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 * from "./merchant-api-types.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 "./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.d.ts b/packages/taler-util/src/kdf.d.ts
index 80a6da41e..eba1455ff 100644
--- a/packages/taler-util/src/kdf.d.ts
+++ b/packages/taler-util/src/kdf.d.ts
@@ -1,5 +1,21 @@
export declare function sha512(data: Uint8Array): Uint8Array;
-export declare function hmac(digest: (d: Uint8Array) => Uint8Array, blockSize: number, key: Uint8Array, message: Uint8Array): Uint8Array;
-export declare function hmacSha512(key: Uint8Array, message: Uint8Array): Uint8Array;
-export declare function hmacSha256(key: Uint8Array, message: Uint8Array): Uint8Array;
-export declare function kdf(outputLength: number, ikm: Uint8Array, salt: Uint8Array, info: Uint8Array): Uint8Array;
+export declare function hmac(
+ digest: (d: Uint8Array) => Uint8Array,
+ blockSize: number,
+ key: Uint8Array,
+ message: Uint8Array,
+): Uint8Array;
+export declare function hmacSha512(
+ key: Uint8Array,
+ message: Uint8Array,
+): Uint8Array;
+export declare function hmacSha256(
+ key: Uint8Array,
+ message: Uint8Array,
+): Uint8Array;
+export declare function kdf(
+ outputLength: number,
+ ikm: Uint8Array,
+ salt: Uint8Array,
+ info: Uint8Array,
+): Uint8Array;
diff --git a/packages/taler-util/src/kdf.js b/packages/taler-util/src/kdf.js
index 32f17beac..6cd3d1ddf 100644
--- a/packages/taler-util/src/kdf.js
+++ b/packages/taler-util/src/kdf.js
@@ -16,61 +16,60 @@
import * as nacl from "./nacl-fast.js";
import { sha256 } from "./sha256.js";
export function sha512(data) {
- return nacl.hash(data);
+ return nacl.hash(data);
}
export function hmac(digest, blockSize, key, message) {
- if (key.byteLength > blockSize) {
- key = digest(key);
- }
- if (key.byteLength < blockSize) {
- const k = key;
- key = new Uint8Array(blockSize);
- key.set(k, 0);
- }
- const okp = new Uint8Array(blockSize);
- const ikp = new Uint8Array(blockSize);
- for (let i = 0; i < blockSize; i++) {
- ikp[i] = key[i] ^ 0x36;
- okp[i] = key[i] ^ 0x5c;
- }
- const b1 = new Uint8Array(blockSize + message.byteLength);
- b1.set(ikp, 0);
- b1.set(message, blockSize);
- const h0 = digest(b1);
- const b2 = new Uint8Array(blockSize + h0.length);
- b2.set(okp, 0);
- b2.set(h0, blockSize);
- return digest(b2);
+ if (key.byteLength > blockSize) {
+ key = digest(key);
+ }
+ if (key.byteLength < blockSize) {
+ const k = key;
+ key = new Uint8Array(blockSize);
+ key.set(k, 0);
+ }
+ const okp = new Uint8Array(blockSize);
+ const ikp = new Uint8Array(blockSize);
+ for (let i = 0; i < blockSize; i++) {
+ ikp[i] = key[i] ^ 0x36;
+ okp[i] = key[i] ^ 0x5c;
+ }
+ const b1 = new Uint8Array(blockSize + message.byteLength);
+ b1.set(ikp, 0);
+ b1.set(message, blockSize);
+ const h0 = digest(b1);
+ const b2 = new Uint8Array(blockSize + h0.length);
+ b2.set(okp, 0);
+ b2.set(h0, blockSize);
+ return digest(b2);
}
export function hmacSha512(key, message) {
- return hmac(sha512, 128, key, message);
+ return hmac(sha512, 128, key, message);
}
export function hmacSha256(key, message) {
- return hmac(sha256, 64, key, message);
+ return hmac(sha256, 64, key, message);
}
export function kdf(outputLength, ikm, salt, info) {
- // extract
- const prk = hmacSha512(salt, ikm);
- // 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);
+ // extract
+ const prk = hmacSha512(salt, ikm);
+ // 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);
}
- return output.slice(0, outputLength);
+ buf[buf.length - 1] = i + 1;
+ const chunk = hmacSha256(prk, buf);
+ output.set(chunk, i * 32);
+ }
+ return output.slice(0, outputLength);
}
-//# sourceMappingURL=kdf.js.map \ No newline at end of file
+//# sourceMappingURL=kdf.js.map
diff --git a/packages/taler-util/src/kdf.ts b/packages/taler-util/src/kdf.ts
index 7710de90c..8f4314340 100644
--- a/packages/taler-util/src/kdf.ts
+++ b/packages/taler-util/src/kdf.ts
@@ -58,50 +58,3 @@ export function hmacSha512(key: Uint8Array, message: Uint8Array): Uint8Array {
export function hmacSha256(key: Uint8Array, message: Uint8Array): Uint8Array {
return hmac(sha256, 64, key, message);
}
-
-/**
- * HMAC-SHA512-SHA256 (see RFC 5869).
- */
-export function kdfKw(args: {
- outputLength: number;
- ikm: Uint8Array;
- salt?: Uint8Array;
- info?: Uint8Array;
-}) {
- return kdf(args.outputLength, args.ikm, args.salt, args.info);
-}
-
-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/merchant-api-types.ts b/packages/taler-util/src/merchant-api-types.ts
new file mode 100644
index 000000000..639ae8d13
--- /dev/null
+++ b/packages/taler-util/src/merchant-api-types.ts
@@ -0,0 +1,352 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 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/>
+ */
+
+/**
+ * Test harness for various GNU Taler components.
+ * Also provides a fault-injection proxy.
+ *
+ * @author Florian Dold <dold@taler.net>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ AbsoluteTime,
+ AmountString,
+ Codec,
+ CoinPublicKeyString,
+ EddsaPublicKeyString,
+ ExchangeWireAccount,
+ FacadeCredentials,
+ MerchantContractTerms,
+ TalerProtocolDuration,
+ TalerProtocolTimestamp,
+ buildCodecForObject,
+ buildCodecForUnion,
+ codecForAmountString,
+ codecForAny,
+ codecForBoolean,
+ codecForCheckPaymentClaimedResponse,
+ codecForCheckPaymentUnpaidResponse,
+ codecForConstString,
+ codecForExchangeWireAccount,
+ codecForList,
+ codecForMerchantContractTerms,
+ codecForNumber,
+ codecForString,
+ codecForTimestamp,
+ codecOptional,
+} from "@gnu-taler/taler-util";
+
+export interface MerchantPostOrderRequest {
+ // The order must at least contain the minimal
+ // order detail, but can override all
+ order: Partial<MerchantContractTerms>;
+
+ // if set, the backend will then set the refund deadline to the current
+ // time plus the specified delay.
+ refund_delay?: TalerProtocolDuration;
+
+ // 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;
+
+ // FIXME: some fields are missing
+
+ // 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;
+}
+
+export type ClaimToken = string;
+
+export interface MerchantPostOrderResponse {
+ order_id: string;
+ token?: ClaimToken;
+}
+
+export const codecForMerchantPostOrderResponse =
+ (): Codec<MerchantPostOrderResponse> =>
+ buildCodecForObject<MerchantPostOrderResponse>()
+ .property("order_id", codecForString())
+ .property("token", codecOptional(codecForString()))
+ .build("PostOrderResponse");
+
+export const codecForMerchantRefundDetails = (): Codec<RefundDetails> =>
+ buildCodecForObject<RefundDetails>()
+ .property("reason", codecForString())
+ .property("pending", codecForBoolean())
+ .property("amount", codecForAmountString())
+ .property("timestamp", codecForTimestamp)
+ .build("PostOrderResponse");
+
+export const codecForMerchantCheckPaymentPaidResponse =
+ (): Codec<MerchantCheckPaymentPaidResponse> =>
+ buildCodecForObject<MerchantCheckPaymentPaidResponse>()
+ .property("order_status_url", codecForString())
+ .property("order_status", codecForConstString("paid"))
+ .property("refunded", codecForBoolean())
+ .property("wired", codecForBoolean())
+ .property("deposit_total", codecForAmountString())
+ .property("exchange_ec", codecForNumber())
+ .property("exchange_hc", codecForNumber())
+ .property("refund_amount", codecForAmountString())
+ .property("contract_terms", codecForMerchantContractTerms())
+ // FIXME: specify
+ .property("wire_details", codecForAny())
+ .property("wire_reports", codecForAny())
+ .property("refund_details", codecForAny())
+ .build("CheckPaymentPaidResponse");
+
+export type MerchantOrderPrivateStatusResponse =
+ | MerchantCheckPaymentPaidResponse
+ | CheckPaymentUnpaidResponse
+ | CheckPaymentClaimedResponse;
+
+export interface CheckPaymentClaimedResponse {
+ // Wallet claimed the order, but didn't pay yet.
+ order_status: "claimed";
+
+ contract_terms: MerchantContractTerms;
+}
+
+export interface MerchantCheckPaymentPaidResponse {
+ // did the customer pay for this contract
+ order_status: "paid";
+
+ // Was the payment refunded (even partially)
+ refunded: 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_ec: 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_hc: number;
+
+ // Total amount that was refunded, 0 if refunded is false.
+ refund_amount: AmountString;
+
+ // Contract terms
+ contract_terms: MerchantContractTerms;
+
+ // Ihe 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[];
+
+ order_status_url: string;
+}
+
+export interface CheckPaymentUnpaidResponse {
+ order_status: "unpaid";
+
+ // URI that the wallet must process to complete the payment.
+ taler_pay_uri: string;
+
+ order_status_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;
+
+ // 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;
+
+ // when was the refund approved
+ timestamp: TalerProtocolTimestamp;
+
+ // has not been taken yet
+ pending: boolean;
+
+ // 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: string;
+
+ // execution time of the wire transfer
+ execution_time: AbsoluteTime;
+
+ // 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_ec: number;
+
+ // HTTP status code received from the exchange.
+ exchange_hc: number;
+
+ // Public key of the coin for which we got the exchange error.
+ coin_pub: CoinPublicKeyString;
+}
+
+export interface ReserveStatusEntry {
+ // Public key of the reserve
+ reserve_pub: string;
+
+ // Timestamp when it was established
+ creation_time: AbsoluteTime;
+
+ // Timestamp when it expires
+ expiration_time: AbsoluteTime;
+
+ // Initial amount as per reserve creation call
+ merchant_initial_amount: AmountString;
+
+ // Initial amount as per exchange, 0 if exchange did
+ // not confirm reserve creation yet.
+ exchange_initial_amount: AmountString;
+
+ // Amount picked up so far.
+ pickup_amount: AmountString;
+
+ // Amount approved for tips that exceeds the pickup_amount.
+ committed_amount: AmountString;
+
+ // Is this reserve active (false if it was deleted but not purged)
+ active: boolean;
+}
+
+export interface MerchantInstancesResponse {
+ // List of instances that are present in the backend (see Instance)
+ instances: MerchantInstanceDetail[];
+}
+
+export interface MerchantInstanceDetail {
+ // Merchant name corresponding to this instance.
+ name: string;
+
+ // Merchant instance this response is about ($INSTANCE)
+ id: string;
+
+ // Public key of the merchant/instance, in Crockford Base32 encoding.
+ merchant_pub: EddsaPublicKeyString;
+
+ // 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[];
+}
+
+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;
+
+ // Additional information in a separate template.
+ template_contract: MerchantTemplateContractDetails;
+
+ // OTP device ID.
+ // This parameter is optional.
+ otp_id?: string;
+}
+
+export interface MerchantReserveCreateConfirmation {
+ // Public key identifying the reserve.
+ reserve_pub: EddsaPublicKeyString;
+
+ // Wire accounts of the exchange where to transfer the funds.
+ accounts: ExchangeWireAccount[];
+}
+
+export const codecForMerchantReserveCreateConfirmation =
+ (): Codec<MerchantReserveCreateConfirmation> =>
+ buildCodecForObject<MerchantReserveCreateConfirmation>()
+ .property("accounts", codecForList(codecForExchangeWireAccount()))
+ .property("reserve_pub", codecForString())
+ .build("MerchantReserveCreateConfirmation");
+
+export interface AccountAddDetails {
+ // payto:// URI of the account.
+ payto_uri: string;
+
+ // 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;
+}
diff --git a/packages/taler-util/src/notifications.ts b/packages/taler-util/src/notifications.ts
index b3d9ad1dc..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,263 +22,228 @@
/**
* Imports.
*/
-import { TalerErrorDetail } from "./walletTypes.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;
-}
-
-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;
-}
-
-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 f1596579f..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 };
}
@@ -37,6 +59,7 @@ export interface PaytoUriIBAN extends PaytoUriGeneric {
isKnown: true;
targetType: "iban";
iban: string;
+ bic?: string;
}
export interface PaytoUriTalerBank extends PaytoUriGeneric {
@@ -49,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
*/
@@ -63,7 +152,11 @@ export function addPaytoQueryParams(
): string {
const [acct, search] = s.slice(paytoPfx.length).split("?");
const searchParams = new URLSearchParams(search || "");
- for (const k of Object.keys(params)) {
+ const keys = Object.keys(params);
+ if (keys.length === 0) {
+ return paytoPfx + acct;
+ }
+ for (const k of keys) {
searchParams.set(k, params[k]);
}
return paytoPfx + acct + "?" + searchParams.toString();
@@ -75,15 +168,13 @@ export function addPaytoQueryParams(
* @param p
* @returns
*/
-export function stringifyPaytoUri(p: PaytoUri): string {
- const url = `${paytoPfx}${p.targetType}//${p.targetPath}`;
- if (p.params) {
- const search = Object.entries(p.params)
- .map(([key, value]) => `${key}=${value}`)
- .join("&");
- return `${url}?${search}`;
- }
- return url;
+export function stringifyPaytoUri(p: PaytoUri): PaytoString {
+ const url = new URL(`${paytoPfx}${p.targetType}/${p.targetPath}`);
+ const paramList = !p.params ? [] : Object.entries(p.params);
+ paramList.forEach(([key, value]) => {
+ url.searchParams.set(key, value);
+ });
+ return url.href as PaytoString;
}
/**
@@ -131,25 +222,42 @@ export function parsePaytoUri(s: string): PaytoUri | undefined {
};
}
if (targetType === "iban") {
+ const parts = targetPath.split("/");
+ let iban: string | undefined = undefined;
+ let bic: string | undefined = undefined;
+ if (parts.length === 1) {
+ iban = parts[0].toUpperCase();
+ }
+ if (parts.length === 2) {
+ bic = parts[0];
+ iban = parts[1].toUpperCase();
+ } else {
+ iban = targetPath.toUpperCase();
+ }
return {
isKnown: true,
targetPath,
targetType,
params,
- iban: targetPath,
+ iban,
+ bic,
};
}
if (targetType === "bitcoin") {
- const msg = /\b([A-Z0-9]{52})\b/.exec(params["message"])
+ const msg = /\b([A-Z0-9]{52})\b/.exec(params["message"]);
const reserve = !msg ? params["subject"] : msg[0];
- const segwitAddrs = !reserve ? [] : generateFakeSegwitAddress(reserve, targetPath);
+ const segwitAddrs = !reserve
+ ? []
+ : generateFakeSegwitAddress(reserve, targetPath);
+ const uppercased = targetType.toUpperCase();
const result: PaytoUriBitcoin = {
isKnown: true,
targetPath,
targetType,
+ address: uppercased,
params,
- segwitAddrs
+ segwitAddrs,
};
return result;
@@ -161,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/punycode.ts b/packages/taler-util/src/punycode.ts
new file mode 100644
index 000000000..acb8ce911
--- /dev/null
+++ b/packages/taler-util/src/punycode.ts
@@ -0,0 +1,468 @@
+/*
+Copyright Mathias Bynens <https://mathiasbynens.be/>
+Copyright (c) 2022 Taler Systems S.A.
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+
+/** Highest positive signed 32-bit float value */
+const maxInt = 2147483647; // aka. 0x7FFFFFFF or 2^31-1
+
+/** Bootstring parameters */
+const base = 36;
+const tMin = 1;
+const tMax = 26;
+const skew = 38;
+const damp = 700;
+const initialBias = 72;
+const initialN = 128; // 0x80
+const delimiter = "-"; // '\x2D'
+
+/** Regular expressions */
+const regexPunycode = /^xn--/;
+const regexNonASCII = /[^\0-\x7E]/; // non-ASCII chars
+const regexSeparators = /[\x2E\u3002\uFF0E\uFF61]/g; // RFC 3490 separators
+
+/** Error messages */
+const errors = {
+ overflow: "Overflow: input needs wider integers to process",
+ "not-basic": "Illegal input >= 0x80 (not a basic code point)",
+ "invalid-input": "Invalid input",
+} as { [x: string]: string };
+
+/** Convenience shortcuts */
+const baseMinusTMin = base - tMin;
+const floor = Math.floor;
+const stringFromCharCode = String.fromCharCode;
+
+/*--------------------------------------------------------------------------*/
+
+/**
+ * A generic error utility function.
+ * @private
+ * @param {String} type The error type.
+ * @returns {Error} Throws a `RangeError` with the applicable error message.
+ */
+function error(type: string) {
+ throw new RangeError(errors[type]);
+}
+
+/**
+ * A generic `Array#map` utility function.
+ * @private
+ * @param {Array} array The array to iterate over.
+ * @param {Function} callback The function that gets called for every array
+ * item.
+ * @returns {Array} A new array of values returned by the callback function.
+ */
+function map(array: any[], fn: (arg0: any) => any) {
+ const result = [];
+ let length = array.length;
+ while (length--) {
+ result[length] = fn(array[length]);
+ }
+ return result;
+}
+
+/**
+ * A simple `Array#map`-like wrapper to work with domain name strings or email
+ * addresses.
+ * @private
+ * @param {String} domain The domain name or email address.
+ * @param {Function} callback The function that gets called for every
+ * character.
+ * @returns {Array} A new string of characters returned by the callback
+ * function.
+ */
+function mapDomain(
+ string: string,
+ fn: { (string: any): any; (string: any): any; (arg0: any): any },
+) {
+ const parts = string.split("@");
+ let result = "";
+ if (parts.length > 1) {
+ // In email addresses, only the domain name should be punycoded. Leave
+ // the local part (i.e. everything up to `@`) intact.
+ result = parts[0] + "@";
+ string = parts[1];
+ }
+ // Avoid `split(regex)` for IE8 compatibility. See #17.
+ string = string.replace(regexSeparators, "\x2E");
+ const labels = string.split(".");
+ const encoded = map(labels, fn).join(".");
+ return result + encoded;
+}
+
+/**
+ * Creates an array containing the numeric code points of each Unicode
+ * character in the string. While JavaScript uses UCS-2 internally,
+ * this function will convert a pair of surrogate halves (each of which
+ * UCS-2 exposes as separate characters) into a single code point,
+ * matching UTF-16.
+ * @see `punycode.ucs2.encode`
+ * @see <https://mathiasbynens.be/notes/javascript-encoding>
+ * @memberOf punycode.ucs2
+ * @name decode
+ * @param {String} string The Unicode input string (UCS-2).
+ * @returns {Array} The new array of code points.
+ */
+function ucs2decode(string: string) {
+ const output = [];
+ let counter = 0;
+ const length = string.length;
+ while (counter < length) {
+ const value = string.charCodeAt(counter++);
+ if (value >= 0xd800 && value <= 0xdbff && counter < length) {
+ // It's a high surrogate, and there is a next character.
+ const extra = string.charCodeAt(counter++);
+ if ((extra & 0xfc00) == 0xdc00) {
+ // Low surrogate.
+ output.push(((value & 0x3ff) << 10) + (extra & 0x3ff) + 0x10000);
+ } else {
+ // It's an unmatched surrogate; only append this code unit, in case the
+ // next code unit is the high surrogate of a surrogate pair.
+ output.push(value);
+ counter--;
+ }
+ } else {
+ output.push(value);
+ }
+ }
+ return output;
+}
+
+/**
+ * Creates a string based on an array of numeric code points.
+ * @see `punycode.ucs2.decode`
+ * @memberOf punycode.ucs2
+ * @name encode
+ * @param {Array} codePoints The array of numeric code points.
+ * @returns {String} The new Unicode string (UCS-2).
+ */
+const ucs2encode = (array: any): string => String.fromCodePoint(...array);
+
+/**
+ * Converts a basic code point into a digit/integer.
+ * @see `digitToBasic()`
+ * @private
+ * @param {Number} codePoint The basic numeric code point value.
+ * @returns {Number} The numeric value of a basic code point (for use in
+ * representing integers) in the range `0` to `base - 1`, or `base` if
+ * the code point does not represent a value.
+ */
+const basicToDigit = function (codePoint: number) {
+ if (codePoint - 0x30 < 0x0a) {
+ return codePoint - 0x16;
+ }
+ if (codePoint - 0x41 < 0x1a) {
+ return codePoint - 0x41;
+ }
+ if (codePoint - 0x61 < 0x1a) {
+ return codePoint - 0x61;
+ }
+ return base;
+};
+
+/**
+ * Converts a digit/integer into a basic code point.
+ * @see `basicToDigit()`
+ * @private
+ * @param {Number} digit The numeric value of a basic code point.
+ * @returns {Number} The basic code point whose value (when used for
+ * representing integers) is `digit`, which needs to be in the range
+ * `0` to `base - 1`. If `flag` is non-zero, the uppercase form is
+ * used; else, the lowercase form is used. The behavior is undefined
+ * if `flag` is non-zero and `digit` has no uppercase form.
+ */
+const digitToBasic = function (digit: number, flag: number) {
+ // 0..25 map to ASCII a..z or A..Z
+ // 26..35 map to ASCII 0..9
+ return digit + 22 + 75 * Number(digit < 26) - (Number(flag != 0) << 5);
+};
+
+/**
+ * Bias adaptation function as per section 3.4 of RFC 3492.
+ * https://tools.ietf.org/html/rfc3492#section-3.4
+ * @private
+ */
+const adapt = function (delta: number, numPoints: number, firstTime: boolean) {
+ let k = 0;
+ delta = firstTime ? floor(delta / damp) : delta >> 1;
+ delta += floor(delta / numPoints);
+ for (
+ ;
+ /* no initialization */ delta > (baseMinusTMin * tMax) >> 1;
+ k += base
+ ) {
+ delta = floor(delta / baseMinusTMin);
+ }
+ return floor(k + ((baseMinusTMin + 1) * delta) / (delta + skew));
+};
+
+/**
+ * Converts a Punycode string of ASCII-only symbols to a string of Unicode
+ * symbols.
+ * @memberOf punycode
+ * @param {String} input The Punycode string of ASCII-only symbols.
+ * @returns {String} The resulting string of Unicode symbols.
+ */
+const decode = function (input: string) {
+ // Don't use UCS-2.
+ const output = [];
+ const inputLength = input.length;
+ let i = 0;
+ let n = initialN;
+ let bias = initialBias;
+
+ // Handle the basic code points: let `basic` be the number of input code
+ // points before the last delimiter, or `0` if there is none, then copy
+ // the first basic code points to the output.
+
+ let basic = input.lastIndexOf(delimiter);
+ if (basic < 0) {
+ basic = 0;
+ }
+
+ for (let j = 0; j < basic; ++j) {
+ // if it's not a basic code point
+ if (input.charCodeAt(j) >= 0x80) {
+ error("not-basic");
+ }
+ output.push(input.charCodeAt(j));
+ }
+
+ // Main decoding loop: start just after the last delimiter if any basic code
+ // points were copied; start at the beginning otherwise.
+
+ for (
+ let index = basic > 0 ? basic + 1 : 0;
+ index < inputLength /* no final expression */;
+
+ ) {
+ // `index` is the index of the next character to be consumed.
+ // Decode a generalized variable-length integer into `delta`,
+ // which gets added to `i`. The overflow checking is easier
+ // if we increase `i` as we go, then subtract off its starting
+ // value at the end to obtain `delta`.
+ let oldi = i;
+ for (let w = 1, k = base /* no condition */; ; k += base) {
+ if (index >= inputLength) {
+ error("invalid-input");
+ }
+
+ const digit = basicToDigit(input.charCodeAt(index++));
+
+ if (digit >= base || digit > floor((maxInt - i) / w)) {
+ error("overflow");
+ }
+
+ i += digit * w;
+ const t = k <= bias ? tMin : k >= bias + tMax ? tMax : k - bias;
+
+ if (digit < t) {
+ break;
+ }
+
+ const baseMinusT = base - t;
+ if (w > floor(maxInt / baseMinusT)) {
+ error("overflow");
+ }
+
+ w *= baseMinusT;
+ }
+
+ const out = output.length + 1;
+ bias = adapt(i - oldi, out, oldi == 0);
+
+ // `i` was supposed to wrap around from `out` to `0`,
+ // incrementing `n` each time, so we'll fix that now:
+ if (floor(i / out) > maxInt - n) {
+ error("overflow");
+ }
+
+ n += floor(i / out);
+ i %= out;
+
+ // Insert `n` at position `i` of the output.
+ output.splice(i++, 0, n);
+ }
+
+ return String.fromCodePoint(...output);
+};
+
+/**
+ * Converts a string of Unicode symbols (e.g. a domain name label) to a
+ * Punycode string of ASCII-only symbols.
+ * @memberOf punycode
+ * @param {String} input The string of Unicode symbols.
+ * @returns {String} The resulting Punycode string of ASCII-only symbols.
+ */
+const encode = function (inputArg: string) {
+ const output = [];
+
+ // Convert the input in UCS-2 to an array of Unicode code points.
+ let input = ucs2decode(inputArg);
+
+ // Cache the length.
+ let inputLength = input.length;
+
+ // Initialize the state.
+ let n = initialN;
+ let delta = 0;
+ let bias = initialBias;
+
+ // Handle the basic code points.
+ for (const currentValue of input) {
+ if (currentValue < 0x80) {
+ output.push(stringFromCharCode(currentValue));
+ }
+ }
+
+ let basicLength = output.length;
+ let handledCPCount = basicLength;
+
+ // `handledCPCount` is the number of code points that have been handled;
+ // `basicLength` is the number of basic code points.
+
+ // Finish the basic string with a delimiter unless it's empty.
+ if (basicLength) {
+ output.push(delimiter);
+ }
+
+ // Main encoding loop:
+ while (handledCPCount < inputLength) {
+ // All non-basic code points < n have been handled already. Find the next
+ // larger one:
+ let m = maxInt;
+ for (const currentValue of input) {
+ if (currentValue >= n && currentValue < m) {
+ m = currentValue;
+ }
+ }
+
+ // Increase `delta` enough to advance the decoder's <n,i> state to <m,0>,
+ // but guard against overflow.
+ const handledCPCountPlusOne = handledCPCount + 1;
+ if (m - n > floor((maxInt - delta) / handledCPCountPlusOne)) {
+ error("overflow");
+ }
+
+ delta += (m - n) * handledCPCountPlusOne;
+ n = m;
+
+ for (const currentValue of input) {
+ if (currentValue < n && ++delta > maxInt) {
+ error("overflow");
+ }
+ if (currentValue == n) {
+ // Represent delta as a generalized variable-length integer.
+ let q = delta;
+ for (let k = base /* no condition */; ; k += base) {
+ const t = k <= bias ? tMin : k >= bias + tMax ? tMax : k - bias;
+ if (q < t) {
+ break;
+ }
+ const qMinusT = q - t;
+ const baseMinusT = base - t;
+ output.push(
+ stringFromCharCode(digitToBasic(t + (qMinusT % baseMinusT), 0)),
+ );
+ q = floor(qMinusT / baseMinusT);
+ }
+
+ output.push(stringFromCharCode(digitToBasic(q, 0)));
+ bias = adapt(
+ delta,
+ handledCPCountPlusOne,
+ handledCPCount == basicLength,
+ );
+ delta = 0;
+ ++handledCPCount;
+ }
+ }
+
+ ++delta;
+ ++n;
+ }
+ return output.join("");
+};
+
+/**
+ * Converts a Punycode string representing a domain name or an email address
+ * to Unicode. Only the Punycoded parts of the input will be converted, i.e.
+ * it doesn't matter if you call it on a string that has already been
+ * converted to Unicode.
+ * @memberOf punycode
+ * @param {String} input The Punycoded domain name or email address to
+ * convert to Unicode.
+ * @returns {String} The Unicode representation of the given Punycode
+ * string.
+ */
+const toUnicode = function (input: string) {
+ return mapDomain(input, function (string) {
+ return regexPunycode.test(string)
+ ? decode(string.slice(4).toLowerCase())
+ : string;
+ });
+};
+
+/**
+ * Converts a Unicode string representing a domain name or an email address to
+ * Punycode. Only the non-ASCII parts of the domain name will be converted,
+ * i.e. it doesn't matter if you call it with a domain that's already in
+ * ASCII.
+ * @memberOf punycode
+ * @param {String} input The domain name or email address to convert, as a
+ * Unicode string.
+ * @returns {String} The Punycode representation of the given domain name or
+ * email address.
+ */
+const toASCII = function (input: string) {
+ return mapDomain(input, function (string) {
+ return regexNonASCII.test(string) ? "xn--" + encode(string) : string;
+ });
+};
+
+/*--------------------------------------------------------------------------*/
+
+/** Define the public API */
+export const punycode = {
+ /**
+ * A string representing the current Punycode.js version number.
+ * @memberOf punycode
+ * @type String
+ */
+ version: "2.1.0",
+ /**
+ * An object of methods to convert from JavaScript's internal character
+ * representation (UCS-2) to Unicode code points, and back.
+ * @see <https://mathiasbynens.be/notes/javascript-encoding>
+ * @memberOf punycode
+ * @type Object
+ */
+ ucs2: {
+ decode: ucs2decode,
+ encode: ucs2encode,
+ },
+ decode: decode,
+ encode: encode,
+ toASCII: toASCII,
+ toUnicode: toUnicode,
+};
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/segwit_addr.ts b/packages/taler-util/src/segwit_addr.ts
index becc5d197..fc1b6140a 100644
--- a/packages/taler-util/src/segwit_addr.ts
+++ b/packages/taler-util/src/segwit_addr.ts
@@ -18,21 +18,26 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
-import bech32 from "./bech32.js"
+import bech32 from "./bech32.js";
export default {
encode: encode,
- decode: decode
+ decode: decode,
};
-function convertbits(data: any, frombits: number, tobits: number, pad: boolean): any[] {
+function convertbits(
+ data: any,
+ frombits: number,
+ tobits: number,
+ pad: boolean,
+): any[] {
var acc = 0;
var bits = 0;
var ret = [];
var maxv = (1 << tobits) - 1;
for (var p = 0; p < data.length; ++p) {
var value = data[p];
- if (value < 0 || (value >> frombits) !== 0) {
+ if (value < 0 || value >> frombits !== 0) {
return []; //check this, was returning null
}
acc = (acc << frombits) | value;
@@ -46,7 +51,7 @@ function convertbits(data: any, frombits: number, tobits: number, pad: boolean):
if (bits > 0) {
ret.push((acc << (tobits - bits)) & maxv);
}
- } else if (bits >= frombits || ((acc << (tobits - bits)) & maxv)) {
+ } else if (bits >= frombits || (acc << (tobits - bits)) & maxv) {
return []; //check this, was returning null
}
return ret;
@@ -59,7 +64,12 @@ function decode(hrp: any, addr: string) {
dec = bech32.decode(addr, bech32.encodings.BECH32M);
bech32m = true;
}
- if (dec === null || dec.hrp !== hrp || dec.data.length < 1 || dec.data[0] > 16) {
+ if (
+ dec === null ||
+ dec.hrp !== hrp ||
+ dec.data.length < 1 ||
+ dec.data[0] > 16
+ ) {
return null;
}
var res = convertbits(dec.data.slice(1), 5, 8, false);
@@ -83,9 +93,13 @@ function encode(hrp: any, version: number, program: any): string {
if (version > 0) {
enc = bech32.encodings.BECH32M;
}
- var ret = bech32.encode(hrp, [version].concat(convertbits(program, 8, 5, true)), enc);
- if (decode(hrp, ret/*, enc*/) === null) {
+ var ret = bech32.encode(
+ hrp,
+ [version].concat(convertbits(program, 8, 5, true)),
+ enc,
+ );
+ if (decode(hrp, ret /*, enc*/) === null) {
return ""; //check this was returning null
}
return ret;
-} \ No newline at end of file
+}
diff --git a/packages/taler-util/src/sha256.ts b/packages/taler-util/src/sha256.ts
index 97723dbfc..ba8f09279 100644
--- a/packages/taler-util/src/sha256.ts
+++ b/packages/taler-util/src/sha256.ts
@@ -16,70 +16,17 @@ export const blockSize = 64;
// SHA-256 constants
const K = new Uint32Array([
- 0x428a2f98,
- 0x71374491,
- 0xb5c0fbcf,
- 0xe9b5dba5,
- 0x3956c25b,
- 0x59f111f1,
- 0x923f82a4,
- 0xab1c5ed5,
- 0xd807aa98,
- 0x12835b01,
- 0x243185be,
- 0x550c7dc3,
- 0x72be5d74,
- 0x80deb1fe,
- 0x9bdc06a7,
- 0xc19bf174,
- 0xe49b69c1,
- 0xefbe4786,
- 0x0fc19dc6,
- 0x240ca1cc,
- 0x2de92c6f,
- 0x4a7484aa,
- 0x5cb0a9dc,
- 0x76f988da,
- 0x983e5152,
- 0xa831c66d,
- 0xb00327c8,
- 0xbf597fc7,
- 0xc6e00bf3,
- 0xd5a79147,
- 0x06ca6351,
- 0x14292967,
- 0x27b70a85,
- 0x2e1b2138,
- 0x4d2c6dfc,
- 0x53380d13,
- 0x650a7354,
- 0x766a0abb,
- 0x81c2c92e,
- 0x92722c85,
- 0xa2bfe8a1,
- 0xa81a664b,
- 0xc24b8b70,
- 0xc76c51a3,
- 0xd192e819,
- 0xd6990624,
- 0xf40e3585,
- 0x106aa070,
- 0x19a4c116,
- 0x1e376c08,
- 0x2748774c,
- 0x34b0bcb5,
- 0x391c0cb3,
- 0x4ed8aa4a,
- 0x5b9cca4f,
- 0x682e6ff3,
- 0x748f82ee,
- 0x78a5636f,
- 0x84c87814,
- 0x8cc70208,
- 0x90befffa,
- 0xa4506ceb,
- 0xbef9a3f7,
- 0xc67178f2,
+ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1,
+ 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
+ 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786,
+ 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
+ 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147,
+ 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
+ 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
+ 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
+ 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a,
+ 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
+ 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
]);
function hashBlocks(
@@ -198,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/talerCrypto.test.ts b/packages/taler-util/src/taler-crypto.test.ts
index 5e8f37d80..021730c7e 100644
--- a/packages/taler-util/src/talerCrypto.test.ts
+++ b/packages/taler-util/src/taler-crypto.test.ts
@@ -21,25 +21,25 @@ import test from "ava";
import {
encodeCrock,
decodeCrock,
- ecdheGetPublic,
+ ecdhGetPublic,
eddsaGetPublic,
- keyExchangeEddsaEcdhe,
- keyExchangeEcdheEddsa,
+ keyExchangeEddsaEcdh,
+ keyExchangeEcdhEddsa,
stringToBytes,
bytesToString,
deriveBSeed,
csBlind,
csUnblind,
csVerify,
- scalarMultBase25519,
deriveSecrets,
calcRBlind,
Edx25519,
getRandomBytes,
bigintToNaclArr,
bigintFromNaclArr,
-} from "./talerCrypto.js";
-import { sha512, kdf } from "./kdf.js";
+ kdf,
+} from "./taler-crypto.js";
+import { sha512 } from "./kdf.js";
import * as nacl from "./nacl-fast.js";
import { initNodePrng } from "./prng-node.js";
@@ -50,6 +50,15 @@ import bigint from "big-integer";
import { AssertionError } from "assert";
import BigInteger from "big-integer";
+/**
+ * Used for testing, simple scalar multiplication with base point of Ed25519
+ * @param s scalar
+ * @returns new point sG
+ */
+async function scalarMultBase25519(s: Uint8Array): Promise<Uint8Array> {
+ return nacl.crypto_scalarmult_ed25519_base_noclamp(s);
+}
+
test("encoding", (t) => {
const s = "Hello, World";
const encStr = encodeCrock(stringToBytes(s));
@@ -119,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),
);
@@ -185,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),
);
@@ -374,14 +383,14 @@ test("taler age restriction crypto", async (t) => {
const priv1 = await Edx25519.keyCreate();
const pub1 = await Edx25519.getPublic(priv1);
- const seed = encodeCrock(getRandomBytes(32));
+ const seed = getRandomBytes(32);
const priv2 = await Edx25519.privateKeyDerive(priv1, seed);
const pub2 = await Edx25519.publicKeyDerive(pub1, seed);
const pub2Ref = await Edx25519.getPublic(priv2);
- t.is(pub2, pub2Ref);
+ t.deepEqual(pub2, pub2Ref);
});
test("edx signing", async (t) => {
@@ -390,21 +399,13 @@ test("edx signing", async (t) => {
const msg = stringToBytes("hello world");
- const sig = nacl.crypto_edx25519_sign_detached(
- msg,
- decodeCrock(priv1),
- decodeCrock(pub1),
- );
+ const sig = nacl.crypto_edx25519_sign_detached(msg, priv1, pub1);
- t.true(
- nacl.crypto_edx25519_sign_detached_verify(msg, sig, decodeCrock(pub1)),
- );
+ t.true(nacl.crypto_edx25519_sign_detached_verify(msg, sig, pub1));
sig[0]++;
- t.false(
- nacl.crypto_edx25519_sign_detached_verify(msg, sig, decodeCrock(pub1)),
- );
+ t.false(nacl.crypto_edx25519_sign_detached_verify(msg, sig, pub1));
});
test("edx test vector", async (t) => {
@@ -412,22 +413,28 @@ test("edx test vector", async (t) => {
const tv = {
operation: "edx25519_derive",
priv1_edx:
- "216KF1XM46K4JN8TX3Z8HNRX1DX4WRMX1BTCQM3KBS83PYKFY1GV6XRNBYRC5YM02HVDX8BDR20V7A27YX4MZJ8X8K0ADPZ43BD1GXG",
- pub1_edx: "RKGRRG74SZ8PKF8SYG5SSDY8VRCYYGY5N2AKAJCG0103Z3JK6HTG",
- seed: "EFK7CYT98YWGPNZNHPP84VJZDMXD5A41PP3E94NSAQZXRCAKVVXHAQNXG9XM2MAND2FJ56ZM238KGDCF3B0KCWNZCYKKHKDB56X6QA0",
+ "P0JAQ53G66M7TSGQTCFVFMPCBC7WHBRYDZGQXM8VD88C72NJANR07V1DQRAE7KSH92HZ3B62PJVRYFTVFTQM43K5AQD8R4A7HWJ3P7G",
+ pub1_edx: "4YZ6D5MGWTWCTKY4W931V4S5SW0XG7AD4A60J2Z9CSEB9WE05WB0",
+ seed: "SQ3YAVGNZ2GYER9VQAJB2M1Z903Y458HYXWBSF9S2A9YKF85R4DHYJX35YXXX82CBGFW2TRBCR1ZCWSQ7A87QW5SHC8WP9JH48P8KK8",
priv2_edx:
- "JRV3S06REHQV90E4HJA1FAMCVDBZZAZP9C6N2WF01MSR3CD5KM28QM7HTGGAV6MBJZ73QJ8PSZFA0D6YENJ7YT97344FDVVCGVAFNER",
- pub2_edx: "ZB546ZC7ZP16DB99AMK67WNZ67WZFPWMRY67Y4PZR9YR1D82GVZ0",
+ "GQ7NCSVNKY0QS7GQVFP2TSG6P4YN1NCK303K5TYXXBKSZ61M3R4XFZ0KA42JND6GBZRXRSJY9EX3HMMY160VQ6Y6H2NZ8H0WVQRCG1R",
+ pub2_edx: "F5X6379F0FSY87MN9210FAN84PR8KYDJQ5G5784H1N3FY12ZKAPG",
};
{
- const pub1Prime = await Edx25519.getPublic(tv.priv1_edx);
- t.is(pub1Prime, tv.pub1_edx);
+ const pub1Prime = await Edx25519.getPublic(decodeCrock(tv.priv1_edx));
+ t.deepEqual(pub1Prime, decodeCrock(tv.pub1_edx));
}
- const pub2Prime = await Edx25519.publicKeyDerive(tv.pub1_edx, tv.seed);
- t.is(pub2Prime, tv.pub2_edx);
+ const pub2Prime = await Edx25519.publicKeyDerive(
+ decodeCrock(tv.pub1_edx),
+ decodeCrock(tv.seed),
+ );
+ t.deepEqual(pub2Prime, decodeCrock(tv.pub2_edx));
- const priv2Prime = await Edx25519.privateKeyDerive(tv.priv1_edx, tv.seed);
- t.is(priv2Prime, tv.priv2_edx);
+ const priv2Prime = await Edx25519.privateKeyDerive(
+ decodeCrock(tv.priv1_edx),
+ decodeCrock(tv.seed),
+ );
+ t.deepEqual(priv2Prime, decodeCrock(tv.priv2_edx));
});
diff --git a/packages/taler-util/src/talerCrypto.ts b/packages/taler-util/src/taler-crypto.ts
index e27e329a9..e587773e2 100644
--- a/packages/taler-util/src/talerCrypto.ts
+++ b/packages/taler-util/src/taler-crypto.ts
@@ -15,29 +15,100 @@
*/
/**
- * Native implementation of GNU Taler crypto.
+ * Native implementation of GNU Taler crypto primitives.
*/
/**
* Imports.
*/
import * as nacl from "./nacl-fast.js";
-import { kdf, kdfKw } from "./kdf.js";
+import { hmacSha256, hmacSha512 } from "./kdf.js";
import bigint from "big-integer";
+import * as argon2 from "./argon2.js";
import {
- Base32String,
CoinEnvelope,
CoinPublicKeyString,
DenominationPubKey,
DenomKeyType,
HashCodeString,
-} from "./talerTypes.js";
+} 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";
+import { AmountLike, Amounts } from "./amounts.js";
+
+export type Flavor<T, FlavorT extends string> = T & {
+ _flavor?: `taler.${FlavorT}`;
+};
+
+export type FlavorP<T, FlavorT extends string, S extends number> = T & {
+ _flavor?: `taler.${FlavorT}`;
+ _size?: S;
+};
export function getRandomBytes(n: number): Uint8Array {
return nacl.randomBytes(n);
}
+export function getRandomBytesF<T extends number, N extends string>(
+ n: T,
+): FlavorP<Uint8Array, N, T> {
+ return nacl.randomBytes(n);
+}
+
+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";
class EncodingError extends Error {
@@ -52,7 +123,7 @@ function getValue(chr: string): number {
switch (chr) {
case "O":
case "o":
- a = "0;";
+ a = "0";
break;
case "i":
case "I":
@@ -82,6 +153,9 @@ function getValue(chr: string): number {
}
export function encodeCrock(data: ArrayBuffer): string {
+ if (tart) {
+ return tart.encodeCrock(data);
+ }
const dataBytes = new Uint8Array(data);
let sb = "";
const size = data.byteLength;
@@ -106,7 +180,60 @@ 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).
+ */
+export function kdfKw(args: {
+ outputLength: number;
+ ikm: Uint8Array;
+ salt?: Uint8Array;
+ info?: Uint8Array;
+}) {
+ return kdf(args.outputLength, args.ikm, args.salt, args.info);
+}
+
export function decodeCrock(encoded: string): Uint8Array {
+ if (tart) {
+ return tart.decodeCrock(encoded);
+ }
const size = encoded.length;
let bitpos = 0;
let bitbuf = 0;
@@ -134,35 +261,72 @@ 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 (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 {
- const ph = nacl.hash(eddsaPriv);
+ 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);
- return nacl.hash(x);
+ const x = nacl.scalarMult(a, ecdhPub);
+ return hash(x);
}
-export function keyExchangeEcdheEddsa(
- ecdhePriv: Uint8Array,
- eddsaPub: Uint8Array,
+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);
- return nacl.hash(x);
+ const x = nacl.scalarMult(ecdhPriv, curve25519Pub);
+ return hash(x);
}
interface RsaPub {
@@ -228,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;
@@ -236,7 +400,6 @@ let decoder: any;
export function stringToBytes(s: string): Uint8Array {
if (!encoder) {
- // @ts-ignore
encoder = new TextEncoder();
}
return encoder.encode(s);
@@ -244,7 +407,6 @@ export function stringToBytes(s: string): Uint8Array {
export function bytesToString(b: Uint8Array): string {
if (!decoder) {
- // @ts-ignore
decoder = new TextDecoder();
}
return decoder.decode(b);
@@ -324,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);
@@ -337,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);
@@ -350,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);
@@ -416,15 +587,6 @@ export function deriveSecrets(bseed: Uint8Array): CsBlindingSecrets {
}
/**
- * Used for testing, simple scalar multiplication with base point of Ed25519
- * @param s scalar
- * @returns new point sG
- */
-export async function scalarMultBase25519(s: Uint8Array): Promise<Uint8Array> {
- return nacl.crypto_scalarmult_ed25519_base_noclamp(s);
-}
-
-/**
* calculation of the blinded public point R in CS
* @param csPub denomination publik key
* @param secrets client blinding secrets
@@ -471,7 +633,7 @@ function csFDH(
const L = bigint.fromArray(lMod, 256, false);
const info = stringToBytes("Curve25519FDH");
- const preshash = nacl.hash(typedArrayConcat([rPub, hm]));
+ const preshash = hash(typedArrayConcat([rPub, hm]));
return csKdfMod(L, preshash, csPub, info).reverse();
}
@@ -531,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
@@ -559,8 +721,8 @@ export async function csUnblind(
* Verification algorithm for CS signatures
* @param hm message signed
* @param csSig unblinded signature
- * @param csPub denomination publick key
- * @returns true if valid, false if unvalid
+ * @param csPub denomination public key
+ * @returns true if valid, false if invalid
*/
export async function csVerify(
hm: Uint8Array,
@@ -597,11 +759,14 @@ 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 (tart) {
+ return tart.hash(d);
+ }
return nacl.hash(d);
}
@@ -610,7 +775,7 @@ export function hash(d: Uint8Array): Uint8Array {
* to 32 bytes.
*/
export function hashTruncate32(d: Uint8Array): Uint8Array {
- const sha512HashCode = nacl.hash(d);
+ const sha512HashCode = hash(d);
return sha512HashCode.subarray(0, 32);
}
@@ -628,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);
@@ -667,7 +832,7 @@ export function hashDenomPub(pub: DenominationPubKey): Uint8Array {
dv.setUint32(0, pub.age_mask ?? 0);
dv.setUint32(4, DenomKeyType.toIntTag(pub.cipher));
uint8ArrayBuf.set(pubBuf, 8);
- return nacl.hash(uint8ArrayBuf);
+ return hash(uint8ArrayBuf);
} else if (pub.cipher === DenomKeyType.ClauseSchnorr) {
const pubBuf = decodeCrock(pub.cs_public_key);
const hashInputBuf = new ArrayBuffer(pubBuf.length + 4 + 4);
@@ -676,16 +841,20 @@ export function hashDenomPub(pub: DenominationPubKey): Uint8Array {
dv.setUint32(0, pub.age_mask ?? 0);
dv.setUint32(4, DenomKeyType.toIntTag(pub.cipher));
uint8ArrayBuf.set(pubBuf, 8);
- return nacl.hash(uint8ArrayBuf);
+ return hash(uint8ArrayBuf);
} else {
throw Error(
- `unsupported cipher (${(pub as DenominationPubKey).cipher
+ `unsupported cipher (${
+ (pub as DenominationPubKey).cipher
}), unable to hash`,
);
}
}
export function eddsaSign(msg: Uint8Array, eddsaPriv: Uint8Array): Uint8Array {
+ if (tart) {
+ return tart.eddsaSign(msg, eddsaPriv);
+ }
const pair = nacl.crypto_sign_keyPair_fromSeed(eddsaPriv);
return nacl.sign_detached(msg, pair.secretKey);
}
@@ -695,10 +864,26 @@ export function eddsaVerify(
sig: Uint8Array,
eddsaPub: Uint8Array,
): boolean {
+ 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();
}
@@ -706,6 +891,8 @@ export interface FreshCoin {
coinPub: Uint8Array;
coinPriv: Uint8Array;
bks: Uint8Array;
+ maxAge: number;
+ ageCommitmentProof: AgeCommitmentProof | undefined;
}
export function bufferForUint32(n: number): Uint8Array {
@@ -716,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);
@@ -724,10 +926,11 @@ export function bufferForUint8(n: number): Uint8Array {
return buf;
}
-export function setupTipPlanchet(
+export async function setupTipPlanchet(
secretSeed: Uint8Array,
+ denomPub: DenominationPubKey,
coinNumber: number,
-): FreshCoin {
+): Promise<FreshCoin> {
const info = stringToBytes("taler-tip-coin-derivation");
const saltArrBuf = new ArrayBuffer(4);
const salt = new Uint8Array(saltArrBuf);
@@ -736,10 +939,20 @@ export function setupTipPlanchet(
const out = kdf(64, secretSeed, salt, info);
const coinPriv = out.slice(0, 32);
const bks = out.slice(32, 64);
+ let maybeAcp: AgeCommitmentProof | undefined;
+ if (denomPub.age_mask != 0) {
+ maybeAcp = await AgeRestriction.restrictionCommitSeeded(
+ denomPub.age_mask,
+ AgeRestriction.AGE_UNRESTRICTED,
+ secretSeed,
+ );
+ }
return {
bks,
coinPriv,
coinPub: eddsaGetPublic(coinPriv),
+ maxAge: AgeRestriction.AGE_UNRESTRICTED,
+ ageCommitmentProof: maybeAcp,
};
}
/**
@@ -762,6 +975,7 @@ export enum TalerSignaturePurpose {
MERCHANT_TRACK_TRANSACTION = 1103,
WALLET_RESERVE_WITHDRAW = 1200,
WALLET_COIN_DEPOSIT = 1201,
+ GLOBAL_FEES = 1022,
MASTER_DENOMINATION_KEY_VALIDITY = 1025,
MASTER_WIRE_FEES = 1028,
MASTER_WIRE_DETAILS = 1030,
@@ -769,21 +983,48 @@ 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,
WALLET_AGE_ATTESTATION = 1207,
+ WALLET_PURSE_CREATE = 1210,
+ WALLET_PURSE_DEPOSIT = 1211,
+ 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 enum WalletAccountMergeFlags {
+ /**
+ * Not a legal mode!
+ */
+ None = 0,
+
+ /**
+ * We are merging a fully paid-up purse into a reserve.
+ */
+ MergeFullyPaidPurse = 1,
+
+ CreateFromPurseQuota = 2,
+
+ CreateWithPurseFee = 3,
+}
+
export class SignaturePurposeBuilder {
private chunks: Uint8Array[] = [];
- constructor(private purposeNum: number) { }
+ constructor(private purposeNum: number) {}
put(bytes: Uint8Array): SignaturePurposeBuilder {
this.chunks.push(Uint8Array.from(bytes));
@@ -813,19 +1054,17 @@ export function buildSigPS(purposeNum: number): SignaturePurposeBuilder {
return new SignaturePurposeBuilder(purposeNum);
}
-export type Flavor<T, FlavorT extends string> = T & {
- _flavor?: `taler.${FlavorT}`;
-};
-
-export type FlavorP<T, FlavorT extends string, S extends number> = T & {
- _flavor?: `taler.${FlavorT}`;
- _size?: S;
-};
+export type OpaqueData = Flavor<Uint8Array, any>;
+export type Edx25519PublicKey = FlavorP<Uint8Array, "Edx25519PublicKey", 32>;
+export type Edx25519PrivateKey = FlavorP<Uint8Array, "Edx25519PrivateKey", 64>;
+export type Edx25519Signature = FlavorP<Uint8Array, "Edx25519Signature", 64>;
-export type OpaqueData = Flavor<string, "OpaqueData">;
-export type Edx25519PublicKey = FlavorP<string, "Edx25519PublicKey", 32>;
-export type Edx25519PrivateKey = FlavorP<string, "Edx25519PrivateKey", 64>;
-export type Edx25519Signature = FlavorP<string, "Edx25519Signature", 64>;
+export type Edx25519PublicKeyEnc = FlavorP<string, "Edx25519PublicKeyEnc", 32>;
+export type Edx25519PrivateKeyEnc = FlavorP<
+ string,
+ "Edx25519PrivateKeyEnc",
+ 64
+>;
/**
* Convert a big integer to a fixed-size, little-endian array.
@@ -857,19 +1096,17 @@ export namespace Edx25519 {
export async function keyCreateFromSeed(
seed: OpaqueData,
): Promise<Edx25519PrivateKey> {
- return encodeCrock(
- nacl.crypto_edx25519_private_key_create_from_seed(decodeCrock(seed)),
- );
+ return nacl.crypto_edx25519_private_key_create_from_seed(seed);
}
export async function keyCreate(): Promise<Edx25519PrivateKey> {
- return encodeCrock(nacl.crypto_edx25519_private_key_create());
+ return nacl.crypto_edx25519_private_key_create();
}
export async function getPublic(
priv: Edx25519PrivateKey,
): Promise<Edx25519PublicKey> {
- return encodeCrock(nacl.crypto_edx25519_get_public(decodeCrock(priv)));
+ return nacl.crypto_edx25519_get_public(priv);
}
export function sign(
@@ -885,12 +1122,12 @@ export namespace Edx25519 {
): Promise<OpaqueData> {
const res = kdfKw({
outputLength: 64,
- salt: decodeCrock(seed),
- ikm: decodeCrock(pub),
- info: stringToBytes("edx2559-derivation"),
+ salt: seed,
+ ikm: pub,
+ info: stringToBytes("edx25519-derivation"),
});
- return encodeCrock(res);
+ return res;
}
export async function privateKeyDerive(
@@ -898,21 +1135,17 @@ export namespace Edx25519 {
seed: OpaqueData,
): Promise<Edx25519PrivateKey> {
const pub = await getPublic(priv);
- const privDec = decodeCrock(priv);
+ const privDec = priv;
const a = bigintFromNaclArr(privDec.subarray(0, 32));
const factorEnc = await deriveFactor(pub, seed);
- const factorModL = bigintFromNaclArr(decodeCrock(factorEnc)).mod(L);
+ const factorModL = bigintFromNaclArr(factorEnc).mod(L);
const aPrime = a.divide(8).multiply(factorModL).mod(L).multiply(8).mod(L);
const bPrime = nacl
- .hash(
- typedArrayConcat([privDec.subarray(32, 64), decodeCrock(factorEnc)]),
- )
+ .hash(typedArrayConcat([privDec.subarray(32, 64), factorEnc]))
.subarray(0, 32);
- const newPriv = encodeCrock(
- typedArrayConcat([bigintToNaclArr(aPrime, 32), bPrime]),
- );
+ const newPriv = typedArrayConcat([bigintToNaclArr(aPrime, 32), bPrime]);
return newPriv;
}
@@ -922,14 +1155,9 @@ export namespace Edx25519 {
seed: OpaqueData,
): Promise<Edx25519PublicKey> {
const factorEnc = await deriveFactor(pub, seed);
- const factorReduced = nacl.crypto_core_ed25519_scalar_reduce(
- decodeCrock(factorEnc),
- );
- const res = nacl.crypto_scalarmult_ed25519_noclamp(
- factorReduced,
- decodeCrock(pub),
- );
- return encodeCrock(res);
+ const factorReduced = nacl.crypto_core_ed25519_scalar_reduce(factorEnc);
+ const res = nacl.crypto_scalarmult_ed25519_noclamp(factorReduced, pub);
+ return res;
}
}
@@ -939,7 +1167,7 @@ export interface AgeCommitment {
/**
* Public keys, one for each age group specified in the age mask.
*/
- publicKeys: Edx25519PublicKey[];
+ publicKeys: Edx25519PublicKeyEnc[];
}
export interface AgeProof {
@@ -947,7 +1175,7 @@ export interface AgeProof {
* Private keys. Typically smaller than the number of public keys,
* because we drop private keys from age groups that are restricted.
*/
- privateKeys: Edx25519PrivateKey[];
+ privateKeys: Edx25519PrivateKeyEnc[];
}
export interface AgeCommitmentProof {
@@ -962,6 +1190,11 @@ function invariant(cond: boolean): asserts cond {
}
export namespace AgeRestriction {
+ /**
+ * Smallest age value that the protocol considers "unrestricted".
+ */
+ export const AGE_UNRESTRICTED = 32;
+
export function hashCommitment(ac: AgeCommitment): HashCodeString {
const hc = new nacl.HashState();
for (const pub of ac.publicKeys) {
@@ -980,6 +1213,23 @@ export namespace AgeRestriction {
return count;
}
+ /**
+ * Get the starting points for age groups in the mask.
+ */
+ export function getAgeGroupsFromMask(mask: number): number[] {
+ const groups: number[] = [];
+ let age = 1;
+ let m = mask >> 1;
+ while (m > 0) {
+ if (m & 1) {
+ groups.push(age);
+ }
+ m = m >> 1;
+ age++;
+ }
+ return groups;
+ }
+
export function getAgeGroupIndex(mask: number, age: number): number {
invariant((mask & 1) === 1);
let i = 0;
@@ -1023,14 +1273,93 @@ export namespace AgeRestriction {
return {
commitment: {
mask: ageMask,
- publicKeys: pubs,
+ publicKeys: pubs.map((x) => encodeCrock(x)),
+ },
+ proof: {
+ privateKeys: privs.map((x) => encodeCrock(x)),
+ },
+ };
+ }
+
+ const PublishedAgeRestrictionBaseKey: Edx25519PublicKey = decodeCrock(
+ "CH0VKFDZ2GWRWHQBBGEK9MWV5YDQVJ0RXEE0KYT3NMB69F0R96TG",
+ );
+
+ export async function restrictionCommitSeeded(
+ ageMask: number,
+ age: number,
+ seed: Uint8Array,
+ ): Promise<AgeCommitmentProof> {
+ invariant((ageMask & 1) === 1);
+ const numPubs = countAgeGroups(ageMask) - 1;
+ const numPrivs = getAgeGroupIndex(ageMask, age);
+
+ const pubs: Edx25519PublicKey[] = [];
+ const privs: Edx25519PrivateKey[] = [];
+
+ for (let i = 0; i < numPrivs; i++) {
+ const privSeed = await kdfKw({
+ outputLength: 32,
+ ikm: seed,
+ info: stringToBytes("age-commitment"),
+ salt: bufferForUint32(i),
+ });
+
+ const priv = await Edx25519.keyCreateFromSeed(privSeed);
+ const pub = await Edx25519.getPublic(priv);
+ pubs.push(pub);
+ 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 {
+ commitment: {
+ mask: ageMask,
+ publicKeys: pubs.map((x) => encodeCrock(x)),
},
proof: {
- privateKeys: privs,
+ privateKeys: privs.map((x) => encodeCrock(x)),
},
};
}
+ /**
+ * Check that c1 = c2*salt
+ */
+ export async function commitCompare(
+ c1: AgeCommitment,
+ c2: AgeCommitment,
+ salt: OpaqueData,
+ ): Promise<boolean> {
+ if (c1.publicKeys.length != c2.publicKeys.length) {
+ return false;
+ }
+ for (let i = 0; i < c1.publicKeys.length; i++) {
+ const k1 = decodeCrock(c1.publicKeys[i]);
+ const k2 = await Edx25519.publicKeyDerive(
+ decodeCrock(c2.publicKeys[i]),
+ salt,
+ );
+ if (k1 != k2) {
+ return false;
+ }
+ }
+ return true;
+ }
+
export async function commitmentDerive(
commitmentProof: AgeCommitmentProof,
salt: OpaqueData,
@@ -1039,20 +1368,22 @@ export namespace AgeRestriction {
const newPubs: Edx25519PublicKey[] = [];
for (const oldPub of commitmentProof.commitment.publicKeys) {
- newPubs.push(await Edx25519.publicKeyDerive(oldPub, salt));
+ newPubs.push(await Edx25519.publicKeyDerive(decodeCrock(oldPub), salt));
}
for (const oldPriv of commitmentProof.proof.privateKeys) {
- newPrivs.push(await Edx25519.privateKeyDerive(oldPriv, salt));
+ newPrivs.push(
+ await Edx25519.privateKeyDerive(decodeCrock(oldPriv), salt),
+ );
}
return {
commitment: {
mask: commitmentProof.commitment.mask,
- publicKeys: newPubs,
+ publicKeys: newPubs.map((x) => encodeCrock(x)),
},
proof: {
- privateKeys: newPrivs,
+ privateKeys: newPrivs.map((x) => encodeCrock(x)),
},
};
}
@@ -1068,7 +1399,7 @@ export namespace AgeRestriction {
const group = getAgeGroupIndex(commitmentProof.commitment.mask, age);
if (group === 0) {
// No attestation required.
- return encodeCrock(new Uint8Array(64));
+ return new Uint8Array(64);
}
const priv = commitmentProof.proof.privateKeys[group - 1];
const pub = commitmentProof.commitment.publicKeys[group - 1];
@@ -1077,13 +1408,255 @@ export namespace AgeRestriction {
decodeCrock(priv),
decodeCrock(pub),
);
- return encodeCrock(sig);
+ return sig;
}
export function commitmentVerify(
- commitmentProof: AgeCommitmentProof,
+ commitment: AgeCommitment,
+ sig: string,
age: number,
- ): Edx25519Signature {
- throw Error("not implemented");
+ ): boolean {
+ const d = buildSigPS(TalerSignaturePurpose.WALLET_AGE_ATTESTATION)
+ .put(bufferForUint32(commitment.mask))
+ .put(bufferForUint32(age))
+ .build();
+ const group = getAgeGroupIndex(commitment.mask, age);
+ if (group === 0) {
+ // No attestation required.
+ return true;
+ }
+ const pub = commitment.publicKeys[group - 1];
+ return nacl.crypto_edx25519_sign_detached_verify(
+ d,
+ decodeCrock(sig),
+ decodeCrock(pub),
+ );
+ }
+}
+
+// FIXME: make it a branded type!
+export type EncryptionNonce = FlavorP<Uint8Array, "EncryptionNonce", 24>;
+
+async function deriveKey(
+ keySeed: OpaqueData,
+ nonce: EncryptionNonce,
+ salt: string,
+): Promise<Uint8Array> {
+ return kdfKw({
+ outputLength: 32,
+ salt: nonce,
+ ikm: keySeed,
+ info: stringToBytes(salt),
+ });
+}
+
+export async function encryptWithDerivedKey(
+ nonce: EncryptionNonce,
+ keySeed: OpaqueData,
+ plaintext: OpaqueData,
+ salt: string,
+): Promise<OpaqueData> {
+ const key = await deriveKey(keySeed, nonce, salt);
+ const cipherText = secretbox(plaintext, nonce, key);
+ return typedArrayConcat([nonce, cipherText]);
+}
+
+const nonceSize = 24;
+
+export async function decryptWithDerivedKey(
+ ciphertext: OpaqueData,
+ keySeed: OpaqueData,
+ salt: string,
+): Promise<OpaqueData> {
+ const ctBuf = ciphertext;
+ const nonceBuf = ctBuf.slice(0, nonceSize);
+ const enc = ctBuf.slice(nonceSize);
+ const key = await deriveKey(keySeed, nonceBuf, salt);
+ const clearText = nacl.secretbox_open(enc, nonceBuf, key);
+ if (!clearText) {
+ throw Error("could not decrypt");
+ }
+ return clearText;
+}
+
+enum ContractFormatTag {
+ PaymentOffer = 0,
+ PaymentRequest = 1,
+}
+
+type MaterialEddsaPub = {
+ _materialType?: "eddsa-pub";
+ _size?: 32;
+};
+
+type MaterialEddsaPriv = {
+ _materialType?: "ecdhe-priv";
+ _size?: 32;
+};
+
+type MaterialEcdhePub = {
+ _materialType?: "ecdhe-pub";
+ _size?: 32;
+};
+
+type MaterialEcdhePriv = {
+ _materialType?: "ecdhe-priv";
+ _size?: 32;
+};
+
+type PursePublicKey = FlavorP<Uint8Array, "PursePublicKey", 32> &
+ MaterialEddsaPub;
+
+type ContractPrivateKey = FlavorP<Uint8Array, "ContractPrivateKey", 32> &
+ MaterialEcdhePriv;
+
+type MergePrivateKey = FlavorP<Uint8Array, "MergePrivateKey", 32> &
+ MaterialEddsaPriv;
+
+const mergeSalt = "p2p-merge-contract";
+const depositSalt = "p2p-deposit-contract";
+
+export function encryptContractForMerge(
+ pursePub: PursePublicKey,
+ contractPriv: ContractPrivateKey,
+ mergePriv: MergePrivateKey,
+ contractTerms: any,
+ nonce: EncryptionNonce,
+): Promise<OpaqueData> {
+ const contractTermsCanon = canonicalJson(contractTerms) + "\0";
+ const contractTermsBytes = stringToBytes(contractTermsCanon);
+ const contractTermsCompressed = fflate.zlibSync(contractTermsBytes);
+ const data = typedArrayConcat([
+ bufferForUint32(ContractFormatTag.PaymentOffer),
+ bufferForUint32(contractTermsBytes.length),
+ mergePriv,
+ contractTermsCompressed,
+ ]);
+ 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);
+ const contractTermsCompressed = fflate.zlibSync(contractTermsBytes);
+ const data = typedArrayConcat([
+ bufferForUint32(ContractFormatTag.PaymentRequest),
+ bufferForUint32(contractTermsBytes.length),
+ contractTermsCompressed,
+ ]);
+ const key = keyExchangeEcdhEddsa(contractPriv, pursePub);
+ return encryptWithDerivedKey(nonce, key, data, depositSalt);
+}
+
+export interface DecryptForMergeResult {
+ contractTerms: any;
+ mergePriv: Uint8Array;
+}
+
+export interface DecryptForDepositResult {
+ contractTerms: any;
+}
+
+export async function decryptContractForMerge(
+ enc: OpaqueData,
+ pursePub: PursePublicKey,
+ contractPriv: ContractPrivateKey,
+): Promise<DecryptForMergeResult> {
+ 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);
+ const contractTermsBuf = fflate.unzlibSync(contractTermsCompressed);
+ // Slice of the '\0' at the end and decode to a string
+ const contractTermsString = bytesToString(
+ contractTermsBuf.slice(0, contractTermsBuf.length - 1),
+ );
+ return {
+ mergePriv: mergePriv,
+ contractTerms: JSON.parse(contractTermsString),
+ };
+}
+
+export async function decryptContractForDeposit(
+ enc: OpaqueData,
+ pursePub: PursePublicKey,
+ contractPriv: ContractPrivateKey,
+): Promise<DecryptForDepositResult> {
+ const key = keyExchangeEcdhEddsa(contractPriv, pursePub);
+ const dec = await decryptWithDerivedKey(enc, key, depositSalt);
+ const contractTermsCompressed = dec.slice(8);
+ const contractTermsBuf = fflate.unzlibSync(contractTermsCompressed);
+ // Slice of the '\0' at the end and decode to a string
+ const contractTermsString = bytesToString(
+ contractTermsBuf.slice(0, contractTermsBuf.length - 1),
+ );
+ return {
+ 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 83ac7c1bd..9985e74b3 100644
--- a/packages/taler-util/src/taler-error-codes.ts
+++ b/packages/taler-util/src/taler-error-codes.ts
@@ -31,230 +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 HTTP method used is invalid for this endpoint.
+ * 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. 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 currencies involved in the operation do not match.
+ * 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 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 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 failed initialize its connection to the database.
+ * 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. 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 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).
@@ -262,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).
@@ -269,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).
@@ -276,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).
@@ -283,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).
@@ -290,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).
@@ -297,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).
@@ -304,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).
@@ -311,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).
@@ -318,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).
@@ -325,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).
@@ -332,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).
@@ -339,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).
@@ -346,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).
@@ -353,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).
@@ -360,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).
@@ -367,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).
@@ -374,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).
@@ -381,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).
@@ -388,12 +519,14 @@ export enum TalerErrorCode {
*/
EXCHANGE_GENERIC_NEW_DENOMS_ARRAY_SIZE_EXCESSIVE = 1018,
+
/**
- * The reserve public key was malformed.
- * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * 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_RESERVE_PUB_MALFORMED = 1019,
+ 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.
@@ -402,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).
@@ -409,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).
@@ -416,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).
@@ -423,6 +559,119 @@ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_GENERIC_PURSE_PUB_MALFORMED = 1024,
+
+
+ /**
+ * The purse 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).
+ */
+ EXCHANGE_GENERIC_PURSE_UNKNOWN = 1025,
+
+
+ /**
+ * The purse 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).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
@@ -430,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).
@@ -437,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).
@@ -444,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).
@@ -451,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).
@@ -458,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).
@@ -465,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).
@@ -472,6 +727,15 @@ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
@@ -479,12 +743,14 @@ export enum TalerErrorCode {
*/
EXCHANGE_WITHDRAW_INSUFFICIENT_FUNDS = 1150,
+
/**
- * The exchange has no information about the "reserve_pub" that was given.
- * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * 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_WITHDRAW_RESERVE_UNKNOWN = 1151,
+ 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.
@@ -493,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).
@@ -500,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).
@@ -507,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.
@@ -521,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).
@@ -528,6 +807,63 @@ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
@@ -535,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).
@@ -542,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).
@@ -549,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).
@@ -556,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).
@@ -563,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).
@@ -570,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).
@@ -577,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).
@@ -584,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).
@@ -591,26 +935,30 @@ export enum TalerErrorCode {
*/
EXCHANGE_DEPOSIT_FEE_ABOVE_AMOUNT = 1222,
+
/**
- * 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).
+ * The proof of policy fulfillment was 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).
*/
- EXCHANGE_RESERVES_STATUS_UNKNOWN = 1250,
+ EXCHANGE_EXTENSIONS_INVALID_FULFILLMENT = 1240,
+
/**
- * 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.
@@ -619,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).
@@ -626,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).
@@ -633,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).
@@ -640,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).
@@ -647,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).
@@ -654,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).
@@ -661,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).
@@ -668,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).
@@ -675,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).
@@ -682,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).
@@ -689,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).
@@ -696,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).
@@ -703,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).
@@ -710,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).
@@ -717,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).
@@ -724,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).
@@ -731,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).
@@ -738,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).
@@ -745,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).
@@ -752,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).
@@ -759,6 +1127,23 @@ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
@@ -766,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).
@@ -773,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).
@@ -780,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).
@@ -787,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).
@@ -794,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).
@@ -801,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).
@@ -808,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).
@@ -815,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).
@@ -822,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).
@@ -829,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).
@@ -836,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).
@@ -843,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).
@@ -850,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).
@@ -857,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).
@@ -864,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).
@@ -871,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).
@@ -878,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).
@@ -885,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).
@@ -892,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).
@@ -899,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).
@@ -906,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).
@@ -913,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).
@@ -920,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).
@@ -927,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).
@@ -934,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).
@@ -941,13 +1351,71 @@ export enum TalerErrorCode {
*/
EXCHANGE_WIRE_FEES_NOT_CONFIGURED = 1653,
+
/**
- * 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).
+ * This purse was previously created with different meta data.
+ * 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_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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_RESERVES_PURSE_FEE_TOO_LOW = 1678,
+
+
+ /**
+ * 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).
@@ -955,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).
@@ -962,13 +1431,23 @@ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
@@ -976,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).
@@ -983,6 +1463,79 @@ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
@@ -990,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).
@@ -997,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).
@@ -1004,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).
@@ -1011,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).
@@ -1018,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).
@@ -1025,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).
@@ -1032,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).
@@ -1039,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).
@@ -1046,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).
@@ -1053,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).
@@ -1060,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).
@@ -1067,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).
@@ -1074,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).
@@ -1081,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).
@@ -1088,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).
@@ -1095,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).
@@ -1102,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).
@@ -1109,6 +1679,63 @@ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
@@ -1116,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).
@@ -1123,13 +1751,15 @@ 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_BAD_REQUEST (400).
+ * 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_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).
@@ -1137,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).
@@ -1144,20 +1775,23 @@ 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_BAD_REQUEST (400).
+ * 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_CREATE_SIGNATURE_INVALID = 1855,
+
/**
* The signature over the encrypted contract is invalid.
- * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * 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_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).
@@ -1165,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).
@@ -1172,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).
@@ -1179,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).
@@ -1186,27 +1823,23 @@ export enum TalerErrorCode {
*/
EXCHANGE_CREATE_PURSE_NEGATIVE_VALUE_AFTER_FEE = 1860,
- /**
- * The purse to be merged is not known.
- * 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_MERGE_PURSE_NOT_FOUND = 1875,
/**
* The signature using the merge key is invalid.
- * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * 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_MERGE_INVALID_MERGE_SIGNATURE = 1876,
+
/**
* The signature using the reserve key is invalid.
- * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * 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_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).
@@ -1214,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).
@@ -1221,19 +1855,30 @@ 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_BAD_REQUEST (400).
+ * 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_MERGE_PURSE_PARTNER_UNKNOWN = 1880,
+
/**
- * The amount in the purse is lower than the wad fee. So the request was accepted, but no transfer is expected to take place. FIXME-DOLD: good HTTP status. Suggestion: no error, make variant of 200 OK.
- * Returned with an HTTP status code of #MHD_HTTP_ACCEPTED (202).
+ * 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_PURSE_MERGE_WAD_FEE_EXCEEDS_PURSE_VALUE = 1881,
+ EXCHANGE_MANAGEMENT_ADD_PARTNER_DATA_CONFLICT = 1891,
+
/**
* The auditor signature over the denomination meta data is invalid.
@@ -1242,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).
@@ -1249,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).
@@ -1256,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).
@@ -1263,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).
@@ -1270,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).
@@ -1277,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).
@@ -1284,13 +1935,87 @@ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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_UNAUTHORIZED (401).
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
* (A value of 0 indicates that the error is generated client-side).
*/
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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
@@ -1298,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).
@@ -1305,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).
@@ -1312,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).
@@ -1319,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).
@@ -1326,40 +2055,54 @@ export enum TalerErrorCode {
*/
EXCHANGE_CONTRACTS_DECODING_FAILED = 1954,
+
/**
- * The backend could not find the merchant instance specified in the request.
- * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * A coin signature for a deposit into the purse 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).
*/
- MERCHANT_GENERIC_INSTANCE_UNKNOWN = 2000,
+ EXCHANGE_PURSE_DEPOSIT_COIN_SIGNATURE_INVALID = 1975,
+
/**
- * The start and end-times in the wire fee structure leave a hole. This is not allowed.
+ * 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).
*/
- MERCHANT_GENERIC_HOLE_IN_WIRE_FEE_STRUCTURE = 2001,
+ EXCHANGE_TOTP_KEY_INVALID = 1980,
+
/**
- * The reserve key of given to a /reserves/ handler was malformed.
- * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * The backend could not find the merchant instance specified in the request.
+ * 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_RESERVE_PUB_MALFORMED = 2002,
+ MERCHANT_GENERIC_INSTANCE_UNKNOWN = 2000,
+
/**
- * 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 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).
* (A value of 0 indicates that the error is generated client-side).
*/
- MERCHANT_GENERIC_FAILED_TO_LOAD_TEMPLATE = 2003,
+ MERCHANT_GENERIC_HOLE_IN_WIRE_FEE_STRUCTURE = 2001,
+
/**
- * The backend could not expand the template to generate an HTML reply.
- * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * 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_FAILED_TO_EXPAND_TEMPLATE = 2004,
+ MERCHANT_GENERIC_EXCHANGE_WIRE_REQUEST_FAILED = 2002,
+
/**
* The proposal is not known to the backend.
@@ -1368,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).
@@ -1375,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.
@@ -1389,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).
@@ -1396,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).
@@ -1403,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).
@@ -1410,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).
@@ -1417,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).
@@ -1424,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).
@@ -1431,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).
@@ -1438,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).
@@ -1445,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).
@@ -1452,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).
@@ -1459,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).
@@ -1466,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).
@@ -1473,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).
@@ -1480,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).
@@ -1487,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).
@@ -1494,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).
@@ -1501,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).
@@ -1508,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).
@@ -1515,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).
@@ -1522,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).
@@ -1543,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).
@@ -1550,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).
@@ -1557,6 +2383,15 @@ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
@@ -1564,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).
@@ -1571,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).
@@ -1578,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).
@@ -1585,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).
@@ -1592,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).
@@ -1599,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).
@@ -1606,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).
@@ -1613,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).
@@ -1620,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).
@@ -1627,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).
@@ -1634,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).
@@ -1641,6 +2487,79 @@ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
@@ -1648,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).
@@ -1655,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).
@@ -1662,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).
@@ -1669,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).
@@ -1676,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).
@@ -1683,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).
@@ -1690,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).
@@ -1697,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).
@@ -1704,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).
@@ -1711,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).
@@ -1718,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).
@@ -1725,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).
@@ -1823,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).
@@ -1830,34 +2871,55 @@ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
@@ -1865,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).
@@ -1872,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).
@@ -1879,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).
@@ -1886,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).
@@ -1893,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).
@@ -1900,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).
@@ -1907,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).
@@ -1914,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).
@@ -1921,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).
@@ -1928,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).
@@ -1935,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).
@@ -1942,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).
@@ -1949,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).
@@ -1956,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).
@@ -1963,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).
@@ -1970,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).
@@ -1977,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).
@@ -1984,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).
@@ -1991,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).
@@ -1998,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).
@@ -2005,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).
@@ -2012,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.
@@ -2047,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).
@@ -2054,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).
@@ -2061,83 +3247,103 @@ 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_UNINITIALIZED (0).
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
* (A value of 0 indicates that the error is generated client-side).
*/
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_UNINITIALIZED (0).
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
* (A value of 0 indicates that the error is generated client-side).
*/
BANK_UNALLOWED_DEBIT = 5102,
+
/**
- * Negative number was used (as value and/or fraction) to initiate a Amount object.
- * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * 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).
* (A value of 0 indicates that the error is generated client-side).
*/
BANK_NEGATIVE_NUMBER_AMOUNT = 5103,
+
/**
- * A number too big was used (as value and/or fraction) to initiate a amount object.
- * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * 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).
* (A value of 0 indicates that the error is generated client-side).
*/
BANK_NUMBER_TOO_BIG = 5104,
- /**
- * Could not login for the requested operation.
- * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
- * (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. Returned along "400 Not found".
- * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * 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).
*/
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_UNINITIALIZED (0).
+ * 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_TRANSACTION_NOT_FOUND = 5107,
+
/**
* Bank received a malformed amount string.
- * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
* (A value of 0 indicates that the error is generated client-side).
*/
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. To be returned along HTTP 403 Forbidden.
- * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * 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).
* (A value of 0 indicates that the error is generated client-side).
*/
BANK_REJECT_NO_RIGHTS = 5109,
+
/**
- * This error code is returned when no known exception types captured the exception, and comes along with a 500 Internal Server Error.
- * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * 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).
* (A value of 0 indicates that the error is generated client-side).
*/
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, but need to signal the middleware that the bank is not responding with 500 Internal Server Error. Used for example when a client is trying to register with a unavailable username.
- * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * 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).
* (A value of 0 indicates that the error is generated client-side).
*/
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).
@@ -2145,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).
@@ -2152,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).
@@ -2159,6 +3367,271 @@ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
@@ -2166,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).
@@ -2173,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).
@@ -2180,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).
@@ -2187,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).
@@ -2194,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).
@@ -2201,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).
@@ -2222,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).
@@ -2229,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).
@@ -2236,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).
@@ -2243,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).
@@ -2250,6 +3735,23 @@ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
@@ -2257,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).
@@ -2264,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).
@@ -2271,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).
@@ -2278,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).
@@ -2285,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).
@@ -2292,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).
@@ -2299,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).
@@ -2306,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).
@@ -2313,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).
@@ -2320,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).
@@ -2327,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).
@@ -2334,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).
@@ -2341,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).
@@ -2348,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).
@@ -2355,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).
@@ -2362,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.
@@ -2376,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).
@@ -2383,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).
@@ -2390,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).
@@ -2397,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).
@@ -2404,6 +3927,127 @@ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_PAY_MERCHANT_SERVER_ERROR = 7022,
+
+
+ /**
+ * The crypto worker failed.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_CRYPTO_WORKER_ERROR = 7023,
+
+
+ /**
+ * The crypto worker received a bad request.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
@@ -2411,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).
@@ -2418,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).
@@ -2425,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).
@@ -2432,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).
@@ -2439,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).
@@ -2446,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).
@@ -2453,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).
@@ -2460,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).
@@ -2467,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).
@@ -2474,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).
@@ -2481,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).
@@ -2488,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).
@@ -2495,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).
@@ -2502,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).
@@ -2509,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).
@@ -2516,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).
@@ -2523,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).
@@ -2530,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).
@@ -2537,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).
@@ -2544,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).
@@ -2551,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).
@@ -2558,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).
@@ -2565,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).
@@ -2572,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).
@@ -2579,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).
@@ -2586,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).
@@ -2593,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).
@@ -2600,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).
@@ -2607,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).
@@ -2614,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).
@@ -2621,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).
@@ -2628,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).
@@ -2635,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).
@@ -2642,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).
@@ -2649,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).
@@ -2656,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).
@@ -2663,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).
@@ -2670,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).
@@ -2677,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).
@@ -2684,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).
@@ -2698,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).
@@ -2705,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).
@@ -2712,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).
@@ -2719,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).
@@ -2726,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).
@@ -2733,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).
@@ -2740,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).
@@ -2747,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).
@@ -2754,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).
@@ -2761,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).
@@ -2768,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).
@@ -2775,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).
@@ -2782,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).
@@ -2789,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).
@@ -2796,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).
@@ -2803,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).
@@ -2810,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).
@@ -2817,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).
@@ -2824,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).
@@ -2831,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).
@@ -2838,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).
@@ -2845,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).
@@ -2852,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).
@@ -2859,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).
@@ -2866,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).
@@ -2873,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).
@@ -2880,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).
@@ -2887,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).
@@ -2894,6 +4639,103 @@ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
@@ -2901,4 +4743,5 @@ export enum TalerErrorCode {
*/
END = 9999,
+
}
diff --git a/packages/taler-util/src/talerTypes.ts b/packages/taler-util/src/taler-types.ts
index b21c6caec..392e7149c 100644
--- a/packages/taler-util/src/talerTypes.ts
+++ b/packages/taler-util/src/taler-types.ts
@@ -25,29 +25,35 @@
* Imports.
*/
+import { Amounts, codecForAmountString } from "./amounts.js";
import {
+ Codec,
buildCodecForObject,
- codecForString,
- codecForList,
- codecOptional,
+ buildCodecForUnion,
codecForAny,
- codecForNumber,
codecForBoolean,
- codecForMap,
- Codec,
- codecForConstNumber,
- buildCodecForUnion,
codecForConstString,
+ codecForList,
+ codecForMap,
+ codecForNumber,
+ codecForString,
+ codecForStringURL,
+ codecOptional,
} from "./codec.js";
+import { strcmp } from "./helpers.js";
+import {
+ CurrencySpecification,
+ codecForCurrencySpecificiation,
+ codecForEither,
+ codecForProduct,
+} from "./index.js";
+import { Edx25519PublicKeyEnc } from "./taler-crypto.js";
import {
- codecForTimestamp,
- codecForDuration,
- TalerProtocolTimestamp,
TalerProtocolDuration,
+ TalerProtocolTimestamp,
+ codecForDuration,
+ codecForTimestamp,
} from "./time.js";
-import { codecForAmountString } from "./amounts.js";
-import { strcmp } from "./helpers.js";
-import { AgeCommitmentProof, Edx25519PublicKey } from "./talerCrypto.js";
/**
* Denomination as found in the /keys response from the exchange.
@@ -287,7 +293,9 @@ export interface CoinDepositPermission {
minimum_age_sig?: EddsaSignatureString;
- age_commitment?: Edx25519PublicKey[];
+ age_commitment?: Edx25519PublicKeyEnc[];
+
+ h_age_commitment?: string;
}
/**
@@ -295,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 {
@@ -315,7 +319,7 @@ export interface AuditorHandle {
/**
* Master public signing key of the auditor.
*/
- auditor_pub: string;
+ auditor_pub: EddsaPublicKeyString;
/**
* Base URL of the auditor.
@@ -359,9 +363,24 @@ export interface Location {
}
export interface MerchantInfo {
+ // The merchant's legal name of business.
name: string;
- jurisdiction?: Location;
+
+ // 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 {
@@ -380,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;
@@ -392,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[];
@@ -407,149 +426,150 @@ export interface InternationalizedString {
/**
* Contract terms from a merchant.
+ * FIXME: Add type field!
*/
-export interface ContractTerms {
- /**
- * Hash of the merchant's wire details.
- */
+export interface MerchantContractTerms {
+ // 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;
}
/**
@@ -604,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 {
@@ -718,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.
@@ -754,8 +755,158 @@ export class ExchangeKeysJson {
version: string;
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 {
+ // What date (inclusive) does these fees go into effect?
+ start_date: TalerProtocolTimestamp;
+
+ // What date (exclusive) does this fees stop going into effect?
+ end_date: TalerProtocolTimestamp;
+
+ // 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: TalerProtocolDuration;
+
+ // Non-negative number of concurrent purses that any
+ // account holder is allowed to create without having
+ // to pay the purse_fee.
+ purse_account_limit: number;
+
+ // 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: TalerProtocolDuration;
+
+ // Signature of TALER_GlobalFeesPS.
+ master_sig: string;
+}
/**
* Wire fees as announced by the exchange.
*/
@@ -765,8 +916,6 @@ export class WireFeesJson {
*/
wire_fee: string;
- wad_fee: string;
-
/**
* Cost of clising a reserve.
*/
@@ -788,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.
*/
@@ -831,6 +970,8 @@ export class CheckPaymentResponse {
* Response from the bank.
*/
export class WithdrawOperationStatusResponse {
+ status: "selected" | "aborted" | "confirmed" | "pending";
+
selection_done: boolean;
transfer_done: boolean;
@@ -851,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;
}
@@ -888,82 +1031,29 @@ export type BlindedDenominationSignature =
| RsaBlindedDenominationSignature
| CSBlindedDenominationSignature;
-export const codecForBlindedDenominationSignature = () =>
- buildCodecForUnion<BlindedDenominationSignature>()
- .discriminateOn("cipher")
- .alternative(DenomKeyType.Rsa, codecForRsaBlindedDenominationSignature())
- .build("BlindedDenominationSignature");
-
export const codecForRsaBlindedDenominationSignature = () =>
buildCodecForObject<RsaBlindedDenominationSignature>()
.property("cipher", codecForConstString(DenomKeyType.Rsa))
.property("blinded_rsa_signature", codecForString())
.build("RsaBlindedDenominationSignature");
-export class WithdrawResponse {
- ev_sig: BlindedDenominationSignature;
-}
+export const codecForBlindedDenominationSignature = () =>
+ buildCodecForUnion<BlindedDenominationSignature>()
+ .discriminateOn("cipher")
+ .alternative(DenomKeyType.Rsa, codecForRsaBlindedDenominationSignature())
+ .build("BlindedDenominationSignature");
-export class WithdrawBatchResponse {
- ev_sigs: WithdrawResponse[];
+export class ExchangeWithdrawResponse {
+ ev_sig: BlindedDenominationSignature;
}
-/**
- * Easy to process format for the public data of coins
- * managed by the wallet.
- */
-export interface CoinDumpJson {
- coins: Array<{
- /**
- * The coin's denomination's public key.
- */
- denom_pub: DenominationPubKey;
- /**
- * Hash of denom_pub.
- */
- denom_pub_hash: string;
- /**
- * Value of the denomination (without any fees).
- */
- denom_value: string;
- /**
- * Public key of the coin.
- */
- coin_pub: string;
- /**
- * Base URL of the exchange for the coin.
- */
- exchange_base_url: string;
- /**
- * Remaining value on the coin, to the knowledge of
- * the wallet.
- */
- remaining_value: string;
- /**
- * Public key of the parent coin.
- * Only present if this coin was obtained via refreshing.
- */
- refresh_parent_coin_pub: string | undefined;
- /**
- * Public key of the reserve for this coin.
- * Only present if this coin was obtained via refreshing.
- */
- withdrawal_reserve_pub: string | undefined;
- /**
- * Is the coin suspended?
- * Suspended coins are not considered for payments.
- */
- coin_suspended: boolean;
-
- /**
- * Information about the age restriction
- */
- ageCommitmentProof: AgeCommitmentProof | undefined;
- }>;
+export class ExchangeWithdrawBatchResponse {
+ ev_sigs: ExchangeWithdrawResponse[];
}
export interface MerchantPayResponse {
sig: string;
+ pos_confirmation?: string;
}
export interface ExchangeMeltRequest {
@@ -1024,15 +1114,17 @@ export interface ExchangeRevealResponse {
}
interface MerchantOrderStatusPaid {
- /**
- * Was the payment refunded (even partially, via refund or abort)?
- */
+ // Was the payment refunded (even partially, via refund or abort)?
refunded: boolean;
- /**
- * Amount that was refunded in total.
- */
+ // 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;
}
interface MerchantOrderRefundResponse {
@@ -1131,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 {
@@ -1183,13 +1308,6 @@ export namespace DenominationPubKey {
}
}
-export const codecForDenominationPubKey = () =>
- buildCodecForUnion<DenominationPubKey>()
- .discriminateOn("cipher")
- .alternative(DenomKeyType.Rsa, codecForRsaDenominationPubKey())
- .alternative(DenomKeyType.ClauseSchnorr, codecForCsDenominationPubKey())
- .build("DenominationPubKey");
-
export const codecForRsaDenominationPubKey = () =>
buildCodecForObject<RsaDenominationPubKey>()
.property("cipher", codecForConstString(DenomKeyType.Rsa))
@@ -1204,16 +1322,20 @@ export const codecForCsDenominationPubKey = () =>
.property("age_mask", codecForNumber())
.build("CsDenominationPubKey");
-export const codecForBankWithdrawalOperationPostResponse =
- (): Codec<BankWithdrawalOperationPostResponse> =>
- buildCodecForObject<BankWithdrawalOperationPostResponse>()
- .property("transfer_done", codecForBoolean())
- .build("BankWithdrawalOperationPostResponse");
+export const codecForDenominationPubKey = () =>
+ buildCodecForUnion<DenominationPubKey>()
+ .discriminateOn("cipher")
+ .alternative(DenomKeyType.Rsa, codecForRsaDenominationPubKey())
+ .alternative(DenomKeyType.ClauseSchnorr, codecForCsDenominationPubKey())
+ .build("DenominationPubKey");
-export type AmountString = string;
+declare const __amount_str: unique symbol;
+export type AmountString = string & { [__amount_str]: true };
+// 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> =>
@@ -1278,30 +1400,11 @@ 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 codecForContractTerms = (): Codec<ContractTerms> =>
- buildCodecForObject<ContractTerms>()
+export const codecForMerchantContractTerms = (): Codec<MerchantContractTerms> =>
+ buildCodecForObject<MerchantContractTerms>()
.property("order_id", codecForString())
.property("fulfillment_url", codecOptional(codecForString()))
.property("fulfillment_message", codecOptional(codecForString()))
@@ -1316,23 +1419,28 @@ export const codecForContractTerms = (): Codec<ContractTerms> =>
.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()))
.property("products", codecOptional(codecForList(codecForProduct())))
.property("extra", codecForAny())
.property("minimum_age", codecOptional(codecForNumber()))
- .build("ContractTerms");
+ .build("MerchantContractTerms");
+
+export const codecForPeerContractTerms = (): Codec<PeerContractTerms> =>
+ buildCodecForObject<PeerContractTerms>()
+ .property("summary", codecForString())
+ .property("amount", codecForAmountString())
+ .property("purse_expiration", codecForTimestamp)
+ .build("PeerContractTerms");
export const codecForMerchantRefundPermission =
(): Codec<MerchantAbortPayRefundDetails> =>
@@ -1348,14 +1456,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())
@@ -1380,9 +1480,26 @@ export const codecForExchangeSigningKey = (): Codec<ExchangeSignKeyJson> =>
.property("stamp_expire", codecForTimestamp)
.build("ExchangeSignKeyJson");
+export const codecForGlobalFees = (): Codec<GlobalFees> =>
+ buildCodecForObject<GlobalFees>()
+ .property("start_date", codecForTimestamp)
+ .property("end_date", codecForTimestamp)
+ .property("history_fee", codecForAmountString())
+ .property("account_fee", codecForAmountString())
+ .property("purse_fee", codecForAmountString())
+ .property("history_expiration", codecForDuration)
+ .property("purse_account_limit", codecForNumber())
+ .property("purse_timeout", codecForDuration)
+ .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)
@@ -1390,30 +1507,21 @@ export const codecForExchangeKeysJson = (): Codec<ExchangeKeysJson> =>
.property("signkeys", codecForList(codecForExchangeSigningKey()))
.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> =>
buildCodecForObject<WireFeesJson>()
.property("wire_fee", codecForString())
.property("closing_fee", codecForString())
- .property("wad_fee", codecForString())
.property("sig", codecForString())
.property("start_date", codecForTimestamp)
.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())
@@ -1433,6 +1541,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())
@@ -1443,12 +1560,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>()
@@ -1456,19 +1575,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> =>
@@ -1490,55 +1611,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_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>()
@@ -1576,11 +1657,6 @@ export interface AbortResponse {
refunds: MerchantAbortPayRefundStatus[];
}
-export const codecForAbortResponse = (): Codec<AbortResponse> =>
- buildCodecForObject<AbortResponse>()
- .property("refunds", codecForList(codecForMerchantAbortPayRefundStatus()))
- .build("AbortResponse");
-
export type MerchantAbortPayRefundStatus =
| MerchantAbortPayRefundSuccessStatus
| MerchantAbortPayRefundFailureStatus;
@@ -1622,45 +1698,6 @@ export interface MerchantAbortPayRefundSuccessStatus {
exchange_pub: string;
}
-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 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[];
@@ -1731,6 +1768,10 @@ export interface ExchangeWithdrawRequest {
coin_ev: CoinEnvelope;
}
+export interface ExchangeBatchWithdrawRequest {
+ planchets: ExchangeWithdrawRequest[];
+}
+
export interface ExchangeRefreshRevealRequest {
new_denoms_h: HashCodeString[];
coin_evs: CoinEnvelope[];
@@ -1748,42 +1789,629 @@ export interface ExchangeRefreshRevealRequest {
* the client MUST provide the original age commitment, i.e. the vector
* of public keys.
*/
- old_age_commitment?: Edx25519PublicKey[];
+ 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 {
+ /**
+ * Amount to be deposited, can be a fraction of the
+ * coin's total value.
+ */
+ amount: AmountString;
+
+ /**
+ * 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;
+
+ /**
+ * Age commitment for the coin, if the denomination is age-restricted.
+ */
+ age_commitment?: string[];
+
+ /**
+ * Attestation for the minimum age, if the denomination is age-restricted.
+ */
+ attest?: string;
+
+ /**
+ * Signature over TALER_PurseDepositSignaturePS
+ * of purpose TALER_SIGNATURE_WALLET_PURSE_DEPOSIT
+ * made by the customer with the
+ * coin's private key.
+ */
+ coin_sig: EddsaSignatureString;
+
+ /**
+ * Public key of the coin being deposited into the purse.
+ */
+ coin_pub: EddsaPublicKeyString;
+}
+
+export interface ExchangePurseMergeRequest {
+ // payto://-URI of the account the purse is to be merged into.
+ // Must be of the form: 'payto://taler/$EXCHANGE_URL/$RESERVE_PUB'.
+ payto_uri: string;
+
+ // EdDSA signature of the account/reserve affirming the merge
+ // over a TALER_AccountMergeSignaturePS.
+ // Must be of purpose TALER_SIGNATURE_ACCOUNT_MERGE
+ reserve_sig: EddsaSignatureString;
+
+ // EdDSA signature of the purse private key affirming the merge
+ // over a TALER_PurseMergeSignaturePS.
+ // Must be of purpose TALER_SIGNATURE_PURSE_MERGE.
+ merge_sig: EddsaSignatureString;
+
+ // Client-side timestamp of when the merge request was made.
+ merge_timestamp: TalerProtocolTimestamp;
+}
+
+export interface ExchangeGetContractResponse {
+ purse_pub: string;
+ econtract_sig: string;
+ econtract: string;
+}
+
+export const codecForExchangeGetContractResponse =
+ (): Codec<ExchangeGetContractResponse> =>
+ buildCodecForObject<ExchangeGetContractResponse>()
+ .property("purse_pub", codecForString())
+ .property("econtract_sig", codecForString())
+ .property("econtract", codecForString())
+ .build("ExchangeGetContractResponse");
+
+/**
+ * Contract terms between two wallets (as opposed to a merchant and wallet).
+ */
+export interface PeerContractTerms {
+ amount: AmountString;
+ summary: string;
+ purse_expiration: TalerProtocolTimestamp;
+}
+
+export interface EncryptedContract {
+ // Encrypted contract.
+ econtract: string;
+
+ // Signature over the (encrypted) contract.
+ econtract_sig: string;
+
+ // Ephemeral public key for the DH operation to decrypt the encrypted contract.
+ contract_pub: string;
+}
+
+/**
+ * Payload for /reserves/{reserve_pub}/purse
+ * endpoint of the exchange.
+ */
+export interface ExchangeReservePurseRequest {
+ /**
+ * Minimum amount that must be credited to the reserve, that is
+ * the total value of the purse minus the deposit fees.
+ * If the deposit fees are lower, the contribution to the
+ * reserve can be higher!
+ */
+ purse_value: AmountString;
+
+ // Minimum age required for all coins deposited into the purse.
+ min_age: number;
+
+ // Purse fee the reserve owner is willing to pay
+ // for the purse creation. Optional, if not present
+ // the purse is to be created from the purse quota
+ // of the reserve.
+ purse_fee: AmountString;
+
+ // Optional encrypted contract, in case the buyer is
+ // proposing the contract and thus establishing the
+ // purse with the payment.
+ econtract?: EncryptedContract;
+
+ // EdDSA public key used to approve merges of this purse.
+ merge_pub: EddsaPublicKeyString;
+
+ // EdDSA signature of the purse private key affirming the merge
+ // over a TALER_PurseMergeSignaturePS.
+ // Must be of purpose TALER_SIGNATURE_PURSE_MERGE.
+ merge_sig: EddsaSignatureString;
+
+ // EdDSA signature of the account/reserve affirming the merge.
+ // Must be of purpose TALER_SIGNATURE_WALLET_ACCOUNT_MERGE
+ reserve_sig: EddsaSignatureString;
+
+ // Purse public key.
+ purse_pub: EddsaPublicKeyString;
+
+ // EdDSA signature of the purse over
+ // TALER_PurseRequestSignaturePS of
+ // purpose TALER_SIGNATURE_PURSE_REQUEST
+ // confirming that the
+ // above details hold for this purse.
+ purse_sig: EddsaSignatureString;
+
+ // SHA-512 hash of the contact of the purse.
+ h_contract_terms: HashCodeString;
+
+ // Client-side timestamp of when the merge request was made.
+ merge_timestamp: TalerProtocolTimestamp;
+
+ // Indicative time by which the purse should expire
+ // if it has not been paid.
+ purse_expiration: TalerProtocolTimestamp;
+}
+
+export interface ExchangePurseDeposits {
+ // Array of coins to deposit into the purse.
+ 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.
+ contribution: AmountString;
+
+ // The merchant's account details.
+ // In case of an auction policy, it refers to the seller.
+ 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: string;
+
+ // SHA-512 hash of the contract of the merchant with the customer. Further
+ // details are never disclosed to the exchange.
+ h_contract_terms: HashCodeString;
+
+ // 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;
+
+ // 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, so that the client can identify the
+ // merchant for refund requests.
+ //
+ // THIS FIELD WILL BE DEPRECATED, once the refund mechanism becomes a
+ // policy via extension.
+ 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 deposit.
+ // This might be a refund, auction or escrow policy.
+ //
+ // Note that support for policies is an optional feature of the exchange.
+ // Optional features are so called "extensions" in Taler. The exchange
+ // provides the list of supported extensions, including policies, in the
+ // ExtensionsManifestsResponse response to the /keys endpoint.
+ policy?: any;
+
+ // Signature over TALER_DepositRequestPS, made by the customer with the
+ // coin's private key.
+ coin_sig: EddsaSignatureString;
+
+ 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 37aace10a..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,53 +23,14 @@
/**
* Imports
*/
-import { AmountJson } from "./amounts.js";
-import { Amounts } from "./amounts.js";
-
-const nodejs_fs = (function () {
- let fs: typeof import("fs");
- return function () {
- if (!fs) {
- /**
- * need to use an expression when doing a require if we want
- * webpack not to find out about the requirement
- */
- const _r = "require";
- fs = module[_r]("fs");
- }
- return fs;
- };
-})();
-
-const nodejs_path = (function () {
- let path: typeof import("path");
- return function () {
- if (!path) {
- /**
- * need to use an expression when doing a require if we want
- * webpack not to find out about the requirement
- */
- const _r = "require";
- path = module[_r]("path");
- }
- return path;
- };
-})();
-
-const nodejs_os = (function () {
- let os: typeof import("os");
- return function () {
- if (!os) {
- /**
- * need to use an expression when doing a require if we want
- * webpack not to find out about the requirement
- */
- const _r = "require";
- os = module[_r]("os");
- }
- return os;
- };
-})();
+import { AmountJson, Amounts } from "./amounts.js";
+import { Logger } from "./logging.js";
+
+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) {
@@ -80,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 {
@@ -94,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,
) {}
@@ -130,6 +159,10 @@ export class ConfigValue<T> {
isDefined(): boolean {
return this.value !== undefined;
}
+
+ getValue(): string | undefined {
+ return this.value;
+ }
}
/**
@@ -138,10 +171,10 @@ export class ConfigValue<T> {
*/
export function expandPath(path: string): string {
if (path[0] === "~") {
- path = nodejs_path().join(nodejs_os().homedir(), path.slice(1));
+ path = nodejs_path.join(nodejs_os.homedir(), path.slice(1));
}
if (path[0] !== "/") {
- path = nodejs_path().join(process.cwd(), path);
+ path = nodejs_path.join(process.cwd(), path);
}
return path;
}
@@ -157,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;
@@ -198,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;
}
@@ -215,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;
}
@@ -229,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 {
@@ -288,15 +322,15 @@ function normalizeInlineFilename(parentFile: string, f: string): string {
if (f[0] === "/") {
return f;
}
- const resolvedParentDir = nodejs_path().dirname(
- nodejs_fs().realpathSync(parentFile),
+ const resolvedParentDir = nodejs_path.dirname(
+ nodejs_fs.realpathSync(parentFile),
);
- return nodejs_path().join(resolvedParentDir, f);
+ return nodejs_path.join(resolvedParentDir, f);
}
/**
* Crude implementation of the which(1) shell command.
- *
+ *
* Tries to locate the location of an executable based on the
* "PATH" environment variable.
*/
@@ -306,8 +340,8 @@ function which(name: string): string | undefined {
return undefined;
}
for (const path of paths) {
- const filename = nodejs_path().join(path, name);
- if (nodejs_fs().existsSync(filename)) {
+ const filename = nodejs_path.join(path, name);
+ if (nodejs_fs.existsSync(filename)) {
return filename;
}
}
@@ -323,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 = () => {
@@ -342,7 +388,7 @@ export class Configuration {
checkCycle();
- const s = nodejs_fs().readFileSync(filename, "utf-8");
+ const s = nodejs_fs.readFileSync(filename, "utf-8");
this.loadedFiles.push({
filename: filename,
level: this.nestLevel,
@@ -350,7 +396,7 @@ export class Configuration {
const oldNestLevel = this.nestLevel;
this.nestLevel += 1;
try {
- this.loadFromString(s, {
+ this.internalLoadFromString(s, isDefaultSource, {
...opts,
filename: filename,
});
@@ -359,43 +405,51 @@ export class Configuration {
}
}
- private loadGlob(parentFilename: string, fileglob: string): void {
- const resolvedParent = nodejs_fs().realpathSync(parentFilename);
- const parentDir = nodejs_path().dirname(resolvedParent);
+ private loadGlob(
+ parentFilename: string,
+ isDefaultSource: boolean,
+ fileglob: string,
+ ): void {
+ const resolvedParent = nodejs_fs.realpathSync(parentFilename);
+ const parentDir = nodejs_path.dirname(resolvedParent);
let fullFileglob: string;
if (fileglob.startsWith("/")) {
fullFileglob = fileglob;
} else {
- fullFileglob = nodejs_path().join(parentDir, fileglob);
+ fullFileglob = nodejs_path.join(parentDir, fileglob);
}
fullFileglob = expandPath(fullFileglob);
- const head = nodejs_path().dirname(fullFileglob);
- const tail = nodejs_path().basename(fullFileglob);
+ const head = nodejs_path.dirname(fullFileglob);
+ const tail = nodejs_path.basename(fullFileglob);
- const files = nodejs_fs().readdirSync(head);
+ const files = nodejs_fs.readdirSync(head);
for (const f of files) {
if (globMatch(tail, f)) {
- const fullPath = nodejs_path().join(head, f);
- this.loadFromFilename(fullPath);
+ const fullPath = nodejs_path.join(head, f);
+ 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();
try {
- nodejs_fs().accessSync(filename, nodejs_fs().constants.R_OK);
+ nodejs_fs.accessSync(filename, nodejs_fs.constants.R_OK);
} catch (err) {
sec.inaccessible = true;
return;
}
- otherCfg.loadFromFilename(filename, {
+ otherCfg.loadFromFilename(filename, isDefaultSource, {
banDirectives: true,
});
const otherSec = otherCfg.provideSection(sectionName);
@@ -404,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*#.*$/;
@@ -431,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": {
@@ -440,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": {
@@ -460,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": {
@@ -470,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:
@@ -503,6 +567,9 @@ export class Configuration {
value: val,
sourceFile: opts.filename ?? "<unknown>",
sourceLine: lineNo,
+ origin: isDefaultSource
+ ? EntryOrigin.DefaultFile
+ : EntryOrigin.Loaded,
};
continue;
}
@@ -537,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,
};
}
@@ -604,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;
}
@@ -619,71 +707,146 @@ export class Configuration {
);
}
- loadFrom(dirname: string): void {
- const files = nodejs_fs().readdirSync(dirname);
+ 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);
+ const fn = nodejs_path.join(dirname, f);
+ 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)) {
+ if (fn && nodejs_fs.existsSync(fn)) {
return fn;
}
- const etc1 = "/etc/taler.conf";
- if (nodejs_fs().existsSync(etc1)) {
+ const etc1 = `/etc/${componentName}.conf`;
+ if (nodejs_fs.existsSync(etc1)) {
return etc1;
}
- const etc2 = "/etc/taler/taler.conf";
- if (nodejs_fs().existsSync(etc2)) {
+ 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";
@@ -698,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 5bf7ad4ee..7f10d21fd 100644
--- a/packages/taler-util/src/taleruri.test.ts
+++ b/packages/taler-util/src/taleruri.test.ts
@@ -15,23 +15,67 @@
*/
import test from "ava";
+import { AmountString } from "./taler-types.js";
import {
+ parseAddExchangeUri,
+ parseDevExperimentUri,
+ parsePayPullUri,
+ parsePayPushUri,
+ parsePayTemplateUri,
parsePayUri,
- parseWithdrawUri,
parseRefundUri,
- parseTipUri,
+ 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);
@@ -75,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);
@@ -107,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) => {
@@ -151,34 +210,358 @@ 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 (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;
+ }
+ t.is(r1.exchangeBaseUrl, "https://exch.example.com/");
+ t.is(r1.contractPriv, "foo");
+});
+
+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;
+ }
+ t.is(r1.exchangeBaseUrl, "https://exch.example.com:123/bla/");
+ t.is(r1.contractPriv, "foo");
+});
+
+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;
+ }
+ t.is(r1.exchangeBaseUrl, "http://exch.example.com:123/bla/");
+ t.is(r1.contractPriv, "foo");
+});
+
+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-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?amount=KUDOS:5";
+ const r1 = parsePayTemplateUri(url1);
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.deepEqual(r1.merchantBaseUrl, "https://merchant.example.com/");
+ t.deepEqual(r1.templateId, "FEGHYJY48FEGU6WETYIOIDEDE2QW3OCZVY");
+ t.deepEqual(r1.templateParams.amount, "KUDOS:5");
+});
+
+test("taler pay template URI (parsing, http with port)", (t) => {
+ const url1 =
+ "taler+http://pay-template/merchant.example.com:1234/FEGHYJY48FEGU6WETYIOIDEDE2QW3OCZVY?amount=KUDOS:5";
+ const r1 = parsePayTemplateUri(url1);
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.deepEqual(r1.merchantBaseUrl, "http://merchant.example.com:1234/");
+ t.deepEqual(r1.templateId, "FEGHYJY48FEGU6WETYIOIDEDE2QW3OCZVY");
+ t.deepEqual(r1.templateParams.amount, "KUDOS:5");
+});
+
+test("taler pay template URI (stringify)", (t) => {
+ const url1 = stringifyPayTemplateUri({
+ merchantBaseUrl: "http://merchant.example.com:1234/",
+ templateId: "FEGHYJY48FEGU6WETYIOIDEDE2QW3OCZVY",
+ templateParams: {
+ amount: "KUDOS:5",
+ },
+ });
+ t.deepEqual(
+ url1,
+ "taler+http://pay-template/merchant.example.com:1234/FEGHYJY48FEGU6WETYIOIDEDE2QW3OCZVY?amount=KUDOS%3A5",
+ );
+});
+
+/**
+ * 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) => {
+ {
+ const r1 = parseWithdrawExchangeUri(
+ "taler://withdraw-exchange/exchange.demo.taler.net/someroot/GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0?a=KUDOS%3A2",
+ );
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.deepEqual(
+ r1.exchangePub,
+ "GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0",
+ );
+ t.deepEqual(
+ r1.exchangeBaseUrl,
+ "https://exchange.demo.taler.net/someroot/",
+ );
+ t.deepEqual(r1.amount, "KUDOS:2");
+ }
+ {
+ const r2 = parseWithdrawExchangeUri(
+ "taler://withdraw-exchange/exchange.demo.taler.net/someroot/",
+ );
+ if (!r2) {
+ t.fail();
+ return;
+ }
+ t.deepEqual(r2.exchangePub, undefined);
+ 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.exchangePub, undefined);
+ 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.exchangePub, undefined);
+ 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",
+ exchangePub: "GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0",
+ });
+ t.deepEqual(
+ url,
+ "taler://withdraw-exchange/exchange.demo.taler.net/GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0",
+ );
+});
+
+test("taler withdraw exchange URI with amount (stringify)", (t) => {
+ const url = stringifyWithdrawExchange({
+ exchangeBaseUrl: "https://exchange.demo.taler.net",
+ exchangePub: "GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0",
+ amount: "KUDOS:19" as AmountString,
+ });
+ t.deepEqual(
+ url,
+ "taler://withdraw-exchange/exchange.demo.taler.net/GJKG23V4ZBHEH45YRK7TWQE8ZTY7JWTY5094TQJSRZN5DSDBX8E0?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 b487c73ae..b4f9db6ef 100644
--- a/packages/taler-util/src/taleruri.ts
+++ b/packages/taler-util/src/taleruri.ts
@@ -14,45 +14,140 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
+/**
+ * @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 { URLSearchParams } 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;
+ templateParams: TemplateParams;
}
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;
+ exchangePub?: 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();
@@ -67,54 +162,101 @@ 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",
- TalerNotifyReserve = "taler-notify-reserve",
+ TalerPayPush = "taler-pay-push",
+ TalerPayPull = "taler-pay-pull",
+ TalerRecovery = "taler-recovery",
+ TalerDevExperiment = "taler-dev-experiment",
Unknown = "unknown",
}
-/**
- * Classify a taler:// URI.
- */
-export function classifyTalerUri(s: string): TalerUriType {
- const sl = s.toLowerCase();
- 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://notify-reserve/")) {
- return TalerUriType.TalerNotifyReserve;
- }
- 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 {
@@ -143,6 +285,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.
@@ -168,37 +399,128 @@ export function parsePayUri(s: string): PayUriResult | undefined {
const merchantBaseUrl = canonicalizeBaseUrl(`${pi.innerProto}://${p}/`);
return {
+ type: TalerUriAction.Pay,
merchantBaseUrl,
orderId,
- sessionId: sessionId,
+ sessionId,
claimToken,
noncePriv,
};
}
-/**
- * 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 parsePayTemplateUri(
+ uriString: string,
+): PayTemplateUriResult | undefined {
+ const pi = parseProtoInfo(uriString, TalerUriAction.PayTemplate);
if (!pi) {
return undefined;
}
- const c = pi?.rest.split("?");
+ 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 tipId = parts[parts.length - 1];
+ const templateId = parts[parts.length - 1];
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 merchantBaseUrl = canonicalizeBaseUrl(
+ `${pi.innerProto}://${hostAndSegments}/`,
+ );
return {
+ type: TalerUriAction.PayTemplate,
merchantBaseUrl,
- merchantTipId: tipId,
+ templateId,
+ templateParams: params,
+ };
+}
+
+export function parsePayPushUri(s: string): PayPushUriResult | undefined {
+ const pi = parseProtoInfo(s, TalerUriAction.PayPush);
+ if (!pi) {
+ return undefined;
+ }
+ const c = pi?.rest.split("?");
+ const parts = c[0].split("/");
+ if (parts.length < 2) {
+ return undefined;
+ }
+ const host = parts[0].toLowerCase();
+ const contractPriv = parts[parts.length - 1];
+ const pathSegments = parts.slice(1, parts.length - 1);
+ 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, TalerUriAction.PayPull);
+ if (!pi) {
+ return undefined;
+ }
+ const c = pi?.rest.split("?");
+ const parts = c[0].split("/");
+ if (parts.length < 2) {
+ return undefined;
+ }
+ const host = parts[0].toLowerCase();
+ const contractPriv = parts[parts.length - 1];
+ const pathSegments = parts.slice(1, parts.length - 1);
+ const hostAndSegments = [host, ...pathSegments].join("/");
+ const exchangeBaseUrl = canonicalizeBaseUrl(
+ `${pi.innerProto}://${hostAndSegments}/`,
+ );
+
+ return {
+ type: TalerUriAction.PayPull,
+ exchangeBaseUrl,
+ contractPriv,
+ };
+}
+
+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 < 1) {
+ return undefined;
+ }
+ const host = parts[0].toLowerCase();
+ const exchangePub = parts.length > 1 ? parts[parts.length - 1] : undefined;
+ const pathSegments = parts.slice(1, parts.length - 1);
+ 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 {
+ type: TalerUriAction.WithdrawExchange,
+ exchangeBaseUrl,
+ exchangePub: exchangePub != "" ? exchangePub : undefined,
+ amount,
};
}
@@ -220,11 +542,190 @@ 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,
};
}
+
+export function parseDevExperimentUri(s: string): DevExperimentUri | undefined {
+ const pi = parseProtoInfo(s, "dev-experiment");
+ const c = pi?.rest.split("?");
+ if (!c) {
+ return undefined;
+ }
+ const parts = c[0].split("/");
+ return {
+ type: TalerUriAction.DevExperiment,
+ devExperimentId: parts[0],
+ };
+}
+
+export function parseRestoreUri(uri: string): BackupRestoreUri | undefined {
+ const pi = parseProtoInfo(uri, "restore");
+ if (!pi) {
+ return undefined;
+ }
+ const c = pi.rest.split("?");
+ const parts = c[0].split("/");
+ if (parts.length < 2) {
+ return undefined;
+ }
+
+ 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,
+ };
+}
+
+// ================================================
+// 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,
+ exchangePub,
+ amount,
+}: Omit<WithdrawExchangeUri, "type">): string {
+ const { proto, path, query } = getUrlInfo(exchangeBaseUrl, {
+ a: amount,
+ });
+ return `${proto}://withdraw-exchange/${path}${exchangePub ?? ""}${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,
+ templateParams,
+}: Omit<PayTemplateUriResult, "type">): string {
+ const { proto, path, query } = getUrlInfo(merchantBaseUrl, templateParams);
+ 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 URL protocol in ${baseUrl}`);
+ }
+ let path = url.hostname;
+ if (url.port) {
+ path = path + ":" + url.port;
+ }
+ if (url.pathname) {
+ path = path + url.pathname;
+ }
+ if (!path.endsWith("/")) {
+ path = path + "/";
+ }
+
+ 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 f12e8d32b..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,37 @@ 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,
+ ): TalerProtocolTimestamp {
+ if (t1.t_s === "never") {
+ return { t_s: t2.t_s };
+ }
+ if (t2.t_s === "never") {
+ 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 {
@@ -69,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(),
@@ -91,19 +194,118 @@ 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);
+ }
+
+ export function min(d1: Duration, d2: Duration): Duration {
+ return durationMin(d1, d2);
+ }
+
+ export function multiply(d1: Duration, n: number): Duration {
+ return durationMul(d1, n);
+ }
+
export function toIntegerYears(d: Duration): number {
if (typeof d.d_ms !== "number") {
throw Error("infinite 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" };
}
+
export function getZero(): Duration {
return { d_ms: 0 };
}
+
export function fromTalerProtocolDuration(
d: TalerProtocolDuration,
): Duration {
@@ -113,9 +315,10 @@ export namespace Duration {
};
}
return {
- d_ms: d.d_us / 1000,
+ d_ms: Math.floor(d.d_us / 1000),
};
}
+
export function toTalerProtocolDuration(d: Duration): TalerProtocolDuration {
if (d.d_ms === "forever") {
return {
@@ -126,18 +329,49 @@ export namespace Duration {
d_us: d.d_ms * 1000,
};
}
+
+ export function fromMilliseconds(ms: number): Duration {
+ return {
+ d_ms: ms,
+ };
+ }
+
+ export function clamp(args: {
+ lower: Duration;
+ upper: Duration;
+ value: Duration;
+ }): Duration {
+ return durationMax(durationMin(args.value, args.upper), args.lower);
+ }
}
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,
};
}
@@ -162,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 {
@@ -194,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 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 toTimestamp(at: AbsoluteTime): TalerProtocolTimestamp {
+ 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" };
}
@@ -236,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();
+ }
+ const stampNow = now();
+ if (stampNow.t_ms === "never") {
+ throw Error("invariant violated");
}
- return { t_ms: t1.t_ms + d.d_ms };
+ return Duration.fromMilliseconds(Math.max(0, t1.t_ms - stampNow.t_ms));
}
export function subtractDuraction(
@@ -246,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 {
@@ -269,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 };
@@ -321,9 +602,29 @@ export function durationAdd(d1: Duration, d2: Duration): Duration {
return { d_ms: d1.d_ms + d2.d_ms };
}
+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", [opaque_AbsoluteTime]: true };
+ }
+ } else if (typeof t_ms === "number") {
+ return { t_ms, [opaque_AbsoluteTime]: true };
+ }
+ throw Error(`expected timestamp at ${renderContext(c)}`);
+ },
+};
+
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") {
@@ -342,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
new file mode 100644
index 000000000..cee3de9fa
--- /dev/null
+++ b/packages/taler-util/src/transactions-types.ts
@@ -0,0 +1,795 @@
+/*
+ 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/>
+ */
+
+/**
+ * Type and schema definitions for the wallet's transaction list.
+ *
+ * @author Florian Dold
+ * @author Torsten Grote
+ */
+
+/**
+ * Imports.
+ */
+import {
+ Codec,
+ buildCodecForObject,
+ codecForAny,
+ codecForBoolean,
+ codecForConstString,
+ codecForEither,
+ codecForList,
+ codecForString,
+ codecOptional,
+} from "./codec.js";
+import {
+ AmountString,
+ InternationalizedString,
+ MerchantInfo,
+ codecForInternationalizedString,
+ codecForMerchantInfo,
+} from "./taler-types.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 {
+ // a list of past and pending transactions sorted by pending, timestamp and transactionId.
+ // In case two events are both pending and have the same timestamp,
+ // they are sorted by the transactionId
+ // (lexically ascending and locale-independent comparison).
+ transactions: Transaction[];
+}
+
+export interface TransactionCommon {
+ // opaque unique ID for the transaction, used as a starting point for paginating queries
+ // and for invoking actions on the transaction (e.g. deleting/hiding it from the history)
+ transactionId: TransactionIdStr;
+
+ // the type of the transaction; different types might provide additional information
+ type: TransactionType;
+
+ // main timestamp of the transaction
+ timestamp: TalerPreciseTimestamp;
+
+ /**
+ * Transaction state, as per DD37.
+ */
+ txState: TransactionState;
+
+ /**
+ * Possible transitions based on the current state.
+ */
+ txActions: TransactionAction[];
+
+ /**
+ * Raw amount of the transaction (exclusive of fees or other extra costs).
+ */
+ amountRaw: AmountString;
+
+ /**
+ * Amount added or removed from the wallet's balance (including all fees and other costs).
+ */
+ 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
+ | TransactionRefresh
+ | TransactionDeposit
+ | TransactionPeerPullCredit
+ | TransactionPeerPullDebit
+ | TransactionPeerPushCredit
+ | TransactionPeerPushDebit
+ | TransactionInternalWithdrawal
+ | TransactionRecoup
+ | TransactionDenomLoss;
+
+export enum TransactionType {
+ Withdrawal = "withdrawal",
+ InternalWithdrawal = "internal-withdrawal",
+ Payment = "payment",
+ Refund = "refund",
+ Refresh = "refresh",
+ 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 {
+ TalerBankIntegrationApi = "taler-bank-integration-api",
+ ManualTransfer = "manual-transfer",
+}
+
+export type WithdrawalDetails =
+ | WithdrawalDetailsForManualTransfer
+ | WithdrawalDetailsForTalerBankIntegrationApi;
+
+interface WithdrawalDetailsForManualTransfer {
+ type: WithdrawalType.ManualTransfer;
+
+ /**
+ * 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 {
+ type: WithdrawalType.TalerBankIntegrationApi;
+
+ /**
+ * Set to true if the bank has confirmed the withdrawal, false if not.
+ * An unconfirmed withdrawal usually requires user-input and should be highlighted in the UI.
+ * See also bankConfirmationUrl below.
+ */
+ confirmed: boolean;
+
+ /**
+ * If the withdrawal is unconfirmed, this can include a URL for user
+ * initiated confirmation.
+ */
+ bankConfirmationUrl?: string;
+
+ // 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;
+}
+
+/**
+ * A withdrawal transaction (either bank-integrated or manual).
+ */
+export interface TransactionWithdrawal extends TransactionCommon {
+ type: TransactionType.Withdrawal;
+
+ /**
+ * 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;
+}
+
+/**
+ * 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;
+}
+
+/**
+ * Credit because we were paid for a P2P invoice we created.
+ */
+export interface TransactionPeerPullCredit extends TransactionCommon {
+ type: TransactionType.PeerPullCredit;
+
+ info: PeerInfoShort;
+ /**
+ * Exchange used.
+ */
+ 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;
+
+ /**
+ * URI to send to the other party.
+ *
+ * Only available in the right state.
+ */
+ talerUri: string | undefined;
+}
+
+/**
+ * Debit because we paid someone's invoice.
+ */
+export interface TransactionPeerPullDebit extends TransactionCommon {
+ type: TransactionType.PeerPullDebit;
+
+ info: PeerInfoShort;
+ /**
+ * Exchange used.
+ */
+ exchangeBaseUrl: string;
+
+ amountRaw: AmountString;
+
+ amountEffective: AmountString;
+}
+
+/**
+ * We sent money via a P2P payment.
+ */
+export interface TransactionPeerPushDebit extends TransactionCommon {
+ type: TransactionType.PeerPushDebit;
+
+ info: PeerInfoShort;
+ /**
+ * Exchange used.
+ */
+ 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;
+
+ /**
+ * URI to accept the payment.
+ *
+ * Only present if the transaction is in a state where the other party can
+ * accept the payment.
+ */
+ talerUri?: string;
+}
+
+/**
+ * We received money via a P2P payment.
+ */
+export interface TransactionPeerPushCredit extends TransactionCommon {
+ type: TransactionType.PeerPushCredit;
+
+ info: PeerInfoShort;
+ /**
+ * Exchange used.
+ */
+ 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;
+}
+
+/**
+ * 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
+ */
+ Aborted = "aborted",
+
+ /**
+ * Payment failed, wallet will auto-retry.
+ * User should be given the option to retry now / abort.
+ */
+ Failed = "failed",
+
+ /**
+ * Paid successfully
+ */
+ Paid = "paid",
+
+ /**
+ * User accepted, payment is processing.
+ */
+ Accepted = "accepted",
+}
+
+export interface TransactionPayment extends TransactionCommon {
+ type: TransactionType.Payment;
+
+ /**
+ * Additional information about the payment.
+ */
+ info: OrderShortInfo;
+
+ /**
+ * Wallet-internal end-to-end identifier for the payment.
+ */
+ proposalId: string;
+
+ /**
+ * Amount that must be paid for the contract
+ */
+ amountRaw: AmountString;
+
+ /**
+ * Amount that was paid, including deposit, wire and refresh fees.
+ */
+ amountEffective: AmountString;
+
+ /**
+ * Amount that has been refunded by the merchant
+ */
+ totalRefundRaw: AmountString;
+
+ /**
+ * Amount will be added to the wallet's balance after fees and refreshing
+ */
+ totalRefundEffective: AmountString;
+
+ /**
+ * Amount pending to be picked up
+ */
+ refundPending: AmountString | undefined;
+
+ /**
+ * 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 {
+ /**
+ * Order ID, uniquely identifies the order within a merchant instance
+ */
+ orderId: string;
+
+ /**
+ * Hash of the contract terms.
+ */
+ contractTermsHash: string;
+
+ /**
+ * More information about the merchant
+ */
+ merchant: MerchantInfo;
+
+ /**
+ * Summary of the order, given by the merchant
+ */
+ summary: string;
+
+ /**
+ * Map from IETF BCP 47 language tags to localized summaries
+ */
+ summary_i18n?: InternationalizedString;
+
+ /**
+ * URL of the fulfillment, given by the merchant
+ */
+ fulfillmentUrl?: string;
+
+ /**
+ * Plain text message that should be shown to the user
+ * when the payment is complete.
+ */
+ fulfillmentMessage?: string;
+
+ /**
+ * Translations of fulfillmentMessage.
+ */
+ fulfillmentMessage_i18n?: InternationalizedString;
+}
+
+export interface RefundInfoShort {
+ transactionId: string;
+ timestamp: TalerProtocolTimestamp;
+ amountEffective: AmountString;
+ amountRaw: AmountString;
+}
+
+/**
+ * Summary information about the payment that we got a refund for.
+ */
+export interface RefundPaymentInfo {
+ summary: string;
+ summary_i18n?: InternationalizedString;
+ /**
+ * More information about the merchant
+ */
+ 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;
+
+ // ID for the transaction that is refunded
+ refundedTransactionId: string;
+
+ paymentInfo: RefundPaymentInfo | undefined;
+}
+
+/**
+ * A transaction shown for refreshes.
+ * Only shown for (1) refreshes not associated with other transactions
+ * and (2) refreshes in an error state.
+ */
+export interface TransactionRefresh extends TransactionCommon {
+ type: TransactionType.Refresh;
+
+ refreshReason: RefreshReason;
+
+ /**
+ * Transaction ID that caused this refresh.
+ */
+ originatingTransactionId?: string;
+
+ /**
+ * Always zero for refreshes
+ */
+ amountRaw: AmountString;
+
+ /**
+ * 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;
+}
+
+/**
+ * Deposit transaction, which effectively sends
+ * money from this wallet somewhere else.
+ */
+export interface TransactionDeposit extends TransactionCommon {
+ type: TransactionType.Deposit;
+
+ depositGroupId: string;
+
+ /**
+ * Target for the deposit.
+ */
+ targetPaytoUri: string;
+
+ /**
+ * Raw amount that is being deposited
+ */
+ amountRaw: AmountString;
+
+ /**
+ * Effective amount that is being deposited
+ */
+ amountEffective: AmountString;
+
+ wireTransferDeadline: TalerProtocolTimestamp;
+
+ wireTransferProgress: number;
+
+ /**
+ * Did all the deposit requests succeed?
+ */
+ deposited: boolean;
+
+ trackingState: Array<DepositTransactionTrackingState>;
+}
+
+export interface TransactionByIdRequest {
+ transactionId: string;
+}
+
+export const codecForTransactionByIdRequest =
+ (): Codec<TransactionByIdRequest> =>
+ buildCodecForObject<TransactionByIdRequest>()
+ .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!
+export const codecForTransactionsResponse = (): Codec<TransactionsResponse> =>
+ buildCodecForObject<TransactionsResponse>()
+ .property("transactions", codecForList(codecForAny()))
+ .build("TransactionsResponse");
+
+export const codecForOrderShortInfo = (): Codec<OrderShortInfo> =>
+ buildCodecForObject<OrderShortInfo>()
+ .property("contractTermsHash", codecForString())
+ .property("fulfillmentMessage", codecOptional(codecForString()))
+ .property(
+ "fulfillmentMessage_i18n",
+ codecOptional(codecForInternationalizedString()),
+ )
+ .property("fulfillmentUrl", codecOptional(codecForString()))
+ .property("merchant", codecForMerchantInfo())
+ .property("orderId", codecForString())
+ .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/transactionsTypes.ts b/packages/taler-util/src/transactionsTypes.ts
deleted file mode 100644
index b9a227b68..000000000
--- a/packages/taler-util/src/transactionsTypes.ts
+++ /dev/null
@@ -1,376 +0,0 @@
-/*
- 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/>
- */
-
-/**
- * Type and schema definitions for the wallet's transaction list.
- *
- * @author Florian Dold
- * @author Torsten Grote
- */
-
-/**
- * Imports.
- */
-import { TalerProtocolTimestamp } from "./time.js";
-import {
- AmountString,
- Product,
- InternationalizedString,
- MerchantInfo,
- codecForInternationalizedString,
- codecForMerchantInfo,
- codecForProduct,
-} from "./talerTypes.js";
-import {
- Codec,
- buildCodecForObject,
- codecOptional,
- codecForString,
- codecForList,
- codecForAny,
-} from "./codec.js";
-import { TalerErrorDetail } from "./walletTypes.js";
-
-export interface TransactionsRequest {
- /**
- * return only transactions in the given currency
- */
- currency?: string;
-
- /**
- * if present, results will be limited to transactions related to the given search string
- */
- search?: string;
-}
-
-export interface TransactionsResponse {
- // a list of past and pending transactions sorted by pending, timestamp and transactionId.
- // In case two events are both pending and have the same timestamp,
- // they are sorted by the transactionId
- // (lexically ascending and locale-independent comparison).
- transactions: Transaction[];
-}
-
-export interface TransactionCommon {
- // opaque unique ID for the transaction, used as a starting point for paginating queries
- // and for invoking actions on the transaction (e.g. deleting/hiding it from the history)
- transactionId: string;
-
- // the type of the transaction; different types might provide additional information
- type: TransactionType;
-
- // main timestamp of the transaction
- timestamp: TalerProtocolTimestamp;
-
- // 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;
-
- /**
- * True if the transaction encountered a problem that might be
- * permanent. A frozen transaction won't be automatically retried.
- */
- frozen: boolean;
-
- // Raw amount of the transaction (exclusive of fees or other extra costs)
- amountRaw: AmountString;
-
- // Amount added or removed from the wallet's balance (including all fees and other costs)
- amountEffective: AmountString;
-
- error?: TalerErrorDetail;
-}
-
-export type Transaction =
- | TransactionWithdrawal
- | TransactionPayment
- | TransactionRefund
- | TransactionTip
- | TransactionRefresh
- | TransactionDeposit;
-
-export enum TransactionType {
- Withdrawal = "withdrawal",
- Payment = "payment",
- Refund = "refund",
- Refresh = "refresh",
- Tip = "tip",
- Deposit = "deposit",
-}
-
-export enum WithdrawalType {
- TalerBankIntegrationApi = "taler-bank-integration-api",
- ManualTransfer = "manual-transfer",
-}
-
-export type WithdrawalDetails =
- | WithdrawalDetailsForManualTransfer
- | WithdrawalDetailsForTalerBankIntegrationApi;
-
-interface WithdrawalDetailsForManualTransfer {
- type: WithdrawalType.ManualTransfer;
-
- /**
- * Payto URIs that the exchange supports.
- *
- * Already contains the amount and message.
- */
- exchangePaytoUris: string[];
-
- // Public key of the reserve
- reservePub: string;
-}
-
-interface WithdrawalDetailsForTalerBankIntegrationApi {
- type: WithdrawalType.TalerBankIntegrationApi;
-
- /**
- * Set to true if the bank has confirmed the withdrawal, false if not.
- * An unconfirmed withdrawal usually requires user-input and should be highlighted in the UI.
- * See also bankConfirmationUrl below.
- */
- confirmed: boolean;
-
- /**
- * If the withdrawal is unconfirmed, this can include a URL for user
- * initiated confirmation.
- */
- bankConfirmationUrl?: string;
-
- // Public key of the reserve
- reservePub: string;
-}
-
-// This should only be used for actual withdrawals
-// and not for tips that have their own transactions type.
-export interface TransactionWithdrawal extends TransactionCommon {
- type: TransactionType.Withdrawal;
-
- /**
- * 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 enum PaymentStatus {
- /**
- * Explicitly aborted after timeout / failure
- */
- Aborted = "aborted",
-
- /**
- * Payment failed, wallet will auto-retry.
- * User should be given the option to retry now / abort.
- */
- Failed = "failed",
-
- /**
- * Paid successfully
- */
- Paid = "paid",
-
- /**
- * User accepted, payment is processing.
- */
- Accepted = "accepted",
-}
-
-export interface TransactionPayment extends TransactionCommon {
- type: TransactionType.Payment;
-
- /**
- * Additional information about the payment.
- */
- info: OrderShortInfo;
-
- /**
- * Wallet-internal end-to-end identifier for the payment.
- */
- proposalId: string;
-
- /**
- * How far did the wallet get with processing the payment?
- */
- status: PaymentStatus;
-
- /**
- * Amount that must be paid for the contract
- */
- amountRaw: AmountString;
-
- /**
- * Amount that was paid, including deposit, wire and refresh fees.
- */
- amountEffective: AmountString;
-}
-
-export interface OrderShortInfo {
- /**
- * Order ID, uniquely identifies the order within a merchant instance
- */
- orderId: string;
-
- /**
- * Hash of the contract terms.
- */
- contractTermsHash: string;
-
- /**
- * More information about the merchant
- */
- merchant: MerchantInfo;
-
- /**
- * Summary of the order, given by the merchant
- */
- summary: string;
-
- /**
- * Map from IETF BCP 47 language tags to localized summaries
- */
- summary_i18n?: InternationalizedString;
-
- /**
- * List of products that are part of the order
- */
- products: Product[] | undefined;
-
- /**
- * URL of the fulfillment, given by the merchant
- */
- fulfillmentUrl?: string;
-
- /**
- * Plain text message that should be shown to the user
- * when the payment is complete.
- */
- fulfillmentMessage?: string;
-
- /**
- * Translations of fulfillmentMessage.
- */
- fulfillmentMessage_i18n?: InternationalizedString;
-}
-
-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;
-
- // 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;
-
- // Amount will be (or was) added to the wallet's balance after fees and refreshing
- amountEffective: AmountString;
-
- merchantBaseUrl: string;
-}
-
-// A transaction shown for refreshes that are not associated to other transactions
-// such as a refresh necessary before coin expiration.
-// It should only be returned by the API if the effective amount is different from zero.
-export interface TransactionRefresh extends TransactionCommon {
- type: TransactionType.Refresh;
-
- // Exchange that the coins are refreshed with
- exchangeBaseUrl: string;
-
- // Raw amount that is refreshed
- amountRaw: AmountString;
-
- // Amount that will be paid as fees for the refresh
- amountEffective: AmountString;
-}
-
-/**
- * Deposit transaction, which effectively sends
- * money from this wallet somewhere else.
- */
-export interface TransactionDeposit extends TransactionCommon {
- type: TransactionType.Deposit;
-
- depositGroupId: string;
-
- /**
- * Target for the deposit.
- */
- targetPaytoUri: string;
-
- /**
- * Raw amount that is being deposited
- */
- amountRaw: AmountString;
-
- /**
- * Effective amount that is being deposited
- */
- amountEffective: AmountString;
-}
-
-export const codecForTransactionsRequest = (): Codec<TransactionsRequest> =>
- buildCodecForObject<TransactionsRequest>()
- .property("currency", codecOptional(codecForString()))
- .property("search", codecOptional(codecForString()))
- .build("TransactionsRequest");
-
-// FIXME: do full validation here!
-export const codecForTransactionsResponse = (): Codec<TransactionsResponse> =>
- buildCodecForObject<TransactionsResponse>()
- .property("transactions", codecForList(codecForAny()))
- .build("TransactionsResponse");
-
-export const codecForOrderShortInfo = (): Codec<OrderShortInfo> =>
- buildCodecForObject<OrderShortInfo>()
- .property("contractTermsHash", codecForString())
- .property("fulfillmentMessage", codecOptional(codecForString()))
- .property(
- "fulfillmentMessage_i18n",
- codecOptional(codecForInternationalizedString()),
- )
- .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");
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/types-test.ts b/packages/taler-util/src/types-test.ts
index e8af13119..6acd2c26e 100644
--- a/packages/taler-util/src/types-test.ts
+++ b/packages/taler-util/src/types-test.ts
@@ -15,7 +15,7 @@
*/
import test from "ava";
-import { codecForContractTerms } from "./talerTypes.js";
+import { codecForMerchantContractTerms as codecForContractTerms } from "./taler-types.js";
test("contract terms validation", (t) => {
const c = {
diff --git a/packages/taler-util/src/url.ts b/packages/taler-util/src/url.ts
index 5246ed066..149997f3f 100644
--- a/packages/taler-util/src/url.ts
+++ b/packages/taler-util/src/url.ts
@@ -14,6 +14,8 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
+import { URLImpl, URLSearchParamsImpl } from "./whatwg-url.js";
+
interface URL {
hash: string;
host: string;
@@ -52,7 +54,12 @@ interface URLSearchParams {
export interface URLSearchParamsCtor {
new (
- init?: string[][] | Record<string, string> | string | URLSearchParams,
+ init?:
+ | URLSearchParams
+ | string
+ | Record<string, string | ReadonlyArray<string>>
+ | Iterable<[string, string]>
+ | ReadonlyArray<[string, string]>,
): URLSearchParams;
}
@@ -75,19 +82,28 @@ export interface URLCtor {
delete Object.prototype.__magic__;
})();
+// Use native or pure JS URL implementation?
+const useOwnUrlImp = true;
+
// @ts-ignore
-const _URL = globalThis.URL;
-if (!_URL) {
- throw Error("FATAL: URL not available");
+let _URL = globalThis.URL;
+if (useOwnUrlImp || !_URL) {
+ // @ts-ignore
+ globalThis.URL = _URL = URLImpl;
+ // @ts-ignore
+ _URL = URLImpl;
}
export const URL: URLCtor = _URL;
// @ts-ignore
-const _URLSearchParams = globalThis.URLSearchParams;
+let _URLSearchParams = globalThis.URLSearchParams;
-if (!_URLSearchParams) {
- throw Error("FATAL: URLSearchParams not available");
+if (useOwnUrlImp || !_URLSearchParams) {
+ // @ts-ignore
+ globalThis.URLSearchParams = URLSearchParamsImpl;
+ // @ts-ignore
+ _URLSearchParams = URLSearchParamsImpl;
}
export const URLSearchParams: URLSearchParamsCtor = _URLSearchParams;
diff --git a/packages/taler-util/src/wallet-types.ts b/packages/taler-util/src/wallet-types.ts
new file mode 100644
index 000000000..ec6cb6f58
--- /dev/null
+++ b/packages/taler-util/src/wallet-types.ts
@@ -0,0 +1,3296 @@
+/*
+ This file is part of GNU Taler
+ (C) 2015-2020 Taler Systems SA
+
+ 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/>
+ */
+
+/**
+ * Types used by clients of the wallet.
+ *
+ * These types are defined in a separate file make tree shaking easier, since
+ * some components use these types (via RPC) but do not depend on the wallet
+ * code directly.
+ *
+ * @author Florian Dold <dold@taler.net>
+ */
+
+/**
+ * Imports.
+ */
+import { AmountJson, codecForAmountString } from "./amounts.js";
+import { BackupRecovery } from "./backup-types.js";
+import {
+ Codec,
+ Context,
+ DecodingError,
+ buildCodecForObject,
+ buildCodecForUnion,
+ codecForAny,
+ codecForBoolean,
+ codecForConstString,
+ codecForEither,
+ codecForList,
+ codecForMap,
+ codecForNumber,
+ codecForString,
+ codecOptional,
+ renderContext,
+} from "./codec.js";
+import {
+ CurrencySpecification,
+ 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,
+ CoinEnvelope,
+ DenomKeyType,
+ DenominationPubKey,
+ EddsaPrivateKeyString,
+ ExchangeAuditor,
+ ExchangeWireAccount,
+ InternationalizedString,
+ MerchantContractTerms,
+ MerchantInfo,
+ PeerContractTerms,
+ UnblindedSignature,
+ codecForExchangeWireAccount,
+ codecForMerchantContractTerms,
+ codecForPeerContractTerms,
+} from "./taler-types.js";
+import {
+ AbsoluteTime,
+ TalerPreciseTimestamp,
+ TalerProtocolDuration,
+ TalerProtocolTimestamp,
+ codecForAbsoluteTime,
+ codecForPreciseTimestamp,
+ codecForTimestamp,
+} from "./time.js";
+import {
+ OrderShortInfo,
+ TransactionState,
+ TransactionType,
+} from "./transactions-types.js";
+
+/**
+ * Identifier for a transaction in the wallet.
+ */
+declare const __txId: unique symbol;
+export type TransactionIdStr = `txn:${string}:${string}` & { [__txId]: true };
+
+/**
+ * Identifier for a pending task in the wallet.
+ */
+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.
+ */
+export class CreateReserveResponse {
+ /**
+ * Exchange URL where the bank should create the reserve.
+ * The URL is canonicalized in the response.
+ */
+ exchange: string;
+
+ /**
+ * Reserve public key of the newly created reserve.
+ */
+ reservePub: string;
+}
+
+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?
+ *
+ * @deprecated use flags and pendingIncoming/pendingOutgoing instead
+ */
+ hasPendingTransactions: boolean;
+
+ /**
+ * 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"]>;
+}
+
+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;
+ };
+}
+
+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: WalletBalance[];
+}
+
+export const codecForBalance = (): Codec<WalletBalance> =>
+ buildCodecForObject<WalletBalance>()
+ .property("scopeInfo", codecForAny()) // FIXME
+ .property("available", codecForAmountString())
+ .property("hasPendingTransactions", codecForBoolean())
+ .property("pendingIncoming", codecForAmountString())
+ .property("pendingOutgoing", codecForAmountString())
+ .property("requiresUserInput", codecForBoolean())
+ .property("flags", codecForAny()) // FIXME
+ .build("Balance");
+
+export const codecForBalancesResponse = (): Codec<BalancesResponse> =>
+ buildCodecForObject<BalancesResponse>()
+ .property("balances", codecForList(codecForBalance()))
+ .build("BalancesResponse");
+
+/**
+ * For terseness.
+ */
+export function mkAmount(
+ value: number,
+ fraction: number,
+ currency: string,
+): AmountJson {
+ return { value, fraction, currency };
+}
+
+/**
+ * Status of a coin.
+ */
+export enum CoinStatus {
+ /**
+ * Withdrawn and never shown to anybody.
+ */
+ 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.
+ */
+ FreshSuspended = "fresh-suspended",
+
+ /**
+ * A coin that has been spent and refreshed.
+ */
+ Dormant = "dormant",
+}
+
+/**
+ * Easy to process format for the public data of coins
+ * managed by the wallet.
+ */
+export interface CoinDumpJson {
+ coins: Array<{
+ /**
+ * The coin's denomination's public key.
+ */
+ denom_pub: DenominationPubKey;
+ /**
+ * Hash of denom_pub.
+ */
+ denom_pub_hash: string;
+ /**
+ * Value of the denomination (without any fees).
+ */
+ denom_value: string;
+ /**
+ * Public key of the coin.
+ */
+ coin_pub: string;
+ /**
+ * Base URL of the exchange for the coin.
+ */
+ exchange_base_url: string;
+ /**
+ * Public key of the parent coin.
+ * Only present if this coin was obtained via refreshing.
+ */
+ refresh_parent_coin_pub: string | undefined;
+ /**
+ * Public key of the reserve for this coin.
+ * Only present if this coin was obtained via refreshing.
+ */
+ withdrawal_reserve_pub: string | undefined;
+ coin_status: CoinStatus;
+ spend_allocation:
+ | {
+ id: string;
+ amount: AmountString;
+ }
+ | undefined;
+ /**
+ * Information about the age restriction
+ */
+ ageCommitmentProof: AgeCommitmentProof | undefined;
+ }>;
+}
+
+export enum ConfirmPayResultType {
+ Done = "done",
+ Pending = "pending",
+}
+
+/**
+ * Result for confirmPay
+ */
+export interface ConfirmPayResultDone {
+ type: ConfirmPayResultType.Done;
+ contractTerms: MerchantContractTerms;
+ transactionId: TransactionIdStr;
+}
+
+export interface ConfirmPayResultPending {
+ type: ConfirmPayResultType.Pending;
+ 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");
+
+export type ConfirmPayResult = ConfirmPayResultDone | ConfirmPayResultPending;
+
+export const codecForConfirmPayResultPending =
+ (): Codec<ConfirmPayResultPending> =>
+ buildCodecForObject<ConfirmPayResultPending>()
+ .property("lastError", codecOptional(codecForTalerErrorDetail()))
+ .property("transactionId", codecForTransactionIdStr())
+ .property("type", codecForConstString(ConfirmPayResultType.Pending))
+ .build("ConfirmPayResultPending");
+
+export const codecForConfirmPayResultDone = (): Codec<ConfirmPayResultDone> =>
+ buildCodecForObject<ConfirmPayResultDone>()
+ .property("type", codecForConstString(ConfirmPayResultType.Done))
+ .property("transactionId", codecForTransactionIdStr())
+ .property("contractTerms", codecForMerchantContractTerms())
+ .build("ConfirmPayResultDone");
+
+export const codecForConfirmPayResult = (): Codec<ConfirmPayResult> =>
+ buildCodecForUnion<ConfirmPayResult>()
+ .discriminateOn("type")
+ .alternative(
+ ConfirmPayResultType.Pending,
+ codecForConfirmPayResultPending(),
+ )
+ .alternative(ConfirmPayResultType.Done, codecForConfirmPayResultDone())
+ .build("ConfirmPayResult");
+
+/**
+ * Information about all sender wire details known to the wallet,
+ * as well as exchanges that accept these wire types.
+ */
+export interface SenderWireInfos {
+ /**
+ * Mapping from exchange base url to list of accepted
+ * wire types.
+ */
+ exchangeWireTypes: { [exchangeBaseUrl: string]: string[] };
+
+ /**
+ * Sender wire information stored in the wallet.
+ */
+ senderWires: string[];
+}
+
+/**
+ * Request to mark a reserve as confirmed.
+ */
+export interface ConfirmReserveRequest {
+ /**
+ * Public key of then reserve that should be marked
+ * as confirmed.
+ */
+ reservePub: string;
+}
+
+export const codecForConfirmReserveRequest = (): Codec<ConfirmReserveRequest> =>
+ buildCodecForObject<ConfirmReserveRequest>()
+ .property("reservePub", codecForString())
+ .build("ConfirmReserveRequest");
+
+export interface PrepareRefundResult {
+ proposalId: string;
+
+ effectivePaid: AmountString;
+ gone: AmountString;
+ granted: AmountString;
+ pending: boolean;
+ awaiting: AmountString;
+
+ info: OrderShortInfo;
+}
+
+export interface BenchmarkResult {
+ time: { [s: string]: number };
+ repetitions: number;
+}
+
+export enum PreparePayResultType {
+ PaymentPossible = "payment-possible",
+ InsufficientBalance = "insufficient-balance",
+ AlreadyConfirmed = "already-confirmed",
+}
+
+export const codecForPreparePayResultPaymentPossible =
+ (): Codec<PreparePayResultPaymentPossible> =>
+ buildCodecForObject<PreparePayResultPaymentPossible>()
+ .property("amountEffective", codecForAmountString())
+ .property("amountRaw", codecForAmountString())
+ .property("contractTerms", codecForMerchantContractTerms())
+ .property("transactionId", codecForTransactionIdStr())
+ .property("proposalId", codecForString())
+ .property("contractTermsHash", codecForString())
+ .property("talerUri", 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>()
+ .property("amountRaw", codecForAmountString())
+ .property("contractTerms", codecForAny())
+ .property("talerUri", codecForString())
+ .property("proposalId", codecForString())
+ .property("transactionId", codecForTransactionIdStr())
+ .property(
+ "status",
+ codecForConstString(PreparePayResultType.InsufficientBalance),
+ )
+ .property(
+ "balanceDetails",
+ codecForPayMerchantInsufficientBalanceDetails(),
+ )
+ .build("PreparePayResultInsufficientBalance");
+
+export const codecForPreparePayResultAlreadyConfirmed =
+ (): Codec<PreparePayResultAlreadyConfirmed> =>
+ buildCodecForObject<PreparePayResultAlreadyConfirmed>()
+ .property(
+ "status",
+ codecForConstString(PreparePayResultType.AlreadyConfirmed),
+ )
+ .property("amountEffective", codecOptional(codecForAmountString()))
+ .property("amountRaw", codecForAmountString())
+ .property("paid", codecForBoolean())
+ .property("talerUri", codecForString())
+ .property("contractTerms", codecForAny())
+ .property("contractTermsHash", codecForString())
+ .property("transactionId", codecForTransactionIdStr())
+ .property("proposalId", codecForString())
+ .build("PreparePayResultAlreadyConfirmed");
+
+export const codecForPreparePayResult = (): Codec<PreparePayResult> =>
+ buildCodecForUnion<PreparePayResult>()
+ .discriminateOn("status")
+ .alternative(
+ PreparePayResultType.AlreadyConfirmed,
+ codecForPreparePayResultAlreadyConfirmed(),
+ )
+ .alternative(
+ PreparePayResultType.InsufficientBalance,
+ codecForPreparePayResultInsufficientBalance(),
+ )
+ .alternative(
+ PreparePayResultType.PaymentPossible,
+ codecForPreparePayResultPaymentPossible(),
+ )
+ .build("PreparePayResult");
+
+/**
+ * Result of a prepare pay operation.
+ */
+export type PreparePayResult =
+ | PreparePayResultInsufficientBalance
+ | PreparePayResultAlreadyConfirmed
+ | PreparePayResultPaymentPossible;
+
+/**
+ * Payment is possible.
+ */
+export interface PreparePayResultPaymentPossible {
+ status: PreparePayResultType.PaymentPossible;
+ transactionId: TransactionIdStr;
+ /**
+ * @deprecated use transactionId instead
+ */
+ proposalId: string;
+ contractTerms: MerchantContractTerms;
+ contractTermsHash: string;
+ amountRaw: AmountString;
+ amountEffective: AmountString;
+ talerUri: string;
+}
+
+export interface PreparePayResultInsufficientBalance {
+ status: PreparePayResultType.InsufficientBalance;
+ transactionId: TransactionIdStr;
+ /**
+ * @deprecated use transactionId
+ */
+ proposalId: string;
+ contractTerms: MerchantContractTerms;
+ amountRaw: AmountString;
+ talerUri: string;
+ balanceDetails: PaymentInsufficientBalanceDetails;
+}
+
+export interface PreparePayResultAlreadyConfirmed {
+ status: PreparePayResultType.AlreadyConfirmed;
+ transactionId: TransactionIdStr;
+ contractTerms: MerchantContractTerms;
+ paid: boolean;
+ amountRaw: AmountString;
+ amountEffective: AmountString | undefined;
+ contractTermsHash: string;
+ /**
+ * @deprecated use transactionId
+ */
+ proposalId: string;
+ talerUri: string;
+}
+
+export interface BankWithdrawDetails {
+ status: WithdrawalOperationStatus;
+ amount: AmountJson;
+ senderWire?: string;
+ suggestedExchange?: string;
+ confirmTransferUrl?: string;
+ wireTypes: string[];
+ operationId: string;
+ apiBaseUrl: string;
+}
+
+export interface AcceptWithdrawalResponse {
+ reservePub: string;
+ confirmTransferUrl?: string;
+ transactionId: TransactionIdStr;
+}
+
+/**
+ * Details about a purchase, including refund status.
+ */
+export interface PurchaseDetails {
+ contractTerms: Record<string, undefined>;
+ hasRefund: boolean;
+ totalRefundAmount: AmountJson;
+ totalRefundAndRefreshFees: AmountJson;
+}
+
+export interface WalletDiagnostics {
+ walletManifestVersion: string;
+ walletManifestDisplayVersion: string;
+ errors: string[];
+ firefoxIdbProblem: boolean;
+ dbOutdated: boolean;
+}
+
+export interface TalerErrorDetail {
+ code: TalerErrorCode;
+ when?: AbsoluteTime;
+ hint?: string;
+ [x: string]: unknown;
+}
+
+/**
+ * Minimal information needed about a planchet for unblinding a signature.
+ *
+ * Can be a withdrawal/refresh planchet.
+ */
+export interface PlanchetUnblindInfo {
+ denomPub: DenominationPubKey;
+ blindingKey: string;
+}
+
+export interface WithdrawalPlanchet {
+ coinPub: string;
+ coinPriv: string;
+ reservePub: string;
+ denomPubHash: string;
+ denomPub: DenominationPubKey;
+ blindingKey: string;
+ withdrawSig: string;
+ coinEv: CoinEnvelope;
+ coinValue: AmountJson;
+ coinEvHash: string;
+ ageCommitmentProof?: AgeCommitmentProof;
+}
+
+export interface PlanchetCreationRequest {
+ secretSeed: string;
+ coinIndex: number;
+ value: AmountJson;
+ feeWithdraw: AmountJson;
+ denomPub: DenominationPubKey;
+ reservePub: string;
+ reservePriv: string;
+ restrictAge?: number;
+}
+
+/**
+ * Reasons for why a coin is being refreshed.
+ */
+export enum RefreshReason {
+ Manual = "manual",
+ PayMerchant = "pay-merchant",
+ PayDeposit = "pay-deposit",
+ PayPeerPush = "pay-peer-push",
+ 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",
+}
+
+/**
+ * Request to refresh a single coin.
+ */
+export interface CoinRefreshRequest {
+ readonly coinPub: string;
+ readonly amount: AmountString;
+}
+
+/**
+ * Private data required to make a deposit permission.
+ */
+export interface DepositInfo {
+ exchangeBaseUrl: string;
+ contractTermsHash: string;
+ coinPub: string;
+ coinPriv: string;
+ spendAmount: AmountJson;
+ timestamp: TalerProtocolTimestamp;
+ refundDeadline: TalerProtocolTimestamp;
+ merchantPub: string;
+ feeDeposit: AmountJson;
+ wireInfoHash: string;
+ denomKeyType: DenomKeyType;
+ denomPubHash: string;
+ denomSig: UnblindedSignature;
+
+ requiredMinimumAge?: number;
+
+ ageCommitmentProof?: AgeCommitmentProof;
+}
+
+export interface ExchangesShortListResponse {
+ exchanges: ShortExchangeListItem[];
+}
+
+export interface ExchangesListResponse {
+ exchanges: ExchangeListItem[];
+}
+
+export interface ExchangeDetailedResponse {
+ exchange: ExchangeFullDetails;
+}
+
+export interface WalletCoreVersion {
+ 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;
+}
+
+export interface KnownBankAccountsInfo {
+ uri: PaytoUri;
+ kyc_completed: boolean;
+ currency: string;
+ alias: string;
+}
+
+export interface KnownBankAccounts {
+ accounts: KnownBankAccountsInfo[];
+}
+
+/**
+ * Wire fee for one wire method
+ */
+export interface WireFee {
+ /**
+ * Fee for wire transfers.
+ */
+ wireFee: AmountString;
+
+ /**
+ * Fees to close and refund a reserve.
+ */
+ closingFee: AmountString;
+
+ /**
+ * Start date of the fee.
+ */
+ startStamp: TalerProtocolTimestamp;
+
+ /**
+ * End date of the fee.
+ */
+ endStamp: TalerProtocolTimestamp;
+
+ /**
+ * Signature made by the exchange master key.
+ */
+ sig: string;
+}
+
+export type WireFeeMap = { [wireMethod: string]: WireFee[] };
+
+export interface WireInfo {
+ feesForType: WireFeeMap;
+ accounts: ExchangeWireAccount[];
+}
+
+export interface ExchangeGlobalFees {
+ startDate: TalerProtocolTimestamp;
+ endDate: TalerProtocolTimestamp;
+
+ historyFee: AmountString;
+ accountFee: AmountString;
+ purseFee: AmountString;
+
+ historyTimeout: TalerProtocolDuration;
+ purseTimeout: TalerProtocolDuration;
+
+ purseLimit: number;
+
+ signature: string;
+}
+
+const codecForWireFee = (): Codec<WireFee> =>
+ buildCodecForObject<WireFee>()
+ .property("sig", codecForString())
+ .property("wireFee", codecForAmountString())
+ .property("closingFee", codecForAmountString())
+ .property("startStamp", codecForTimestamp)
+ .property("endStamp", codecForTimestamp)
+ .build("codecForWireFee");
+
+const codecForWireInfo = (): Codec<WireInfo> =>
+ buildCodecForObject<WireInfo>()
+ .property("feesForType", codecForMap(codecForList(codecForWireFee())))
+ .property("accounts", codecForList(codecForExchangeWireAccount()))
+ .build("codecForWireInfo");
+
+export interface DenominationInfo {
+ /**
+ * Value of one coin of the denomination.
+ */
+ value: AmountString;
+
+ /**
+ * Hash of the denomination public key.
+ * Stored in the database for faster lookups.
+ */
+ denomPubHash: string;
+
+ denomPub: DenominationPubKey;
+
+ /**
+ * Fee for withdrawing.
+ */
+ feeWithdraw: AmountString;
+
+ /**
+ * Fee for depositing.
+ */
+ feeDeposit: AmountString;
+
+ /**
+ * Fee for refreshing.
+ */
+ feeRefresh: AmountString;
+
+ /**
+ * Fee for refunding.
+ */
+ feeRefund: AmountString;
+
+ /**
+ * Validity start date of the denomination.
+ */
+ stampStart: TalerProtocolTimestamp;
+
+ /**
+ * Date after which the currency can't be withdrawn anymore.
+ */
+ stampExpireWithdraw: TalerProtocolTimestamp;
+
+ /**
+ * Date after the denomination officially doesn't exist anymore.
+ */
+ stampExpireLegal: TalerProtocolTimestamp;
+
+ /**
+ * Data after which coins of this denomination can't be deposited anymore.
+ */
+ stampExpireDeposit: TalerProtocolTimestamp;
+
+ exchangeBaseUrl: string;
+}
+
+export type DenomOperation = "deposit" | "withdraw" | "refresh" | "refund";
+export type DenomOperationMap<T> = { [op in DenomOperation]: T };
+
+export interface FeeDescription {
+ group: string;
+ from: AbsoluteTime;
+ until: AbsoluteTime;
+ fee?: AmountString;
+}
+
+export interface FeeDescriptionPair {
+ group: string;
+ from: AbsoluteTime;
+ until: AbsoluteTime;
+ left?: AmountString;
+ right?: AmountString;
+}
+
+export interface TimePoint<T> {
+ id: string;
+ group: string;
+ fee: AmountString;
+ type: "start" | "end";
+ moment: AbsoluteTime;
+ denom: T;
+}
+
+export interface ExchangeFullDetails {
+ exchangeBaseUrl: string;
+ currency: string;
+ paytoUris: string[];
+ auditors: ExchangeAuditor[];
+ wireInfo: WireInfo;
+ denomFees: DenomOperationMap<FeeDescription[]>;
+ transferFees: Record<string, FeeDescription[]>;
+ globalFees: FeeDescription[];
+}
+
+export enum ExchangeTosStatus {
+ Pending = "pending",
+ Proposed = "proposed",
+ Accepted = "accepted",
+}
+
+export enum ExchangeEntryStatus {
+ 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;
+}
+
+export interface ShortExchangeListItem {
+ exchangeBaseUrl: string;
+}
+
+/**
+ * Info about an exchange entry in the wallet.
+ */
+export interface ExchangeListItem {
+ exchangeBaseUrl: string;
+ masterPub: string | undefined;
+ currency: string;
+ paytoUris: string[];
+ tosStatus: ExchangeTosStatus;
+ exchangeEntryStatus: ExchangeEntryStatus;
+ exchangeUpdateStatus: ExchangeUpdateStatus;
+ ageRestrictionOptions: number[];
+
+ /**
+ * P2P payments are disabled with this exchange
+ * (e.g. because no global fees are configured).
+ */
+ 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
+ * to update the exchange info.
+ */
+ lastUpdateErrorInfo?: OperationErrorInfo;
+}
+
+const codecForAuditorDenomSig = (): Codec<AuditorDenomSig> =>
+ buildCodecForObject<AuditorDenomSig>()
+ .property("denom_pub_h", codecForString())
+ .property("auditor_sig", codecForString())
+ .build("AuditorDenomSig");
+
+const codecForExchangeAuditor = (): Codec<ExchangeAuditor> =>
+ buildCodecForObject<ExchangeAuditor>()
+ .property("auditor_pub", codecForString())
+ .property("auditor_url", codecForString())
+ .property("denomination_keys", codecForList(codecForAuditorDenomSig()))
+ .build("codecForExchangeAuditor");
+
+export const codecForFeeDescriptionPair = (): Codec<FeeDescriptionPair> =>
+ buildCodecForObject<FeeDescriptionPair>()
+ .property("group", codecForString())
+ .property("from", codecForAbsoluteTime)
+ .property("until", codecForAbsoluteTime)
+ .property("left", codecOptional(codecForAmountString()))
+ .property("right", codecOptional(codecForAmountString()))
+ .build("FeeDescriptionPair");
+
+export const codecForFeeDescription = (): Codec<FeeDescription> =>
+ buildCodecForObject<FeeDescription>()
+ .property("group", codecForString())
+ .property("from", codecForAbsoluteTime)
+ .property("until", codecForAbsoluteTime)
+ .property("fee", codecOptional(codecForAmountString()))
+ .build("FeeDescription");
+
+export const codecForFeesByOperations = (): Codec<
+ DenomOperationMap<FeeDescription[]>
+> =>
+ buildCodecForObject<DenomOperationMap<FeeDescription[]>>()
+ .property("deposit", codecForList(codecForFeeDescription()))
+ .property("withdraw", codecForList(codecForFeeDescription()))
+ .property("refresh", codecForList(codecForFeeDescription()))
+ .property("refund", codecForList(codecForFeeDescription()))
+ .build("DenomOperationMap");
+
+export const codecForExchangeFullDetails = (): Codec<ExchangeFullDetails> =>
+ buildCodecForObject<ExchangeFullDetails>()
+ .property("currency", codecForString())
+ .property("exchangeBaseUrl", codecForCanonBaseUrl())
+ .property("paytoUris", codecForList(codecForString()))
+ .property("auditors", codecForList(codecForExchangeAuditor()))
+ .property("wireInfo", codecForWireInfo())
+ .property("denomFees", codecForFeesByOperations())
+ .property(
+ "transferFees",
+ codecForMap(codecForList(codecForFeeDescription())),
+ )
+ .property("globalFees", codecForList(codecForFeeDescription()))
+ .build("ExchangeFullDetails");
+
+export const codecForExchangeListItem = (): Codec<ExchangeListItem> =>
+ buildCodecForObject<ExchangeListItem>()
+ .property("currency", codecForString())
+ .property("exchangeBaseUrl", codecForCanonBaseUrl())
+ .property("masterPub", codecOptional(codecForString()))
+ .property("paytoUris", codecForList(codecForString()))
+ .property("tosStatus", codecForAny())
+ .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> =>
+ buildCodecForObject<ExchangesListResponse>()
+ .property("exchanges", codecForList(codecForExchangeListItem()))
+ .build("ExchangesListResponse");
+
+export interface AcceptManualWithdrawalResult {
+ /**
+ * Payto URIs that can be used to fund the withdrawal.
+ *
+ * @deprecated in favor of withdrawalAccountsList
+ */
+ exchangePaytoUris: string[];
+
+ /**
+ * Public key of the newly created reserve.
+ */
+ reservePub: string;
+
+ withdrawalAccountsList: WithdrawalExchangeAccountDetails[];
+
+ transactionId: TransactionIdStr;
+}
+
+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;
+
+ /**
+ * Amount that the user will transfer to the exchange.
+ */
+ amountRaw: AmountString;
+
+ /**
+ * Amount that will be added to the user's wallet balance.
+ */
+ 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;
+}
+
+/**
+ * Selected denominations withn some extra info.
+ */
+export interface DenomSelectionState {
+ totalCoinValue: AmountString;
+ totalWithdrawCost: AmountString;
+ selectedDenoms: DenomSelItem[];
+ earliestDepositExpiration: TalerProtocolTimestamp;
+ hasDenomWithAgeRestriction: boolean;
+}
+
+/**
+ * Information about what will happen doing a withdrawal.
+ *
+ * Sent to the wallet frontend to be rendered and shown to the user.
+ */
+export interface ExchangeWithdrawalDetails {
+ exchangePaytoUris: string[];
+
+ /**
+ * Filtered wire info to send to the bank.
+ */
+ exchangeWireAccounts: string[];
+
+ exchangeCreditAccountDetails: WithdrawalExchangeAccountDetails[];
+
+ /**
+ * Selected denominations for withdraw.
+ */
+ selectedDenoms: DenomSelectionState;
+
+ /**
+ * Did the user already accept the current terms of service for the exchange?
+ */
+ termsOfServiceAccepted: boolean;
+
+ /**
+ * The earliest deposit expiration of the selected coins.
+ */
+ earliestDepositExpiration: TalerProtocolTimestamp;
+
+ /**
+ * Result of checking the wallet's version
+ * against the exchange's version.
+ *
+ * Older exchanges don't return version information.
+ */
+ versionMatch: VersionMatchResult | undefined;
+
+ /**
+ * Libtool-style version string for the exchange or "unknown"
+ * for older exchanges.
+ */
+ exchangeVersion: string;
+
+ /**
+ * Libtool-style version string for the wallet.
+ */
+ walletVersion: string;
+
+ /**
+ * Amount that will be subtracted from the reserve's balance.
+ */
+ withdrawalAmountRaw: AmountString;
+
+ /**
+ * Amount that will actually be added to the wallet's balance.
+ */
+ withdrawalAmountEffective: AmountString;
+
+ /**
+ * If the exchange supports age-restricted coins it will return
+ * the array of ages.
+ *
+ */
+ ageRestrictionOptions?: number[];
+
+ scopeInfo: ScopeInfo;
+}
+
+export interface GetExchangeTosResult {
+ /**
+ * Markdown version of the current ToS.
+ */
+ content: string;
+
+ /**
+ * Version tag of the current ToS.
+ */
+ currentEtag: string;
+
+ /**
+ * Version tag of the last ToS that the user has accepted,
+ * if any.
+ */
+ acceptedEtag: string | undefined;
+
+ /**
+ * Accepted content type
+ */
+ 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: AmountString;
+ summary: string;
+ forcedCoinSel?: ForcedCoinSel;
+}
+
+export const codecForTestPayArgs = (): Codec<TestPayArgs> =>
+ buildCodecForObject<TestPayArgs>()
+ .property("merchantBaseUrl", codecForCanonBaseUrl())
+ .property("merchantAuthToken", codecOptional(codecForString()))
+ .property("amount", codecForAmountString())
+ .property("summary", codecForString())
+ .property("forcedCoinSel", codecForAny())
+ .build("TestPayArgs");
+
+export interface IntegrationTestArgs {
+ exchangeBaseUrl: string;
+ corebankApiBaseUrl: string;
+ merchantBaseUrl: string;
+ merchantAuthToken?: string;
+ amountToWithdraw: AmountString;
+ amountToSpend: AmountString;
+}
+
+export const codecForIntegrationTestArgs = (): Codec<IntegrationTestArgs> =>
+ buildCodecForObject<IntegrationTestArgs>()
+ .property("exchangeBaseUrl", codecForCanonBaseUrl())
+ .property("merchantBaseUrl", codecForCanonBaseUrl())
+ .property("merchantAuthToken", codecOptional(codecForString()))
+ .property("amountToSpend", codecForAmountString())
+ .property("amountToWithdraw", 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;
+
+ masterPub?: string;
+}
+
+export const codecForAddExchangeRequest = (): Codec<AddExchangeRequest> =>
+ buildCodecForObject<AddExchangeRequest>()
+ .property("exchangeBaseUrl", codecForCanonBaseUrl())
+ .property("forceUpdate", codecOptional(codecForBoolean()))
+ .property("masterPub", codecOptional(codecForString()))
+ .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;
+}
+
+export const codecForForceExchangeUpdateRequest =
+ (): Codec<AddExchangeRequest> =>
+ buildCodecForObject<AddExchangeRequest>()
+ .property("exchangeBaseUrl", codecForCanonBaseUrl())
+ .build("AddExchangeRequest");
+
+export interface GetExchangeTosRequest {
+ exchangeBaseUrl: string;
+ acceptedFormat?: string[];
+ acceptLanguage?: string;
+}
+
+export const codecForGetExchangeTosRequest = (): Codec<GetExchangeTosRequest> =>
+ buildCodecForObject<GetExchangeTosRequest>()
+ .property("exchangeBaseUrl", codecForCanonBaseUrl())
+ .property("acceptedFormat", codecOptional(codecForList(codecForString())))
+ .property("acceptLanguage", codecOptional(codecForString()))
+ .build("GetExchangeTosRequest");
+
+export interface AcceptManualWithdrawalRequest {
+ exchangeBaseUrl: 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 codecForAcceptManualWithdrawalRequest =
+ (): Codec<AcceptManualWithdrawalRequest> =>
+ buildCodecForObject<AcceptManualWithdrawalRequest>()
+ .property("exchangeBaseUrl", codecForCanonBaseUrl())
+ .property("amount", codecForAmountString())
+ .property("restrictAge", codecOptional(codecForNumber()))
+ .property("forceReservePriv", codecOptional(codecForString()))
+ .build("AcceptManualWithdrawalRequest");
+
+export interface GetWithdrawalDetailsForAmountRequest {
+ exchangeBaseUrl: 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;
+ exchangeBaseUrl: string;
+ forcedDenomSel?: ForcedDenomSel;
+ restrictAge?: number;
+}
+
+export const codecForPrepareBankIntegratedWithdrawalRequest =
+ (): Codec<PrepareBankIntegratedWithdrawalRequest> =>
+ buildCodecForObject<PrepareBankIntegratedWithdrawalRequest>()
+ .property("exchangeBaseUrl", codecForCanonBaseUrl())
+ .property("talerWithdrawUri", codecForString())
+ .property("forcedDenomSel", codecForAny())
+ .property("restrictAge", codecOptional(codecForNumber()))
+ .build("PrepareBankIntegratedWithdrawalRequest");
+
+export interface PrepareBankIntegratedWithdrawalResponse {
+ transactionId: string;
+}
+
+export interface ConfirmWithdrawalRequest {
+ transactionId: string;
+}
+
+export const codecForConfirmWithdrawalRequestRequest =
+ (): Codec<ConfirmWithdrawalRequest> =>
+ buildCodecForObject<ConfirmWithdrawalRequest>()
+ .property("transactionId", codecForString())
+ .build("ConfirmWithdrawalRequest");
+
+export interface AcceptBankIntegratedWithdrawalRequest {
+ talerWithdrawUri: string;
+ exchangeBaseUrl: string;
+ forcedDenomSel?: ForcedDenomSel;
+ restrictAge?: number;
+}
+
+export const codecForAcceptBankIntegratedWithdrawalRequest =
+ (): Codec<AcceptBankIntegratedWithdrawalRequest> =>
+ buildCodecForObject<AcceptBankIntegratedWithdrawalRequest>()
+ .property("exchangeBaseUrl", codecForCanonBaseUrl())
+ .property("talerWithdrawUri", codecForString())
+ .property("forcedDenomSel", codecForAny())
+ .property("restrictAge", codecOptional(codecForNumber()))
+ .build("AcceptBankIntegratedWithdrawalRequest");
+
+export const codecForGetWithdrawalDetailsForAmountRequest =
+ (): Codec<GetWithdrawalDetailsForAmountRequest> =>
+ buildCodecForObject<GetWithdrawalDetailsForAmountRequest>()
+ .property("exchangeBaseUrl", codecForCanonBaseUrl())
+ .property("amount", codecForAmountString())
+ .property("restrictAge", codecOptional(codecForNumber()))
+ .property("clientCancellationId", codecOptional(codecForString()))
+ .build("GetWithdrawalDetailsForAmountRequest");
+
+export interface AcceptExchangeTosRequest {
+ exchangeBaseUrl: string;
+}
+
+export const codecForAcceptExchangeTosRequest =
+ (): Codec<AcceptExchangeTosRequest> =>
+ buildCodecForObject<AcceptExchangeTosRequest>()
+ .property("exchangeBaseUrl", codecForCanonBaseUrl())
+ .build("AcceptExchangeTosRequest");
+
+export interface ForgetExchangeTosRequest {
+ exchangeBaseUrl: string;
+}
+
+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;
+}
+
+export const codecForApplyRefundFromPurchaseIdRequest =
+ (): Codec<ApplyRefundFromPurchaseIdRequest> =>
+ buildCodecForObject<ApplyRefundFromPurchaseIdRequest>()
+ .property("purchaseId", codecForString())
+ .build("ApplyRefundFromPurchaseIdRequest");
+
+export interface GetWithdrawalDetailsForUriRequest {
+ talerWithdrawUri: string;
+ restrictAge?: number;
+}
+
+export const codecForGetWithdrawalDetailsForUri =
+ (): Codec<GetWithdrawalDetailsForUriRequest> =>
+ buildCodecForObject<GetWithdrawalDetailsForUriRequest>()
+ .property("talerWithdrawUri", codecForString())
+ .property("restrictAge", codecOptional(codecForNumber()))
+ .build("GetWithdrawalDetailsForUriRequest");
+
+export interface ListKnownBankAccountsRequest {
+ currency?: string;
+}
+
+export const codecForListKnownBankAccounts =
+ (): Codec<ListKnownBankAccountsRequest> =>
+ buildCodecForObject<ListKnownBankAccountsRequest>()
+ .property("currency", codecOptional(codecForString()))
+ .build("ListKnownBankAccountsRequest");
+
+export interface AddKnownBankAccountsRequest {
+ payto: string;
+ alias: string;
+ currency: string;
+}
+export const codecForAddKnownBankAccounts =
+ (): Codec<AddKnownBankAccountsRequest> =>
+ buildCodecForObject<AddKnownBankAccountsRequest>()
+ .property("payto", codecForString())
+ .property("alias", codecForString())
+ .property("currency", codecForString())
+ .build("AddKnownBankAccountsRequest");
+
+export interface ForgetKnownBankAccountsRequest {
+ payto: string;
+}
+
+export const codecForForgetKnownBankAccounts =
+ (): Codec<ForgetKnownBankAccountsRequest> =>
+ buildCodecForObject<ForgetKnownBankAccountsRequest>()
+ .property("payto", codecForString())
+ .build("ForgetKnownBankAccountsRequest");
+
+export interface AbortProposalRequest {
+ proposalId: string;
+}
+
+export const codecForAbortProposalRequest = (): Codec<AbortProposalRequest> =>
+ buildCodecForObject<AbortProposalRequest>()
+ .property("proposalId", codecForString())
+ .build("AbortProposalRequest");
+
+export interface GetContractTermsDetailsRequest {
+ // @deprecated use transaction id
+ proposalId?: string;
+ transactionId?: string;
+}
+
+export const codecForGetContractTermsDetails =
+ (): Codec<GetContractTermsDetailsRequest> =>
+ buildCodecForObject<GetContractTermsDetailsRequest>()
+ .property("proposalId", codecOptional(codecForString()))
+ .property("transactionId", codecOptional(codecForString()))
+ .build("GetContractTermsDetails");
+
+export interface PreparePayRequest {
+ talerPayUri: string;
+}
+
+export const codecForPreparePayRequest = (): Codec<PreparePayRequest> =>
+ buildCodecForObject<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 PreparePayTemplateRequest {
+ talerPayTemplateUri: string;
+ templateParams?: TemplateParams;
+}
+
+export const codecForPreparePayTemplateRequest =
+ (): Codec<PreparePayTemplateRequest> =>
+ buildCodecForObject<PreparePayTemplateRequest>()
+ .property("talerPayTemplateUri", codecForString())
+ .property("templateParams", codecForAny())
+ .build("PreparePayTemplate");
+
+export interface ConfirmPayRequest {
+ /**
+ * @deprecated use transactionId instead
+ */
+ proposalId?: string;
+ transactionId?: TransactionIdStr;
+ sessionId?: string;
+ forcedCoinSel?: ForcedCoinSel;
+}
+
+export const codecForConfirmPayRequest = (): Codec<ConfirmPayRequest> =>
+ buildCodecForObject<ConfirmPayRequest>()
+ .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 CoreApiMessageEnvelope = CoreApiResponse | CoreApiNotification;
+
+export interface CoreApiNotification {
+ type: "notification";
+ payload: unknown;
+}
+
+export interface CoreApiResponseSuccess {
+ // To distinguish the message from notifications
+ type: "response";
+ operation: string;
+ id: string;
+ result: unknown;
+}
+
+export interface CoreApiResponseError {
+ // To distinguish the message from notifications
+ type: "error";
+ operation: string;
+ id: string;
+ error: TalerErrorDetail;
+}
+
+export interface WithdrawTestBalanceRequest {
+ amount: AmountString;
+ /**
+ * Corebank API base URL.
+ */
+ corebankApiBaseUrl: string;
+ exchangeBaseUrl: string;
+ forcedDenomSel?: ForcedDenomSel;
+}
+
+/**
+ * Request to the crypto worker to make a sync signature.
+ */
+export interface MakeSyncSignatureRequest {
+ accountPriv: string;
+ oldHash: string | undefined;
+ newHash: string;
+}
+
+/**
+ * Planchet for a coin during refresh.
+ */
+export interface RefreshPlanchetInfo {
+ /**
+ * Public key for the coin.
+ */
+ coinPub: string;
+
+ /**
+ * Private key for the coin.
+ */
+ coinPriv: string;
+
+ /**
+ * Blinded public key.
+ */
+ coinEv: CoinEnvelope;
+
+ coinEvHash: string;
+
+ /**
+ * Blinding key used.
+ */
+ blindingKey: string;
+
+ maxAge: number;
+ ageCommitmentProof?: AgeCommitmentProof;
+}
+
+/**
+ * Strategy for loading recovery information.
+ */
+export enum RecoveryMergeStrategy {
+ /**
+ * Keep the local wallet root key, import and take over providers.
+ */
+ Ours = "ours",
+
+ /**
+ * Migrate to the wallet root key from the recovery information.
+ */
+ Theirs = "theirs",
+}
+
+/**
+ * Load recovery information into the wallet.
+ */
+export interface RecoveryLoadRequest {
+ recovery: BackupRecovery;
+ strategy?: RecoveryMergeStrategy;
+}
+
+export const codecForWithdrawTestBalance =
+ (): Codec<WithdrawTestBalanceRequest> =>
+ buildCodecForObject<WithdrawTestBalanceRequest>()
+ .property("amount", codecForAmountString())
+ .property("exchangeBaseUrl", codecForCanonBaseUrl())
+ .property("forcedDenomSel", codecForAny())
+ .property("corebankApiBaseUrl", codecForCanonBaseUrl())
+ .build("WithdrawTestBalanceRequest");
+
+export interface SetCoinSuspendedRequest {
+ coinPub: string;
+ suspended: boolean;
+}
+
+export const codecForSetCoinSuspendedRequest =
+ (): Codec<SetCoinSuspendedRequest> =>
+ buildCodecForObject<SetCoinSuspendedRequest>()
+ .property("coinPub", codecForString())
+ .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 {
+ refreshCoinSpecs: RefreshCoinSpec[];
+}
+
+export const codecForForceRefreshRequest = (): Codec<ForceRefreshRequest> =>
+ buildCodecForObject<ForceRefreshRequest>()
+ .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 StartRefundQueryRequest {
+ transactionId: TransactionIdStr;
+}
+
+export const codecForStartRefundQueryRequest =
+ (): Codec<StartRefundQueryRequest> =>
+ buildCodecForObject<StartRefundQueryRequest>()
+ .property("transactionId", codecForTransactionIdStr())
+ .build("StartRefundQueryRequest");
+
+export interface FailTransactionRequest {
+ transactionId: TransactionIdStr;
+}
+
+export const codecForFailTransactionRequest =
+ (): Codec<FailTransactionRequest> =>
+ buildCodecForObject<FailTransactionRequest>()
+ .property("transactionId", codecForTransactionIdStr())
+ .build("FailTransactionRequest");
+
+export interface SuspendTransactionRequest {
+ transactionId: TransactionIdStr;
+}
+
+export const codecForSuspendTransaction =
+ (): Codec<SuspendTransactionRequest> =>
+ buildCodecForObject<AbortTransactionRequest>()
+ .property("transactionId", codecForTransactionIdStr())
+ .build("SuspendTransactionRequest");
+
+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;
+ refresh: AmountString;
+}
+
+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 interface PrepareDepositRequest {
+ depositPaytoUri: string;
+ amount: AmountString;
+}
+export const codecForPrepareDepositRequest = (): Codec<PrepareDepositRequest> =>
+ buildCodecForObject<PrepareDepositRequest>()
+ .property("amount", codecForAmountString())
+ .property("depositPaytoUri", codecForString())
+ .build("PrepareDepositRequest");
+
+export interface PrepareDepositResponse {
+ totalDepositCost: AmountString;
+ effectiveDepositAmount: AmountString;
+ fees: DepositGroupFees;
+}
+
+export const codecForCreateDepositGroupRequest =
+ (): Codec<CreateDepositGroupRequest> =>
+ buildCodecForObject<CreateDepositGroupRequest>()
+ .property("amount", codecForAmountString())
+ .property("depositPaytoUri", codecForString())
+ .property("transactionId", codecOptional(codecForTransactionIdStr()))
+ .build("CreateDepositGroupRequest");
+
+export interface CreateDepositGroupResponse {
+ depositGroupId: string;
+ transactionId: TransactionIdStr;
+}
+
+export interface TxIdResponse {
+ transactionId: TransactionIdStr;
+}
+
+export interface WithdrawUriInfoResponse {
+ operationId: string;
+ status: WithdrawalOperationStatus;
+ confirmTransferUrl?: string;
+ amount: AmountString;
+ defaultExchangeBaseUrl?: string;
+ possibleExchanges: ExchangeListItem[];
+}
+
+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(codecForCanonBaseUrl()))
+ .property("possibleExchanges", codecForList(codecForExchangeListItem()))
+ .build("WithdrawUriInfoResponse");
+
+export interface WalletCurrencyInfo {
+ trustedAuditors: {
+ currency: string;
+ auditorPub: string;
+ auditorBaseUrl: string;
+ }[];
+ trustedExchanges: {
+ currency: string;
+ exchangeMasterPub: string;
+ exchangeBaseUrl: string;
+ }[];
+}
+
+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: TransactionIdStr;
+}
+
+export interface RetryTransactionRequest {
+ transactionId: TransactionIdStr;
+}
+
+export const codecForDeleteTransactionRequest =
+ (): Codec<DeleteTransactionRequest> =>
+ buildCodecForObject<DeleteTransactionRequest>()
+ .property("transactionId", codecForTransactionIdStr())
+ .build("DeleteTransactionRequest");
+
+export const codecForRetryTransactionRequest =
+ (): Codec<RetryTransactionRequest> =>
+ buildCodecForObject<RetryTransactionRequest>()
+ .property("transactionId", codecForTransactionIdStr())
+ .build("RetryTransactionRequest");
+
+export interface SetWalletDeviceIdRequest {
+ /**
+ * New wallet device ID to set.
+ */
+ walletDeviceId: string;
+}
+
+export const codecForSetWalletDeviceIdRequest =
+ (): Codec<SetWalletDeviceIdRequest> =>
+ buildCodecForObject<SetWalletDeviceIdRequest>()
+ .property("walletDeviceId", codecForString())
+ .build("SetWalletDeviceIdRequest");
+
+export interface WithdrawFakebankRequest {
+ amount: AmountString;
+ exchange: string;
+ bank: string;
+}
+
+export enum AttentionPriority {
+ High = "high",
+ Medium = "medium",
+ Low = "low",
+}
+
+export interface UserAttentionByIdRequest {
+ entityId: string;
+ type: AttentionType;
+}
+
+export const codecForUserAttentionByIdRequest =
+ (): Codec<UserAttentionByIdRequest> =>
+ buildCodecForObject<UserAttentionByIdRequest>()
+ .property("type", codecForAny())
+ .property("entityId", codecForString())
+ .build("UserAttentionByIdRequest");
+
+export const codecForUserAttentionsRequest = (): Codec<UserAttentionsRequest> =>
+ buildCodecForObject<UserAttentionsRequest>()
+ .property(
+ "priority",
+ codecOptional(
+ codecForEither(
+ codecForConstString(AttentionPriority.Low),
+ codecForConstString(AttentionPriority.Medium),
+ codecForConstString(AttentionPriority.High),
+ ),
+ ),
+ )
+ .build("UserAttentionsRequest");
+
+export interface UserAttentionsRequest {
+ priority?: AttentionPriority;
+}
+
+export type AttentionInfo =
+ | AttentionKycWithdrawal
+ | AttentionBackupUnpaid
+ | AttentionBackupExpiresSoon
+ | AttentionMerchantRefund
+ | AttentionExchangeTosChanged
+ | AttentionExchangeKeyExpired
+ | AttentionExchangeDenominationExpired
+ | AttentionAuditorTosChanged
+ | AttentionAuditorKeyExpires
+ | AttentionAuditorDenominationExpires
+ | AttentionPullPaymentPaid
+ | AttentionPushPaymentReceived;
+
+export enum AttentionType {
+ KycWithdrawal = "kyc-withdrawal",
+
+ BackupUnpaid = "backup-unpaid",
+ BackupExpiresSoon = "backup-expires-soon",
+ MerchantRefund = "merchant-refund",
+
+ ExchangeTosChanged = "exchange-tos-changed",
+ ExchangeKeyExpired = "exchange-key-expired",
+ ExchangeKeyExpiresSoon = "exchange-key-expires-soon",
+ ExchangeDenominationsExpired = "exchange-denominations-expired",
+ ExchangeDenominationsExpiresSoon = "exchange-denominations-expires-soon",
+
+ AuditorTosChanged = "auditor-tos-changed",
+ AuditorKeyExpires = "auditor-key-expires",
+ AuditorDenominationsExpires = "auditor-denominations-expires",
+
+ PullPaymentPaid = "pull-payment-paid",
+ PushPaymentReceived = "push-payment-withdrawn",
+}
+
+export const UserAttentionPriority: {
+ [type in AttentionType]: AttentionPriority;
+} = {
+ "kyc-withdrawal": AttentionPriority.Medium,
+
+ "backup-unpaid": AttentionPriority.High,
+ "backup-expires-soon": AttentionPriority.Medium,
+ "merchant-refund": AttentionPriority.Medium,
+
+ "exchange-tos-changed": AttentionPriority.Medium,
+
+ "exchange-key-expired": AttentionPriority.High,
+ "exchange-key-expires-soon": AttentionPriority.Medium,
+ "exchange-denominations-expired": AttentionPriority.High,
+ "exchange-denominations-expires-soon": AttentionPriority.Medium,
+
+ "auditor-tos-changed": AttentionPriority.Medium,
+ "auditor-key-expires": AttentionPriority.Medium,
+ "auditor-denominations-expires": AttentionPriority.Medium,
+
+ "pull-payment-paid": AttentionPriority.High,
+ "push-payment-withdrawn": AttentionPriority.High,
+};
+
+interface AttentionBackupExpiresSoon {
+ type: AttentionType.BackupExpiresSoon;
+ provider_base_url: string;
+}
+interface AttentionBackupUnpaid {
+ type: AttentionType.BackupUnpaid;
+ provider_base_url: string;
+ talerUri: string;
+}
+
+interface AttentionMerchantRefund {
+ type: AttentionType.MerchantRefund;
+ transactionId: TransactionIdStr;
+}
+
+interface AttentionKycWithdrawal {
+ type: AttentionType.KycWithdrawal;
+ transactionId: TransactionIdStr;
+}
+
+interface AttentionExchangeTosChanged {
+ type: AttentionType.ExchangeTosChanged;
+ exchange_base_url: string;
+}
+interface AttentionExchangeKeyExpired {
+ type: AttentionType.ExchangeKeyExpired;
+ exchange_base_url: string;
+}
+interface AttentionExchangeDenominationExpired {
+ type: AttentionType.ExchangeDenominationsExpired;
+ exchange_base_url: string;
+}
+interface AttentionAuditorTosChanged {
+ type: AttentionType.AuditorTosChanged;
+ auditor_base_url: string;
+}
+
+interface AttentionAuditorKeyExpires {
+ type: AttentionType.AuditorKeyExpires;
+ auditor_base_url: string;
+}
+interface AttentionAuditorDenominationExpires {
+ type: AttentionType.AuditorDenominationsExpires;
+ auditor_base_url: string;
+}
+interface AttentionPullPaymentPaid {
+ type: AttentionType.PullPaymentPaid;
+ transactionId: TransactionIdStr;
+}
+
+interface AttentionPushPaymentReceived {
+ type: AttentionType.PushPaymentReceived;
+ transactionId: TransactionIdStr;
+}
+
+export type UserAttentionUnreadList = Array<{
+ info: AttentionInfo;
+ when: TalerPreciseTimestamp;
+ read: boolean;
+}>;
+
+export interface UserAttentionsResponse {
+ pending: UserAttentionUnreadList;
+}
+
+export interface UserAttentionsCountResponse {
+ total: number;
+}
+
+export const codecForWithdrawFakebankRequest =
+ (): Codec<WithdrawFakebankRequest> =>
+ buildCodecForObject<WithdrawFakebankRequest>()
+ .property("amount", codecForAmountString())
+ .property("bank", codecForString())
+ .property("exchange", codecForString())
+ .build("WithdrawFakebankRequest");
+
+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<ImportDbRequest> =>
+ buildCodecForObject<ImportDbRequest>()
+ .property("dump", codecForAny())
+ .build("ImportDbRequest");
+
+export interface ForcedDenomSel {
+ denoms: {
+ value: AmountString;
+ count: number;
+ }[];
+}
+
+/**
+ * Forced coin selection for deposits/payments.
+ */
+export interface ForcedCoinSel {
+ coins: {
+ value: AmountString;
+ contribution: AmountString;
+ }[];
+}
+
+export interface TestPayResult {
+ /**
+ * 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;
+}
+
+/**
+ * Result of selecting coins, contains the exchange, and selected
+ * coins with their denomination.
+ */
+export interface PayCoinSelection {
+ coins: SelectedCoin[];
+
+ /**
+ * How much of the wire fees is the customer paying?
+ */
+ customerWireFees: AmountString;
+
+ /**
+ * How much of the deposit fees is the customer paying?
+ */
+ customerDepositFees: AmountString;
+}
+
+export interface ProspectivePayCoinSelection {
+ prospectiveCoins: SelectedProspectiveCoin[];
+
+ /**
+ * How much of the wire fees is the customer paying?
+ */
+ customerWireFees: AmountString;
+
+ /**
+ * How much of the deposit fees is the customer paying?
+ */
+ customerDepositFees: AmountString;
+}
+
+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 codecForCheckPeerPushDebitRequest =
+ (): Codec<CheckPeerPushDebitRequest> =>
+ buildCodecForObject<CheckPeerPushDebitRequest>()
+ .property("exchangeBaseUrl", codecOptional(codecForCanonBaseUrl()))
+ .property("amount", codecForAmountString())
+ .build("CheckPeerPushDebitRequest");
+
+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 InitiatePeerPushDebitRequest {
+ exchangeBaseUrl?: string;
+ partialContractTerms: PeerContractTerms;
+}
+
+export interface InitiatePeerPushDebitResponse {
+ exchangeBaseUrl: string;
+ pursePub: string;
+ mergePriv: string;
+ contractPriv: string;
+ transactionId: TransactionIdStr;
+}
+
+export const codecForInitiatePeerPushDebitRequest =
+ (): Codec<InitiatePeerPushDebitRequest> =>
+ buildCodecForObject<InitiatePeerPushDebitRequest>()
+ .property("partialContractTerms", codecForPeerContractTerms())
+ .build("InitiatePeerPushDebitRequest");
+
+export interface PreparePeerPushCreditRequest {
+ talerUri: string;
+}
+
+export interface PreparePeerPullDebitRequest {
+ talerUri: string;
+}
+
+export interface PreparePeerPushCreditResponse {
+ contractTerms: PeerContractTerms;
+ amountRaw: AmountString;
+ amountEffective: AmountString;
+
+ transactionId: TransactionIdStr;
+
+ exchangeBaseUrl: string;
+
+ /**
+ * @deprecated use transaction ID instead.
+ */
+ peerPushCreditId: string;
+
+ /**
+ * @deprecated
+ */
+ amount: AmountString;
+}
+
+export interface PreparePeerPullDebitResponse {
+ contractTerms: PeerContractTerms;
+ /**
+ * @deprecated Redundant field with bad name, will be removed soon.
+ */
+ amount: AmountString;
+
+ amountRaw: AmountString;
+ amountEffective: AmountString;
+
+ peerPullDebitId: string;
+
+ transactionId: TransactionIdStr;
+}
+
+export const codecForPreparePeerPushCreditRequest =
+ (): Codec<PreparePeerPushCreditRequest> =>
+ buildCodecForObject<PreparePeerPushCreditRequest>()
+ .property("talerUri", codecForString())
+ .build("CheckPeerPushPaymentRequest");
+
+export const codecForCheckPeerPullPaymentRequest =
+ (): Codec<PreparePeerPullDebitRequest> =>
+ buildCodecForObject<PreparePeerPullDebitRequest>()
+ .property("talerUri", codecForString())
+ .build("PreparePeerPullDebitRequest");
+
+export interface ConfirmPeerPushCreditRequest {
+ transactionId: string;
+}
+export interface AcceptPeerPushPaymentResponse {
+ transactionId: TransactionIdStr;
+}
+
+export interface AcceptPeerPullPaymentResponse {
+ transactionId: TransactionIdStr;
+}
+
+export const codecForConfirmPeerPushPaymentRequest =
+ (): Codec<ConfirmPeerPushCreditRequest> =>
+ buildCodecForObject<ConfirmPeerPushCreditRequest>()
+ .property("transactionId", codecForString())
+ .build("ConfirmPeerPushCreditRequest");
+
+export interface ConfirmPeerPullDebitRequest {
+ transactionId: TransactionIdStr;
+}
+
+export interface ApplyDevExperimentRequest {
+ devExperimentUri: string;
+}
+
+export const codecForApplyDevExperiment =
+ (): Codec<ApplyDevExperimentRequest> =>
+ buildCodecForObject<ApplyDevExperimentRequest>()
+ .property("devExperimentUri", codecForString())
+ .build("ApplyDevExperimentRequest");
+
+export const codecForAcceptPeerPullPaymentRequest =
+ (): Codec<ConfirmPeerPullDebitRequest> =>
+ buildCodecForObject<ConfirmPeerPullDebitRequest>()
+ .property("transactionId", codecForTransactionIdStr())
+ .build("ConfirmPeerPullDebitRequest");
+
+export interface CheckPeerPullCreditRequest {
+ exchangeBaseUrl?: string;
+ amount: AmountString;
+}
+export const codecForPreparePeerPullPaymentRequest =
+ (): Codec<CheckPeerPullCreditRequest> =>
+ buildCodecForObject<CheckPeerPullCreditRequest>()
+ .property("amount", codecForAmountString())
+ .property("exchangeBaseUrl", codecOptional(codecForCanonBaseUrl()))
+ .build("CheckPeerPullCreditRequest");
+
+export interface CheckPeerPullCreditResponse {
+ exchangeBaseUrl: string;
+ amountRaw: AmountString;
+ amountEffective: AmountString;
+
+ /**
+ * Number of coins that will be used,
+ * can be used by the UI to warn if excessively large.
+ */
+ numCoins: number;
+}
+export interface InitiatePeerPullCreditRequest {
+ exchangeBaseUrl?: string;
+ partialContractTerms: PeerContractTerms;
+}
+
+export const codecForInitiatePeerPullPaymentRequest =
+ (): Codec<InitiatePeerPullCreditRequest> =>
+ buildCodecForObject<InitiatePeerPullCreditRequest>()
+ .property("partialContractTerms", codecForPeerContractTerms())
+ .property("exchangeBaseUrl", codecOptional(codecForCanonBaseUrl()))
+ .build("InitiatePeerPullCreditRequest");
+
+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: 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 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");
diff --git a/packages/taler-util/src/walletTypes.ts b/packages/taler-util/src/walletTypes.ts
deleted file mode 100644
index a8946fbbb..000000000
--- a/packages/taler-util/src/walletTypes.ts
+++ /dev/null
@@ -1,1207 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2015-2020 Taler Systems SA
-
- 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/>
- */
-
-/**
- * Types used by clients of the wallet.
- *
- * These types are defined in a separate file make tree shaking easier, since
- * some components use these types (via RPC) but do not depend on the wallet
- * code directly.
- *
- * @author Florian Dold <dold@taler.net>
- */
-
-/**
- * Imports.
- */
-import {
- AmountJson,
- codecForAmountJson,
- codecForAmountString,
-} from "./amounts.js";
-import {
- AbsoluteTime,
- codecForTimestamp,
- TalerProtocolTimestamp,
-} from "./time.js";
-import {
- buildCodecForObject,
- codecForString,
- codecOptional,
- Codec,
- codecForList,
- codecForBoolean,
- codecForConstString,
- codecForAny,
- buildCodecForUnion,
- codecForNumber,
-} from "./codec.js";
-import {
- AmountString,
- codecForContractTerms,
- CoinEnvelope,
- ContractTerms,
- DenominationPubKey,
- DenomKeyType,
- UnblindedSignature,
-} from "./talerTypes.js";
-import { OrderShortInfo, codecForOrderShortInfo } from "./transactionsTypes.js";
-import { BackupRecovery } from "./backupTypes.js";
-import { PaytoUri } from "./payto.js";
-import { TalerErrorCode } from "./taler-error-codes.js";
-import { AgeCommitmentProof } from "./talerCrypto.js";
-
-/**
- * Response for the create reserve request to the wallet.
- */
-export class CreateReserveResponse {
- /**
- * Exchange URL where the bank should create the reserve.
- * The URL is canonicalized in the response.
- */
- exchange: string;
-
- /**
- * Reserve public key of the newly created reserve.
- */
- reservePub: string;
-}
-
-export interface Balance {
- available: AmountString;
- pendingIncoming: AmountString;
- pendingOutgoing: AmountString;
-
- // Does the balance for this currency have a pending
- // transaction?
- hasPendingTransactions: boolean;
-
- // Is there a pending transaction that would affect the balance
- // and requires user input?
- requiresUserInput: boolean;
-}
-
-export interface BalancesResponse {
- balances: Balance[];
-}
-
-export const codecForBalance = (): Codec<Balance> =>
- buildCodecForObject<Balance>()
- .property("available", codecForString())
- .property("hasPendingTransactions", codecForBoolean())
- .property("pendingIncoming", codecForString())
- .property("pendingOutgoing", codecForString())
- .property("requiresUserInput", codecForBoolean())
- .build("Balance");
-
-export const codecForBalancesResponse = (): Codec<BalancesResponse> =>
- buildCodecForObject<BalancesResponse>()
- .property("balances", codecForList(codecForBalance()))
- .build("BalancesResponse");
-
-/**
- * For terseness.
- */
-export function mkAmount(
- value: number,
- fraction: number,
- currency: string,
-): AmountJson {
- return { value, fraction, currency };
-}
-
-export enum ConfirmPayResultType {
- Done = "done",
- Pending = "pending",
-}
-
-/**
- * Result for confirmPay
- */
-export interface ConfirmPayResultDone {
- type: ConfirmPayResultType.Done;
- contractTerms: ContractTerms;
-}
-
-export interface ConfirmPayResultPending {
- type: ConfirmPayResultType.Pending;
-
- lastError: TalerErrorDetail | undefined;
-}
-
-export type ConfirmPayResult = ConfirmPayResultDone | ConfirmPayResultPending;
-
-export const codecForConfirmPayResultPending =
- (): Codec<ConfirmPayResultPending> =>
- buildCodecForObject<ConfirmPayResultPending>()
- .property("lastError", codecForAny())
- .property("type", codecForConstString(ConfirmPayResultType.Pending))
- .build("ConfirmPayResultPending");
-
-export const codecForConfirmPayResultDone = (): Codec<ConfirmPayResultDone> =>
- buildCodecForObject<ConfirmPayResultDone>()
- .property("type", codecForConstString(ConfirmPayResultType.Done))
- .property("contractTerms", codecForContractTerms())
- .build("ConfirmPayResultDone");
-
-export const codecForConfirmPayResult = (): Codec<ConfirmPayResult> =>
- buildCodecForUnion<ConfirmPayResult>()
- .discriminateOn("type")
- .alternative(
- ConfirmPayResultType.Pending,
- codecForConfirmPayResultPending(),
- )
- .alternative(ConfirmPayResultType.Done, codecForConfirmPayResultDone())
- .build("ConfirmPayResult");
-
-/**
- * Information about all sender wire details known to the wallet,
- * as well as exchanges that accept these wire types.
- */
-export interface SenderWireInfos {
- /**
- * Mapping from exchange base url to list of accepted
- * wire types.
- */
- exchangeWireTypes: { [exchangeBaseUrl: string]: string[] };
-
- /**
- * Sender wire information stored in the wallet.
- */
- senderWires: string[];
-}
-
-/**
- * Request to create a reserve.
- */
-export interface CreateReserveRequest {
- /**
- * The initial amount for the reserve.
- */
- amount: AmountJson;
-
- /**
- * Exchange URL where the bank should create the reserve.
- */
- exchange: string;
-
- /**
- * Payto URI that identifies the exchange's account that the funds
- * for this reserve go into.
- */
- exchangePaytoUri?: string;
-
- /**
- * Wire details (as a payto URI) for the bank account that sent the funds to
- * the exchange.
- */
- senderWire?: string;
-
- /**
- * URL to fetch the withdraw status from the bank.
- */
- bankWithdrawStatusUrl?: string;
-
- /**
- * Forced denomination selection for the first withdrawal
- * from this reserve, only used for testing.
- */
- forcedDenomSel?: ForcedDenomSel;
-
- restrictAge?: number;
-}
-
-export const codecForCreateReserveRequest = (): Codec<CreateReserveRequest> =>
- buildCodecForObject<CreateReserveRequest>()
- .property("amount", codecForAmountJson())
- .property("exchange", codecForString())
- .property("exchangePaytoUri", codecForString())
- .property("senderWire", codecOptional(codecForString()))
- .property("bankWithdrawStatusUrl", codecOptional(codecForString()))
- .build("CreateReserveRequest");
-
-/**
- * Request to mark a reserve as confirmed.
- */
-export interface ConfirmReserveRequest {
- /**
- * Public key of then reserve that should be marked
- * as confirmed.
- */
- reservePub: string;
-}
-
-export const codecForConfirmReserveRequest = (): Codec<ConfirmReserveRequest> =>
- buildCodecForObject<ConfirmReserveRequest>()
- .property("reservePub", codecForString())
- .build("ConfirmReserveRequest");
-
-/**
- * Wire coins to the user's own bank account.
- */
-export class ReturnCoinsRequest {
- /**
- * The amount to wire.
- */
- amount: AmountJson;
-
- /**
- * The exchange to take the coins from.
- */
- exchange: string;
-
- /**
- * Wire details for the bank account of the customer that will
- * receive the funds.
- */
- senderWire?: string;
-
- /**
- * Verify that a value matches the schema of this class and convert it into a
- * member.
- */
- static checked: (obj: any) => ReturnCoinsRequest;
-}
-
-export interface PrepareRefundResult {
- proposalId: string;
-
- applied: number;
- failed: number;
- total: number;
-
- amountEffectivePaid: AmountString;
-
- 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 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;
-}
-
-export enum PreparePayResultType {
- PaymentPossible = "payment-possible",
- InsufficientBalance = "insufficient-balance",
- AlreadyConfirmed = "already-confirmed",
-}
-
-export const codecForPreparePayResultPaymentPossible =
- (): Codec<PreparePayResultPaymentPossible> =>
- buildCodecForObject<PreparePayResultPaymentPossible>()
- .property("amountEffective", codecForAmountString())
- .property("amountRaw", codecForAmountString())
- .property("contractTerms", codecForContractTerms())
- .property("proposalId", codecForString())
- .property("contractTermsHash", codecForString())
- .property("noncePriv", codecForString())
- .property(
- "status",
- codecForConstString(PreparePayResultType.PaymentPossible),
- )
- .build("PreparePayResultPaymentPossible");
-
-export const codecForPreparePayResultInsufficientBalance =
- (): Codec<PreparePayResultInsufficientBalance> =>
- buildCodecForObject<PreparePayResultInsufficientBalance>()
- .property("amountRaw", codecForAmountString())
- .property("contractTerms", codecForAny())
- .property("proposalId", codecForString())
- .property("noncePriv", codecForString())
- .property(
- "status",
- codecForConstString(PreparePayResultType.InsufficientBalance),
- )
- .build("PreparePayResultInsufficientBalance");
-
-export const codecForPreparePayResultAlreadyConfirmed =
- (): Codec<PreparePayResultAlreadyConfirmed> =>
- buildCodecForObject<PreparePayResultAlreadyConfirmed>()
- .property(
- "status",
- codecForConstString(PreparePayResultType.AlreadyConfirmed),
- )
- .property("amountEffective", codecForAmountString())
- .property("amountRaw", codecForAmountString())
- .property("paid", codecForBoolean())
- .property("contractTerms", codecForAny())
- .property("contractTermsHash", codecForString())
- .property("proposalId", codecForString())
- .build("PreparePayResultAlreadyConfirmed");
-
-export const codecForPreparePayResult = (): Codec<PreparePayResult> =>
- buildCodecForUnion<PreparePayResult>()
- .discriminateOn("status")
- .alternative(
- PreparePayResultType.AlreadyConfirmed,
- codecForPreparePayResultAlreadyConfirmed(),
- )
- .alternative(
- PreparePayResultType.InsufficientBalance,
- codecForPreparePayResultInsufficientBalance(),
- )
- .alternative(
- PreparePayResultType.PaymentPossible,
- codecForPreparePayResultPaymentPossible(),
- )
- .build("PreparePayResult");
-
-export type PreparePayResult =
- | PreparePayResultInsufficientBalance
- | PreparePayResultAlreadyConfirmed
- | PreparePayResultPaymentPossible;
-
-export interface PreparePayResultPaymentPossible {
- status: PreparePayResultType.PaymentPossible;
- proposalId: string;
- contractTerms: ContractTerms;
- contractTermsHash: string;
- amountRaw: string;
- amountEffective: string;
- noncePriv: string;
-}
-
-export interface PreparePayResultInsufficientBalance {
- status: PreparePayResultType.InsufficientBalance;
- proposalId: string;
- contractTerms: ContractTerms;
- amountRaw: string;
- noncePriv: string;
-}
-
-export interface PreparePayResultAlreadyConfirmed {
- status: PreparePayResultType.AlreadyConfirmed;
- contractTerms: ContractTerms;
- paid: boolean;
- amountRaw: string;
- amountEffective: string;
- contractTermsHash: string;
- proposalId: string;
-}
-
-export interface BankWithdrawDetails {
- selectionDone: boolean;
- transferDone: boolean;
- amount: AmountJson;
- senderWire?: string;
- suggestedExchange?: string;
- confirmTransferUrl?: string;
- wireTypes: string[];
- extractedStatusUrl: string;
-}
-
-export interface AcceptWithdrawalResponse {
- reservePub: string;
- confirmTransferUrl?: string;
-}
-
-/**
- * Details about a purchase, including refund status.
- */
-export interface PurchaseDetails {
- contractTerms: Record<string, undefined>;
- hasRefund: boolean;
- totalRefundAmount: AmountJson;
- totalRefundAndRefreshFees: AmountJson;
-}
-
-export interface WalletDiagnostics {
- walletManifestVersion: string;
- walletManifestDisplayVersion: string;
- errors: string[];
- firefoxIdbProblem: boolean;
- dbOutdated: boolean;
-}
-
-export interface TalerErrorDetail {
- code: TalerErrorCode;
- hint?: string;
- [x: string]: unknown;
-}
-
-/**
- * Minimal information needed about a planchet for unblinding a signature.
- *
- * Can be a withdrawal/tipping/refresh planchet.
- */
-export interface PlanchetUnblindInfo {
- denomPub: DenominationPubKey;
- blindingKey: string;
-}
-
-export interface WithdrawalPlanchet {
- coinPub: string;
- coinPriv: string;
- reservePub: string;
- denomPubHash: string;
- denomPub: DenominationPubKey;
- blindingKey: string;
- withdrawSig: string;
- coinEv: CoinEnvelope;
- coinValue: AmountJson;
- coinEvHash: string;
- ageCommitmentProof?: AgeCommitmentProof;
-}
-
-export interface PlanchetCreationRequest {
- secretSeed: string;
- coinIndex: number;
- value: AmountJson;
- feeWithdraw: AmountJson;
- denomPub: DenominationPubKey;
- reservePub: string;
- reservePriv: string;
- restrictAge?: number;
-}
-
-/**
- * Reasons for why a coin is being refreshed.
- */
-export enum RefreshReason {
- Manual = "manual",
- Pay = "pay",
- Refund = "refund",
- AbortPay = "abort-pay",
- Recoup = "recoup",
- BackupRestored = "backup-restored",
- Scheduled = "scheduled",
-}
-
-/**
- * Wrapper for coin public keys.
- */
-export interface CoinPublicKey {
- readonly coinPub: string;
-}
-
-/**
- * Wrapper for refresh group IDs.
- */
-export interface RefreshGroupId {
- readonly refreshGroupId: string;
-}
-
-/**
- * Private data required to make a deposit permission.
- */
-export interface DepositInfo {
- exchangeBaseUrl: string;
- contractTermsHash: string;
- coinPub: string;
- coinPriv: string;
- spendAmount: AmountJson;
- timestamp: TalerProtocolTimestamp;
- refundDeadline: TalerProtocolTimestamp;
- merchantPub: string;
- feeDeposit: AmountJson;
- wireInfoHash: string;
- denomKeyType: DenomKeyType;
- denomPubHash: string;
- denomSig: UnblindedSignature;
-
- requiredMinimumAge?: number;
-
- ageCommitmentProof?: AgeCommitmentProof;
-}
-
-export interface ExchangesListRespose {
- exchanges: ExchangeListItem[];
-}
-
-export interface KnownBankAccounts {
- accounts: PaytoUri[];
-}
-
-export interface ExchangeTos {
- acceptedVersion?: string;
- currentVersion?: string;
- contentType?: string;
- content?: string;
-}
-export interface ExchangeListItem {
- exchangeBaseUrl: string;
- currency: string;
- paytoUris: string[];
- tos: ExchangeTos;
-}
-
-const codecForExchangeTos = (): Codec<ExchangeTos> =>
- buildCodecForObject<ExchangeTos>()
- .property("acceptedVersion", codecOptional(codecForString()))
- .property("currentVersion", codecOptional(codecForString()))
- .property("contentType", codecOptional(codecForString()))
- .property("content", codecOptional(codecForString()))
- .build("ExchangeTos");
-
-export const codecForExchangeListItem = (): Codec<ExchangeListItem> =>
- buildCodecForObject<ExchangeListItem>()
- .property("currency", codecForString())
- .property("exchangeBaseUrl", codecForString())
- .property("paytoUris", codecForList(codecForString()))
- .property("tos", codecForExchangeTos())
- .build("ExchangeListItem");
-
-export const codecForExchangesListResponse = (): Codec<ExchangesListRespose> =>
- buildCodecForObject<ExchangesListRespose>()
- .property("exchanges", codecForList(codecForExchangeListItem()))
- .build("ExchangesListRespose");
-
-export interface AcceptManualWithdrawalResult {
- /**
- * Payto URIs that can be used to fund the withdrawal.
- */
- exchangePaytoUris: string[];
-
- /**
- * Public key of the newly created reserve.
- */
- reservePub: string;
-}
-
-export interface ManualWithdrawalDetails {
- /**
- * Did the user accept the current version of the exchange's
- * terms of service?
- */
- tosAccepted: boolean;
-
- /**
- * Amount that the user will transfer to the exchange.
- */
- amountRaw: AmountString;
-
- /**
- * Amount that will be added to the user's wallet balance.
- */
- amountEffective: AmountString;
-
- /**
- * Ways to pay the exchange.
- */
- paytoUris: string[];
-}
-
-export interface GetExchangeTosResult {
- /**
- * Markdown version of the current ToS.
- */
- content: string;
-
- /**
- * Version tag of the current ToS.
- */
- currentEtag: string;
-
- /**
- * Version tag of the last ToS that the user has accepted,
- * if any.
- */
- acceptedEtag: string | undefined;
-
- /**
- * Accepted content type
- */
- contentType: string;
-}
-
-export interface TestPayArgs {
- merchantBaseUrl: string;
- merchantAuthToken?: string;
- amount: string;
- summary: string;
-}
-
-export const codecForTestPayArgs = (): Codec<TestPayArgs> =>
- buildCodecForObject<TestPayArgs>()
- .property("merchantBaseUrl", codecForString())
- .property("merchantAuthToken", codecOptional(codecForString()))
- .property("amount", codecForString())
- .property("summary", codecForString())
- .build("TestPayArgs");
-
-export interface IntegrationTestArgs {
- exchangeBaseUrl: string;
- bankBaseUrl: string;
- merchantBaseUrl: string;
- merchantAuthToken?: string;
- amountToWithdraw: string;
- amountToSpend: string;
-}
-
-export const codecForIntegrationTestArgs = (): Codec<IntegrationTestArgs> =>
- buildCodecForObject<IntegrationTestArgs>()
- .property("exchangeBaseUrl", codecForString())
- .property("bankBaseUrl", codecForString())
- .property("merchantBaseUrl", codecForString())
- .property("merchantAuthToken", codecOptional(codecForString()))
- .property("amountToSpend", codecForAmountString())
- .property("amountToWithdraw", codecForAmountString())
- .build("IntegrationTestArgs");
-
-export interface AddExchangeRequest {
- exchangeBaseUrl: string;
- forceUpdate?: boolean;
-}
-
-export const codecForAddExchangeRequest = (): Codec<AddExchangeRequest> =>
- buildCodecForObject<AddExchangeRequest>()
- .property("exchangeBaseUrl", codecForString())
- .property("forceUpdate", codecOptional(codecForBoolean()))
- .build("AddExchangeRequest");
-
-export interface ForceExchangeUpdateRequest {
- exchangeBaseUrl: string;
-}
-
-export const codecForForceExchangeUpdateRequest =
- (): Codec<AddExchangeRequest> =>
- buildCodecForObject<AddExchangeRequest>()
- .property("exchangeBaseUrl", codecForString())
- .build("AddExchangeRequest");
-
-export interface GetExchangeTosRequest {
- exchangeBaseUrl: string;
- acceptedFormat?: string[];
-}
-
-export const codecForGetExchangeTosRequest = (): Codec<GetExchangeTosRequest> =>
- buildCodecForObject<GetExchangeTosRequest>()
- .property("exchangeBaseUrl", codecForString())
- .property("acceptedFormat", codecOptional(codecForList(codecForString())))
- .build("GetExchangeTosRequest");
-
-export interface AcceptManualWithdrawalRequest {
- exchangeBaseUrl: string;
- amount: string;
- restrictAge?: number,
-}
-
-export const codecForAcceptManualWithdrawalRequet =
- (): Codec<AcceptManualWithdrawalRequest> =>
- buildCodecForObject<AcceptManualWithdrawalRequest>()
- .property("exchangeBaseUrl", codecForString())
- .property("amount", codecForString())
- .property("restrictAge", codecOptional(codecForNumber()))
- .build("AcceptManualWithdrawalRequest");
-
-export interface GetWithdrawalDetailsForAmountRequest {
- exchangeBaseUrl: string;
- amount: string;
- restrictAge?: number;
-}
-
-export interface AcceptBankIntegratedWithdrawalRequest {
- talerWithdrawUri: string;
- exchangeBaseUrl: string;
- forcedDenomSel?: ForcedDenomSel;
- restrictAge?: number;
-}
-
-export const codecForAcceptBankIntegratedWithdrawalRequest =
- (): Codec<AcceptBankIntegratedWithdrawalRequest> =>
- buildCodecForObject<AcceptBankIntegratedWithdrawalRequest>()
- .property("exchangeBaseUrl", codecForString())
- .property("talerWithdrawUri", codecForString())
- .property("forcedDenomSel", codecForAny())
- .property("restrictAge", codecOptional(codecForNumber()))
- .build("AcceptBankIntegratedWithdrawalRequest");
-
-export const codecForGetWithdrawalDetailsForAmountRequest =
- (): Codec<GetWithdrawalDetailsForAmountRequest> =>
- buildCodecForObject<GetWithdrawalDetailsForAmountRequest>()
- .property("exchangeBaseUrl", codecForString())
- .property("amount", 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()))
- .build("AcceptExchangeTosRequest");
-
-export interface ApplyRefundRequest {
- talerRefundUri: string;
-}
-
-export const codecForApplyRefundRequest = (): Codec<ApplyRefundRequest> =>
- buildCodecForObject<ApplyRefundRequest>()
- .property("talerRefundUri", codecForString())
- .build("ApplyRefundRequest");
-
-export interface GetWithdrawalDetailsForUriRequest {
- talerWithdrawUri: string;
- restrictAge?: number;
-}
-export const codecForGetWithdrawalDetailsForUri =
- (): Codec<GetWithdrawalDetailsForUriRequest> =>
- buildCodecForObject<GetWithdrawalDetailsForUriRequest>()
- .property("talerWithdrawUri", codecForString())
- .property("restrictAge", codecOptional(codecForNumber()))
- .build("GetWithdrawalDetailsForUriRequest");
-
-export interface ListKnownBankAccountsRequest {
- currency?: string;
-}
-export const codecForListKnownBankAccounts =
- (): Codec<ListKnownBankAccountsRequest> =>
- buildCodecForObject<ListKnownBankAccountsRequest>()
- .property("currency", codecOptional(codecForString()))
- .build("ListKnownBankAccountsRequest");
-
-export interface GetExchangeWithdrawalInfo {
- exchangeBaseUrl: string;
- amount: AmountJson;
- tosAcceptedFormat?: string[];
-}
-
-export const codecForGetExchangeWithdrawalInfo =
- (): Codec<GetExchangeWithdrawalInfo> =>
- buildCodecForObject<GetExchangeWithdrawalInfo>()
- .property("exchangeBaseUrl", codecForString())
- .property("amount", codecForAmountJson())
- .property(
- "tosAcceptedFormat",
- codecOptional(codecForList(codecForString())),
- )
- .build("GetExchangeWithdrawalInfo");
-
-export interface AbortProposalRequest {
- proposalId: string;
-}
-
-export const codecForAbortProposalRequest = (): Codec<AbortProposalRequest> =>
- buildCodecForObject<AbortProposalRequest>()
- .property("proposalId", codecForString())
- .build("AbortProposalRequest");
-
-export interface PreparePayRequest {
- talerPayUri: string;
-}
-
-export const codecForPreparePayRequest = (): Codec<PreparePayRequest> =>
- buildCodecForObject<PreparePayRequest>()
- .property("talerPayUri", codecForString())
- .build("PreparePay");
-
-export interface ConfirmPayRequest {
- proposalId: string;
- sessionId?: string;
-}
-
-export const codecForConfirmPayRequest = (): Codec<ConfirmPayRequest> =>
- buildCodecForObject<ConfirmPayRequest>()
- .property("proposalId", codecForString())
- .property("sessionId", codecOptional(codecForString()))
- .build("ConfirmPay");
-
-export type CoreApiResponse = CoreApiResponseSuccess | CoreApiResponseError;
-
-export type CoreApiEnvelope = CoreApiResponse | CoreApiNotification;
-
-export interface CoreApiNotification {
- type: "notification";
- payload: unknown;
-}
-
-export interface CoreApiResponseSuccess {
- // To distinguish the message from notifications
- type: "response";
- operation: string;
- id: string;
- result: unknown;
-}
-
-export interface CoreApiResponseError {
- // To distinguish the message from notifications
- type: "error";
- operation: string;
- id: string;
- error: TalerErrorDetail;
-}
-
-export interface WithdrawTestBalanceRequest {
- amount: string;
- bankBaseUrl: string;
- exchangeBaseUrl: string;
-}
-
-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.
- */
-export interface MakeSyncSignatureRequest {
- accountPriv: string;
- oldHash: string | undefined;
- newHash: string;
-}
-
-/**
- * Planchet for a coin during refresh.
- */
-export interface RefreshPlanchetInfo {
- /**
- * Public key for the coin.
- */
- coinPub: string;
-
- /**
- * Private key for the coin.
- */
- coinPriv: string;
-
- /**
- * Blinded public key.
- */
- coinEv: CoinEnvelope;
-
- coinEvHash: string;
-
- /**
- * Blinding key used.
- */
- blindingKey: string;
-}
-
-/**
- * Strategy for loading recovery information.
- */
-export enum RecoveryMergeStrategy {
- /**
- * Keep the local wallet root key, import and take over providers.
- */
- Ours = "ours",
-
- /**
- * Migrate to the wallet root key from the recovery information.
- */
- Theirs = "theirs",
-}
-
-/**
- * Load recovery information into the wallet.
- */
-export interface RecoveryLoadRequest {
- recovery: BackupRecovery;
- strategy?: RecoveryMergeStrategy;
-}
-
-export const codecForWithdrawTestBalance =
- (): Codec<WithdrawTestBalanceRequest> =>
- buildCodecForObject<WithdrawTestBalanceRequest>()
- .property("amount", codecForString())
- .property("bankBaseUrl", codecForString())
- .property("exchangeBaseUrl", codecForString())
- .build("WithdrawTestBalanceRequest");
-
-export interface ApplyRefundResponse {
- contractTermsHash: 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("info", codecForOrderShortInfo())
- .build("ApplyRefundResponse");
-
-export interface SetCoinSuspendedRequest {
- coinPub: string;
- suspended: boolean;
-}
-
-export const codecForSetCoinSuspendedRequest =
- (): Codec<SetCoinSuspendedRequest> =>
- buildCodecForObject<SetCoinSuspendedRequest>()
- .property("coinPub", codecForString())
- .property("suspended", codecForBoolean())
- .build("SetCoinSuspendedRequest");
-
-export interface ForceRefreshRequest {
- coinPubList: string[];
-}
-
-export const codecForForceRefreshRequest = (): Codec<ForceRefreshRequest> =>
- buildCodecForObject<ForceRefreshRequest>()
- .property("coinPubList", codecForList(codecForString()))
- .build("ForceRefreshRequest");
-
-
-
-export interface PrepareRefundRequest {
- talerRefundUri: string;
-}
-
-export const codecForPrepareRefundRequest = (): Codec<PrepareRefundRequest> =>
- buildCodecForObject<PrepareRefundRequest>()
- .property("talerRefundUri", codecForString())
- .build("PrepareRefundRequest");
-
-export interface PrepareTipRequest {
- talerTipUri: string;
-}
-
-export const codecForPrepareTipRequest = (): Codec<PrepareTipRequest> =>
- buildCodecForObject<PrepareTipRequest>()
- .property("talerTipUri", codecForString())
- .build("PrepareTipRequest");
-
-export interface AcceptTipRequest {
- walletTipId: string;
-}
-
-export const codecForAcceptTipRequest = (): Codec<AcceptTipRequest> =>
- buildCodecForObject<AcceptTipRequest>()
- .property("walletTipId", codecForString())
- .build("AcceptTipRequest");
-
-export interface AbortPayWithRefundRequest {
- proposalId: string;
-}
-
-export const codecForAbortPayWithRefundRequest =
- (): Codec<AbortPayWithRefundRequest> =>
- buildCodecForObject<AbortPayWithRefundRequest>()
- .property("proposalId", codecForString())
- .build("AbortPayWithRefundRequest");
-
-export interface GetFeeForDepositRequest {
- depositPaytoUri: string;
- amount: AmountString;
-}
-
-export interface CreateDepositGroupRequest {
- 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;
-
-}
-export const codecForPrepareDepositRequest =
- (): Codec<PrepareDepositRequest> =>
- buildCodecForObject<PrepareDepositRequest>()
- .property("amount", codecForAmountString())
- .property("depositPaytoUri", codecForString())
- .build("PrepareDepositRequest");
-
-export interface PrepareDepositResponse {
- totalDepositCost: AmountJson;
- effectiveDepositAmount: AmountJson;
-}
-
-export const codecForCreateDepositGroupRequest =
- (): Codec<CreateDepositGroupRequest> =>
- buildCodecForObject<CreateDepositGroupRequest>()
- .property("amount", codecForAmountString())
- .property("depositPaytoUri", codecForString())
- .build("CreateDepositGroupRequest");
-
-export interface CreateDepositGroupResponse {
- depositGroupId: string;
-}
-
-export interface TrackDepositGroupRequest {
- depositGroupId: string;
-}
-
-export interface TrackDepositGroupResponse {
- responses: {
- status: number;
- body: any;
- }[];
-}
-
-export const codecForTrackDepositGroupRequest =
- (): Codec<TrackDepositGroupRequest> =>
- buildCodecForObject<TrackDepositGroupRequest>()
- .property("depositGroupId", codecForAmountString())
- .build("TrackDepositGroupRequest");
-
-export interface WithdrawUriInfoResponse {
- amount: AmountString;
- defaultExchangeBaseUrl?: string;
- possibleExchanges: ExchangeListItem[];
-}
-
-export const codecForWithdrawUriInfoResponse =
- (): Codec<WithdrawUriInfoResponse> =>
- buildCodecForObject<WithdrawUriInfoResponse>()
- .property("amount", codecForAmountString())
- .property("defaultExchangeBaseUrl", codecOptional(codecForString()))
- .property("possibleExchanges", codecForList(codecForExchangeListItem()))
- .build("WithdrawUriInfoResponse");
-
-export interface WalletCurrencyInfo {
- trustedAuditors: {
- currency: string;
- auditorPub: string;
- auditorBaseUrl: string;
- }[];
- trustedExchanges: {
- currency: string;
- exchangeMasterPub: string;
- exchangeBaseUrl: string;
- }[];
-}
-
-export interface DeleteTransactionRequest {
- transactionId: string;
-}
-
-export interface RetryTransactionRequest {
- transactionId: string;
-}
-
-export const codecForDeleteTransactionRequest =
- (): Codec<DeleteTransactionRequest> =>
- buildCodecForObject<DeleteTransactionRequest>()
- .property("transactionId", codecForString())
- .build("DeleteTransactionRequest");
-
-export const codecForRetryTransactionRequest =
- (): Codec<RetryTransactionRequest> =>
- buildCodecForObject<RetryTransactionRequest>()
- .property("transactionId", codecForString())
- .build("RetryTransactionRequest");
-
-export interface SetWalletDeviceIdRequest {
- /**
- * New wallet device ID to set.
- */
- walletDeviceId: string;
-}
-
-export const codecForSetWalletDeviceIdRequest =
- (): Codec<SetWalletDeviceIdRequest> =>
- buildCodecForObject<SetWalletDeviceIdRequest>()
- .property("walletDeviceId", codecForString())
- .build("SetWalletDeviceIdRequest");
-
-export interface WithdrawFakebankRequest {
- amount: AmountString;
- exchange: string;
- bank: string;
-}
-
-export const codecForWithdrawFakebankRequest =
- (): Codec<WithdrawFakebankRequest> =>
- buildCodecForObject<WithdrawFakebankRequest>()
- .property("amount", codecForAmountString())
- .property("bank", codecForString())
- .property("exchange", codecForString())
- .build("WithdrawFakebankRequest");
-
-export interface ImportDb {
- dump: any;
-}
-export const codecForImportDbRequest = (): Codec<ImportDb> =>
- buildCodecForObject<ImportDb>()
- .property("dump", codecForAny())
- .build("ImportDbRequest");
-
-export interface ForcedDenomSel {
- denoms: {
- value: AmountString;
- count: number;
- }[];
-}
diff --git a/packages/taler-util/src/whatwg-url.ts b/packages/taler-util/src/whatwg-url.ts
new file mode 100644
index 000000000..13abf5397
--- /dev/null
+++ b/packages/taler-util/src/whatwg-url.ts
@@ -0,0 +1,2126 @@
+/*
+The MIT License (MIT)
+
+Copyright (c) Sebastian Mayr
+Copyright (c) 2022 Taler Systems S.A.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+// Vendored with modifications (TypeScript etc.) from https://github.com/jsdom/whatwg-url
+
+const utf8Encoder = new TextEncoder();
+const utf8Decoder = new TextDecoder("utf-8", { ignoreBOM: true });
+
+function utf8Encode(string: string | undefined) {
+ return utf8Encoder.encode(string);
+}
+
+function utf8DecodeWithoutBOM(
+ bytes: DataView | ArrayBuffer | null | undefined,
+) {
+ return utf8Decoder.decode(bytes);
+}
+
+// https://url.spec.whatwg.org/#concept-urlencoded-parser
+function parseUrlencoded(input: Uint8Array) {
+ const sequences = strictlySplitByteSequence(input, p("&"));
+ const output = [];
+ for (const bytes of sequences) {
+ if (bytes.length === 0) {
+ continue;
+ }
+
+ let name, value;
+ const indexOfEqual = bytes.indexOf(p("=")!);
+
+ if (indexOfEqual >= 0) {
+ name = bytes.slice(0, indexOfEqual);
+ value = bytes.slice(indexOfEqual + 1);
+ } else {
+ name = bytes;
+ value = new Uint8Array(0);
+ }
+
+ name = replaceByteInByteSequence(name, 0x2b, 0x20);
+ value = replaceByteInByteSequence(value, 0x2b, 0x20);
+
+ const nameString = utf8DecodeWithoutBOM(percentDecodeBytes(name));
+ const valueString = utf8DecodeWithoutBOM(percentDecodeBytes(value));
+
+ output.push([nameString, valueString]);
+ }
+ return output;
+}
+
+// https://url.spec.whatwg.org/#concept-urlencoded-string-parser
+function parseUrlencodedString(input: string | undefined) {
+ return parseUrlencoded(utf8Encode(input));
+}
+
+// https://url.spec.whatwg.org/#concept-urlencoded-serializer
+function serializeUrlencoded(tuples: any[], encodingOverride = undefined) {
+ let encoding = "utf-8";
+ if (encodingOverride !== undefined) {
+ // TODO "get the output encoding", i.e. handle encoding labels vs. names.
+ encoding = encodingOverride;
+ }
+
+ let output = "";
+ for (const [i, tuple] of tuples.entries()) {
+ // TODO: handle encoding override
+
+ const name = utf8PercentEncodeString(
+ tuple[0],
+ isURLEncodedPercentEncode,
+ true,
+ );
+
+ let value = tuple[1];
+ if (tuple.length > 2 && tuple[2] !== undefined) {
+ if (tuple[2] === "hidden" && name === "_charset_") {
+ value = encoding;
+ } else if (tuple[2] === "file") {
+ // value is a File object
+ value = value.name;
+ }
+ }
+
+ value = utf8PercentEncodeString(value, isURLEncodedPercentEncode, true);
+
+ if (i !== 0) {
+ output += "&";
+ }
+ output += `${name}=${value}`;
+ }
+ return output;
+}
+
+function strictlySplitByteSequence(buf: Uint8Array, cp: any) {
+ const list = [];
+ let last = 0;
+ let i = buf.indexOf(cp);
+ while (i >= 0) {
+ list.push(buf.slice(last, i));
+ last = i + 1;
+ i = buf.indexOf(cp, last);
+ }
+ if (last !== buf.length) {
+ list.push(buf.slice(last));
+ }
+ return list;
+}
+
+function replaceByteInByteSequence(buf: Uint8Array, from: number, to: number) {
+ let i = buf.indexOf(from);
+ while (i >= 0) {
+ buf[i] = to;
+ i = buf.indexOf(from, i + 1);
+ }
+ return buf;
+}
+
+function p(char: string) {
+ return char.codePointAt(0);
+}
+
+// https://url.spec.whatwg.org/#percent-encode
+function percentEncode(c: number) {
+ let hex = c.toString(16).toUpperCase();
+ if (hex.length === 1) {
+ hex = `0${hex}`;
+ }
+
+ return `%${hex}`;
+}
+
+// https://url.spec.whatwg.org/#percent-decode
+function percentDecodeBytes(input: Uint8Array) {
+ const output = new Uint8Array(input.byteLength);
+ let outputIndex = 0;
+ for (let i = 0; i < input.byteLength; ++i) {
+ const byte = input[i];
+ if (byte !== 0x25) {
+ output[outputIndex++] = byte;
+ } else if (
+ byte === 0x25 &&
+ (!isASCIIHex(input[i + 1]) || !isASCIIHex(input[i + 2]))
+ ) {
+ output[outputIndex++] = byte;
+ } else {
+ const bytePoint = parseInt(
+ String.fromCodePoint(input[i + 1], input[i + 2]),
+ 16,
+ );
+ output[outputIndex++] = bytePoint;
+ i += 2;
+ }
+ }
+
+ return output.slice(0, outputIndex);
+}
+
+// https://url.spec.whatwg.org/#string-percent-decode
+function percentDecodeString(input: string) {
+ const bytes = utf8Encode(input);
+ return percentDecodeBytes(bytes);
+}
+
+// https://url.spec.whatwg.org/#c0-control-percent-encode-set
+function isC0ControlPercentEncode(c: number) {
+ return c <= 0x1f || c > 0x7e;
+}
+
+// https://url.spec.whatwg.org/#fragment-percent-encode-set
+const extraFragmentPercentEncodeSet = new Set([
+ p(" "),
+ p('"'),
+ p("<"),
+ p(">"),
+ p("`"),
+]);
+
+function isFragmentPercentEncode(c: number) {
+ return isC0ControlPercentEncode(c) || extraFragmentPercentEncodeSet.has(c);
+}
+
+// https://url.spec.whatwg.org/#query-percent-encode-set
+const extraQueryPercentEncodeSet = new Set([
+ p(" "),
+ p('"'),
+ p("#"),
+ p("<"),
+ p(">"),
+]);
+
+function isQueryPercentEncode(c: number) {
+ return isC0ControlPercentEncode(c) || extraQueryPercentEncodeSet.has(c);
+}
+
+// https://url.spec.whatwg.org/#special-query-percent-encode-set
+function isSpecialQueryPercentEncode(c: number) {
+ return isQueryPercentEncode(c) || c === p("'");
+}
+
+// https://url.spec.whatwg.org/#path-percent-encode-set
+const extraPathPercentEncodeSet = new Set([p("?"), p("`"), p("{"), p("}")]);
+function isPathPercentEncode(c: number) {
+ return isQueryPercentEncode(c) || extraPathPercentEncodeSet.has(c);
+}
+
+// https://url.spec.whatwg.org/#userinfo-percent-encode-set
+const extraUserinfoPercentEncodeSet = new Set([
+ p("/"),
+ p(":"),
+ p(";"),
+ p("="),
+ p("@"),
+ p("["),
+ p("\\"),
+ p("]"),
+ p("^"),
+ p("|"),
+]);
+function isUserinfoPercentEncode(c: number) {
+ return isPathPercentEncode(c) || extraUserinfoPercentEncodeSet.has(c);
+}
+
+// https://url.spec.whatwg.org/#component-percent-encode-set
+const extraComponentPercentEncodeSet = new Set([
+ p("$"),
+ p("%"),
+ p("&"),
+ p("+"),
+ p(","),
+]);
+function isComponentPercentEncode(c: number) {
+ return isUserinfoPercentEncode(c) || extraComponentPercentEncodeSet.has(c);
+}
+
+// https://url.spec.whatwg.org/#application-x-www-form-urlencoded-percent-encode-set
+const extraURLEncodedPercentEncodeSet = new Set([
+ p("!"),
+ p("'"),
+ p("("),
+ p(")"),
+ p("~"),
+]);
+
+function isURLEncodedPercentEncode(c: number) {
+ return isComponentPercentEncode(c) || extraURLEncodedPercentEncodeSet.has(c);
+}
+
+// https://url.spec.whatwg.org/#code-point-percent-encode-after-encoding
+// https://url.spec.whatwg.org/#utf-8-percent-encode
+// Assuming encoding is always utf-8 allows us to trim one of the logic branches. TODO: support encoding.
+// The "-Internal" variant here has code points as JS strings. The external version used by other files has code points
+// as JS numbers, like the rest of the codebase.
+function utf8PercentEncodeCodePointInternal(
+ codePoint: string,
+ percentEncodePredicate: (arg0: number) => any,
+) {
+ const bytes = utf8Encode(codePoint);
+ let output = "";
+ for (const byte of bytes) {
+ // Our percentEncodePredicate operates on bytes, not code points, so this is slightly different from the spec.
+ if (!percentEncodePredicate(byte)) {
+ output += String.fromCharCode(byte);
+ } else {
+ output += percentEncode(byte);
+ }
+ }
+
+ return output;
+}
+
+function utf8PercentEncodeCodePoint(
+ codePoint: number,
+ percentEncodePredicate: (arg0: number) => any,
+) {
+ return utf8PercentEncodeCodePointInternal(
+ String.fromCodePoint(codePoint),
+ percentEncodePredicate,
+ );
+}
+
+// https://url.spec.whatwg.org/#string-percent-encode-after-encoding
+// https://url.spec.whatwg.org/#string-utf-8-percent-encode
+function utf8PercentEncodeString(
+ input: string,
+ percentEncodePredicate: {
+ (c: number): boolean;
+ (c: number): boolean;
+ (arg0: number): any;
+ },
+ spaceAsPlus = false,
+) {
+ let output = "";
+ for (const codePoint of input) {
+ if (spaceAsPlus && codePoint === " ") {
+ output += "+";
+ } else {
+ output += utf8PercentEncodeCodePointInternal(
+ codePoint,
+ percentEncodePredicate,
+ );
+ }
+ }
+ return output;
+}
+
+// Note that we take code points as JS numbers, not JS strings.
+
+function isASCIIDigit(c: number) {
+ return c >= 0x30 && c <= 0x39;
+}
+
+function isASCIIAlpha(c: number) {
+ return (c >= 0x41 && c <= 0x5a) || (c >= 0x61 && c <= 0x7a);
+}
+
+function isASCIIAlphanumeric(c: number) {
+ return isASCIIAlpha(c) || isASCIIDigit(c);
+}
+
+function isASCIIHex(c: number) {
+ return (
+ isASCIIDigit(c) || (c >= 0x41 && c <= 0x46) || (c >= 0x61 && c <= 0x66)
+ );
+}
+
+export class URLSearchParamsImpl {
+ _list: any[];
+ _url: any;
+ constructor(init: any, { doNotStripQMark = false }: any = {}) {
+ this._list = [];
+ this._url = null;
+
+ if (!doNotStripQMark && typeof init === "string" && init[0] === "?") {
+ init = init.slice(1);
+ }
+
+ if (Array.isArray(init)) {
+ for (const pair of init) {
+ if (pair.length !== 2) {
+ throw new TypeError(
+ "Failed to construct 'URLSearchParams': parameter 1 sequence's element does not " +
+ "contain exactly two elements.",
+ );
+ }
+ this._list.push([pair[0], pair[1]]);
+ }
+ } else if (
+ typeof init === "object" &&
+ Object.getPrototypeOf(init) === null
+ ) {
+ for (const name of Object.keys(init)) {
+ const value = init[name];
+ this._list.push([name, value]);
+ }
+ } else {
+ this._list = parseUrlencodedString(init);
+ }
+ }
+
+ _updateSteps() {
+ if (this._url !== null) {
+ let query: string | null = serializeUrlencoded(this._list);
+ if (query === "") {
+ query = null;
+ }
+ this._url._url.query = query;
+ }
+ }
+
+ append(name: string, value: string) {
+ this._list.push([name, value]);
+ this._updateSteps();
+ }
+
+ delete(name: string) {
+ let i = 0;
+ while (i < this._list.length) {
+ if (this._list[i][0] === name) {
+ this._list.splice(i, 1);
+ } else {
+ i++;
+ }
+ }
+ this._updateSteps();
+ }
+
+ get(name: string) {
+ for (const tuple of this._list) {
+ if (tuple[0] === name) {
+ return tuple[1];
+ }
+ }
+ return null;
+ }
+
+ getAll(name: string) {
+ const output = [];
+ for (const tuple of this._list) {
+ if (tuple[0] === name) {
+ output.push(tuple[1]);
+ }
+ }
+ return output;
+ }
+
+ forEach(
+ callbackfn: (
+ value: string,
+ key: string,
+ parent: URLSearchParamsImpl,
+ ) => void,
+ thisArg?: any,
+ ): void {
+ for (const tuple of this._list) {
+ callbackfn.call(thisArg, tuple[1], tuple[0], this);
+ }
+ }
+
+ has(name: string) {
+ for (const tuple of this._list) {
+ if (tuple[0] === name) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ set(name: string, value: string) {
+ let found = false;
+ let i = 0;
+ while (i < this._list.length) {
+ if (this._list[i][0] === name) {
+ if (found) {
+ this._list.splice(i, 1);
+ } else {
+ found = true;
+ this._list[i][1] = value;
+ i++;
+ }
+ } else {
+ i++;
+ }
+ }
+ if (!found) {
+ this._list.push([name, value]);
+ }
+ this._updateSteps();
+ }
+
+ sort() {
+ this._list.sort((a, b) => {
+ if (a[0] < b[0]) {
+ return -1;
+ }
+ if (a[0] > b[0]) {
+ return 1;
+ }
+ return 0;
+ });
+
+ this._updateSteps();
+ }
+
+ [Symbol.iterator]() {
+ return this._list[Symbol.iterator]();
+ }
+
+ toString() {
+ return serializeUrlencoded(this._list);
+ }
+}
+
+const specialSchemes = {
+ ftp: 21,
+ file: null,
+ http: 80,
+ https: 443,
+ ws: 80,
+ wss: 443,
+} as { [x: string]: number | null };
+
+const failure = Symbol("failure");
+
+function countSymbols(str: any) {
+ return [...str].length;
+}
+
+function at(input: any, idx: any) {
+ const c = input[idx];
+ return isNaN(c) ? undefined : String.fromCodePoint(c);
+}
+
+function isSingleDot(buffer: string) {
+ return buffer === "." || buffer.toLowerCase() === "%2e";
+}
+
+function isDoubleDot(buffer: string) {
+ buffer = buffer.toLowerCase();
+ return (
+ buffer === ".." ||
+ buffer === "%2e." ||
+ buffer === ".%2e" ||
+ buffer === "%2e%2e"
+ );
+}
+
+function isWindowsDriveLetterCodePoints(cp1: number, cp2: number) {
+ return isASCIIAlpha(cp1) && (cp2 === p(":") || cp2 === p("|"));
+}
+
+function isWindowsDriveLetterString(string: string) {
+ return (
+ string.length === 2 &&
+ isASCIIAlpha(string.codePointAt(0)!) &&
+ (string[1] === ":" || string[1] === "|")
+ );
+}
+
+function isNormalizedWindowsDriveLetterString(string: string) {
+ return (
+ string.length === 2 &&
+ isASCIIAlpha(string.codePointAt(0)!) &&
+ string[1] === ":"
+ );
+}
+
+function containsForbiddenHostCodePoint(string: string) {
+ return (
+ string.search(
+ /\u0000|\u0009|\u000A|\u000D|\u0020|#|\/|:|<|>|\?|@|\[|\\|\]|\^|\|/u,
+ ) !== -1
+ );
+}
+
+function containsForbiddenDomainCodePoint(string: string) {
+ return (
+ containsForbiddenHostCodePoint(string) ||
+ string.search(/[\u0000-\u001F]|%|\u007F/u) !== -1
+ );
+}
+
+function isSpecialScheme(scheme: string) {
+ return specialSchemes[scheme] !== undefined;
+}
+
+function isSpecial(url: any) {
+ return isSpecialScheme(url.scheme);
+}
+
+function isNotSpecial(url: UrlObj) {
+ return !isSpecialScheme(url.scheme);
+}
+
+function defaultPort(scheme: string) {
+ return specialSchemes[scheme];
+}
+
+function parseIPv4Number(input: string) {
+ if (input === "") {
+ return failure;
+ }
+
+ let R = 10;
+
+ if (
+ input.length >= 2 &&
+ input.charAt(0) === "0" &&
+ input.charAt(1).toLowerCase() === "x"
+ ) {
+ input = input.substring(2);
+ R = 16;
+ } else if (input.length >= 2 && input.charAt(0) === "0") {
+ input = input.substring(1);
+ R = 8;
+ }
+
+ if (input === "") {
+ return 0;
+ }
+
+ let regex = /[^0-7]/u;
+ if (R === 10) {
+ regex = /[^0-9]/u;
+ }
+ if (R === 16) {
+ regex = /[^0-9A-Fa-f]/u;
+ }
+
+ if (regex.test(input)) {
+ return failure;
+ }
+
+ return parseInt(input, R);
+}
+
+function parseIPv4(input: string) {
+ const parts = input.split(".");
+ if (parts[parts.length - 1] === "") {
+ if (parts.length > 1) {
+ parts.pop();
+ }
+ }
+
+ if (parts.length > 4) {
+ return failure;
+ }
+
+ const numbers = [];
+ for (const part of parts) {
+ const n = parseIPv4Number(part);
+ if (n === failure) {
+ return failure;
+ }
+
+ numbers.push(n);
+ }
+
+ for (let i = 0; i < numbers.length - 1; ++i) {
+ if (numbers[i] > 255) {
+ return failure;
+ }
+ }
+ if (numbers[numbers.length - 1] >= 256 ** (5 - numbers.length)) {
+ return failure;
+ }
+
+ let ipv4 = numbers.pop();
+ let counter = 0;
+
+ for (const n of numbers) {
+ ipv4! += n * 256 ** (3 - counter);
+ ++counter;
+ }
+
+ return ipv4;
+}
+
+function serializeIPv4(address: number) {
+ let output = "";
+ let n = address;
+
+ for (let i = 1; i <= 4; ++i) {
+ output = String(n % 256) + output;
+ if (i !== 4) {
+ output = `.${output}`;
+ }
+ n = Math.floor(n / 256);
+ }
+
+ return output;
+}
+
+function parseIPv6(inputArg: string) {
+ const address = [0, 0, 0, 0, 0, 0, 0, 0];
+ let pieceIndex = 0;
+ let compress = null;
+ let pointer = 0;
+
+ const input = Array.from(inputArg, (c) => c.codePointAt(0));
+
+ if (input[pointer] === p(":")) {
+ if (input[pointer + 1] !== p(":")) {
+ return failure;
+ }
+
+ pointer += 2;
+ ++pieceIndex;
+ compress = pieceIndex;
+ }
+
+ while (pointer < input.length) {
+ if (pieceIndex === 8) {
+ return failure;
+ }
+
+ if (input[pointer] === p(":")) {
+ if (compress !== null) {
+ return failure;
+ }
+ ++pointer;
+ ++pieceIndex;
+ compress = pieceIndex;
+ continue;
+ }
+
+ let value = 0;
+ let length = 0;
+
+ while (length < 4 && isASCIIHex(input[pointer]!)) {
+ value = value * 0x10 + parseInt(at(input, pointer)!, 16);
+ ++pointer;
+ ++length;
+ }
+
+ if (input[pointer] === p(".")) {
+ if (length === 0) {
+ return failure;
+ }
+
+ pointer -= length;
+
+ if (pieceIndex > 6) {
+ return failure;
+ }
+
+ let numbersSeen = 0;
+
+ while (input[pointer] !== undefined) {
+ let ipv4Piece = null;
+
+ if (numbersSeen > 0) {
+ if (input[pointer] === p(".") && numbersSeen < 4) {
+ ++pointer;
+ } else {
+ return failure;
+ }
+ }
+
+ if (!isASCIIDigit(input[pointer]!)) {
+ return failure;
+ }
+
+ while (isASCIIDigit(input[pointer]!)) {
+ const number = parseInt(at(input, pointer)!);
+ if (ipv4Piece === null) {
+ ipv4Piece = number;
+ } else if (ipv4Piece === 0) {
+ return failure;
+ } else {
+ ipv4Piece = ipv4Piece * 10 + number;
+ }
+ if (ipv4Piece > 255) {
+ return failure;
+ }
+ ++pointer;
+ }
+
+ address[pieceIndex] = address[pieceIndex] * 0x100 + ipv4Piece!;
+
+ ++numbersSeen;
+
+ if (numbersSeen === 2 || numbersSeen === 4) {
+ ++pieceIndex;
+ }
+ }
+
+ if (numbersSeen !== 4) {
+ return failure;
+ }
+
+ break;
+ } else if (input[pointer] === p(":")) {
+ ++pointer;
+ if (input[pointer] === undefined) {
+ return failure;
+ }
+ } else if (input[pointer] !== undefined) {
+ return failure;
+ }
+
+ address[pieceIndex] = value;
+ ++pieceIndex;
+ }
+
+ if (compress !== null) {
+ let swaps = pieceIndex - compress;
+ pieceIndex = 7;
+ while (pieceIndex !== 0 && swaps > 0) {
+ const temp = address[compress + swaps - 1];
+ address[compress + swaps - 1] = address[pieceIndex];
+ address[pieceIndex] = temp;
+ --pieceIndex;
+ --swaps;
+ }
+ } else if (compress === null && pieceIndex !== 8) {
+ return failure;
+ }
+
+ return address;
+}
+
+function serializeIPv6(address: any[]) {
+ let output = "";
+ const compress = findLongestZeroSequence(address);
+ let ignore0 = false;
+
+ for (let pieceIndex = 0; pieceIndex <= 7; ++pieceIndex) {
+ if (ignore0 && address[pieceIndex] === 0) {
+ continue;
+ } else if (ignore0) {
+ ignore0 = false;
+ }
+
+ if (compress === pieceIndex) {
+ const separator = pieceIndex === 0 ? "::" : ":";
+ output += separator;
+ ignore0 = true;
+ continue;
+ }
+
+ output += address[pieceIndex].toString(16);
+
+ if (pieceIndex !== 7) {
+ output += ":";
+ }
+ }
+
+ return output;
+}
+
+function parseHost(input: string, isNotSpecialArg = false) {
+ if (input[0] === "[") {
+ if (input[input.length - 1] !== "]") {
+ return failure;
+ }
+
+ return parseIPv6(input.substring(1, input.length - 1));
+ }
+
+ if (isNotSpecialArg) {
+ return parseOpaqueHost(input);
+ }
+
+ const domain = utf8DecodeWithoutBOM(percentDecodeString(input));
+ const asciiDomain = domainToASCII(domain);
+ if (asciiDomain === failure) {
+ return failure;
+ }
+
+ if (containsForbiddenDomainCodePoint(asciiDomain)) {
+ return failure;
+ }
+
+ if (endsInANumber(asciiDomain)) {
+ return parseIPv4(asciiDomain);
+ }
+
+ return asciiDomain;
+}
+
+function endsInANumber(input: string) {
+ const parts = input.split(".");
+ if (parts[parts.length - 1] === "") {
+ if (parts.length === 1) {
+ return false;
+ }
+ parts.pop();
+ }
+
+ const last = parts[parts.length - 1];
+ if (parseIPv4Number(last) !== failure) {
+ return true;
+ }
+
+ if (/^[0-9]+$/u.test(last)) {
+ return true;
+ }
+
+ return false;
+}
+
+function parseOpaqueHost(input: string) {
+ if (containsForbiddenHostCodePoint(input)) {
+ return failure;
+ }
+
+ return utf8PercentEncodeString(input, isC0ControlPercentEncode);
+}
+
+function findLongestZeroSequence(arr: number[]) {
+ let maxIdx = null;
+ let maxLen = 1; // only find elements > 1
+ let currStart = null;
+ let currLen = 0;
+
+ for (let i = 0; i < arr.length; ++i) {
+ if (arr[i] !== 0) {
+ if (currLen > maxLen) {
+ maxIdx = currStart;
+ maxLen = currLen;
+ }
+
+ currStart = null;
+ currLen = 0;
+ } else {
+ if (currStart === null) {
+ currStart = i;
+ }
+ ++currLen;
+ }
+ }
+
+ // if trailing zeros
+ if (currLen > maxLen) {
+ return currStart;
+ }
+
+ return maxIdx;
+}
+
+function serializeHost(host: number | number[] | string) {
+ if (typeof host === "number") {
+ return serializeIPv4(host);
+ }
+
+ // IPv6 serializer
+ if (host instanceof Array) {
+ return `[${serializeIPv6(host)}]`;
+ }
+
+ return host;
+}
+
+import { punycode } from "./punycode.js";
+
+function domainToASCII(domain: string, beStrict = false) {
+ // const result = tr46.toASCII(domain, {
+ // checkBidi: true,
+ // checkHyphens: false,
+ // checkJoiners: true,
+ // useSTD3ASCIIRules: beStrict,
+ // verifyDNSLength: beStrict,
+ // });
+ let result;
+ try {
+ result = punycode.toASCII(domain);
+ } catch (e) {
+ return failure;
+ }
+ if (result === null || result === "") {
+ return failure;
+ }
+ return result;
+}
+
+function trimControlChars(url: string) {
+ return url.replace(/^[\u0000-\u001F\u0020]+|[\u0000-\u001F\u0020]+$/gu, "");
+}
+
+function trimTabAndNewline(url: string) {
+ return url.replace(/\u0009|\u000A|\u000D/gu, "");
+}
+
+function shortenPath(url: UrlObj) {
+ const { path } = url;
+ if (path.length === 0) {
+ return;
+ }
+ if (
+ url.scheme === "file" &&
+ path.length === 1 &&
+ isNormalizedWindowsDriveLetter(path[0])
+ ) {
+ return;
+ }
+
+ path.pop();
+}
+
+function includesCredentials(url: UrlObj) {
+ return url.username !== "" || url.password !== "";
+}
+
+function cannotHaveAUsernamePasswordPort(url: UrlObj) {
+ return url.host === null || url.host === "" || url.scheme === "file";
+}
+
+function hasAnOpaquePath(url: UrlObj) {
+ return typeof url.path === "string";
+}
+
+function isNormalizedWindowsDriveLetter(string: string) {
+ return /^[A-Za-z]:$/u.test(string);
+}
+
+export interface UrlObj {
+ scheme: string;
+ username: string;
+ password: string;
+ host: string | number[] | number | null | undefined;
+ port: number | null;
+ path: string[];
+ query: any;
+ fragment: any;
+}
+
+class URLStateMachine {
+ pointer: number;
+ input: number[];
+ base: any;
+ encodingOverride: string;
+ url: UrlObj;
+ state: string;
+ stateOverride: string;
+ failure: boolean;
+ parseError: boolean;
+ buffer: string;
+ atFlag: boolean;
+ arrFlag: boolean;
+ passwordTokenSeenFlag: boolean;
+
+ constructor(
+ input: string,
+ base: any,
+ encodingOverride: string,
+ url: UrlObj,
+ stateOverride: string,
+ ) {
+ this.pointer = 0;
+ this.base = base || null;
+ this.encodingOverride = encodingOverride || "utf-8";
+ this.url = url;
+ this.failure = false;
+ this.parseError = false;
+
+ if (!this.url) {
+ this.url = {
+ scheme: "",
+ username: "",
+ password: "",
+ host: null,
+ port: null,
+ path: [],
+ query: null,
+ fragment: null,
+ };
+
+ const res = trimControlChars(input);
+ if (res !== input) {
+ this.parseError = true;
+ }
+ input = res;
+ }
+
+ const res = trimTabAndNewline(input);
+ if (res !== input) {
+ this.parseError = true;
+ }
+ input = res;
+
+ this.state = stateOverride || "scheme start";
+
+ this.buffer = "";
+ this.atFlag = false;
+ this.arrFlag = false;
+ this.passwordTokenSeenFlag = false;
+
+ this.input = Array.from(input, (c) => c.codePointAt(0)!);
+
+ for (; this.pointer <= this.input.length; ++this.pointer) {
+ const c = this.input[this.pointer];
+ const cStr = isNaN(c) ? undefined : String.fromCodePoint(c);
+
+ // exec state machine
+ const ret = this.table[`parse ${this.state}`].call(this, c, cStr!);
+ if (!ret) {
+ break; // terminate algorithm
+ } else if (ret === failure) {
+ this.failure = true;
+ break;
+ }
+ }
+ }
+
+ table = {
+ "parse scheme start": this.parseSchemeStart,
+ "parse scheme": this.parseScheme,
+ "parse no scheme": this.parseNoScheme,
+ "parse special relative or authority": this.parseSpecialRelativeOrAuthority,
+ "parse path or authority": this.parsePathOrAuthority,
+ "parse relative": this.parseRelative,
+ "parse relative slash": this.parseRelativeSlash,
+ "parse special authority slashes": this.parseSpecialAuthoritySlashes,
+ "parse special authority ignore slashes":
+ this.parseSpecialAuthorityIgnoreSlashes,
+ "parse authority": this.parseAuthority,
+ "parse host": this.parseHostName,
+ "parse hostname": this.parseHostName /* intentional duplication */,
+ "parse port": this.parsePort,
+ "parse file": this.parseFile,
+ "parse file slash": this.parseFileSlash,
+ "parse file host": this.parseFileHost,
+ "parse path start": this.parsePathStart,
+ "parse path": this.parsePath,
+ "parse opaque path": this.parseOpaquePath,
+ "parse query": this.parseQuery,
+ "parse fragment": this.parseFragment,
+ } as { [x: string]: (c: number, cStr: string) => any };
+
+ parseSchemeStart(c: number, cStr: string) {
+ if (isASCIIAlpha(c)) {
+ this.buffer += cStr.toLowerCase();
+ this.state = "scheme";
+ } else if (!this.stateOverride) {
+ this.state = "no scheme";
+ --this.pointer;
+ } else {
+ this.parseError = true;
+ return failure;
+ }
+
+ return true;
+ }
+
+ parseScheme(c: number, cStr: string) {
+ if (
+ isASCIIAlphanumeric(c) ||
+ c === p("+") ||
+ c === p("-") ||
+ c === p(".")
+ ) {
+ this.buffer += cStr.toLowerCase();
+ } else if (c === p(":")) {
+ if (this.stateOverride) {
+ if (isSpecial(this.url) && !isSpecialScheme(this.buffer)) {
+ return false;
+ }
+
+ if (!isSpecial(this.url) && isSpecialScheme(this.buffer)) {
+ return false;
+ }
+
+ if (
+ (includesCredentials(this.url) || this.url.port !== null) &&
+ this.buffer === "file"
+ ) {
+ return false;
+ }
+
+ if (this.url.scheme === "file" && this.url.host === "") {
+ return false;
+ }
+ }
+ this.url.scheme = this.buffer;
+ if (this.stateOverride) {
+ if (this.url.port === defaultPort(this.url.scheme)) {
+ this.url.port = null;
+ }
+ return false;
+ }
+ this.buffer = "";
+ if (this.url.scheme === "file") {
+ if (
+ this.input[this.pointer + 1] !== p("/") ||
+ this.input[this.pointer + 2] !== p("/")
+ ) {
+ this.parseError = true;
+ }
+ this.state = "file";
+ } else if (
+ isSpecial(this.url) &&
+ this.base !== null &&
+ this.base.scheme === this.url.scheme
+ ) {
+ this.state = "special relative or authority";
+ } else if (isSpecial(this.url)) {
+ this.state = "special authority slashes";
+ } else if (this.input[this.pointer + 1] === p("/")) {
+ this.state = "path or authority";
+ ++this.pointer;
+ } else {
+ this.url.path = [""];
+ this.state = "opaque path";
+ }
+ } else if (!this.stateOverride) {
+ this.buffer = "";
+ this.state = "no scheme";
+ this.pointer = -1;
+ } else {
+ this.parseError = true;
+ return failure;
+ }
+
+ return true;
+ }
+
+ parseNoScheme(c: number) {
+ if (this.base === null || (hasAnOpaquePath(this.base) && c !== p("#"))) {
+ return failure;
+ } else if (hasAnOpaquePath(this.base) && c === p("#")) {
+ this.url.scheme = this.base.scheme;
+ this.url.path = this.base.path;
+ this.url.query = this.base.query;
+ this.url.fragment = "";
+ this.state = "fragment";
+ } else if (this.base.scheme === "file") {
+ this.state = "file";
+ --this.pointer;
+ } else {
+ this.state = "relative";
+ --this.pointer;
+ }
+
+ return true;
+ }
+
+ parseSpecialRelativeOrAuthority(c: number) {
+ if (c === p("/") && this.input[this.pointer + 1] === p("/")) {
+ this.state = "special authority ignore slashes";
+ ++this.pointer;
+ } else {
+ this.parseError = true;
+ this.state = "relative";
+ --this.pointer;
+ }
+
+ return true;
+ }
+
+ parsePathOrAuthority(c: number) {
+ if (c === p("/")) {
+ this.state = "authority";
+ } else {
+ this.state = "path";
+ --this.pointer;
+ }
+
+ return true;
+ }
+
+ parseRelative(c: number) {
+ this.url.scheme = this.base.scheme;
+ if (c === p("/")) {
+ this.state = "relative slash";
+ } else if (isSpecial(this.url) && c === p("\\")) {
+ this.parseError = true;
+ this.state = "relative slash";
+ } else {
+ this.url.username = this.base.username;
+ this.url.password = this.base.password;
+ this.url.host = this.base.host;
+ this.url.port = this.base.port;
+ this.url.path = this.base.path.slice();
+ this.url.query = this.base.query;
+ if (c === p("?")) {
+ this.url.query = "";
+ this.state = "query";
+ } else if (c === p("#")) {
+ this.url.fragment = "";
+ this.state = "fragment";
+ } else if (!isNaN(c)) {
+ this.url.query = null;
+ this.url.path.pop();
+ this.state = "path";
+ --this.pointer;
+ }
+ }
+
+ return true;
+ }
+
+ parseRelativeSlash(c: number) {
+ if (isSpecial(this.url) && (c === p("/") || c === p("\\"))) {
+ if (c === p("\\")) {
+ this.parseError = true;
+ }
+ this.state = "special authority ignore slashes";
+ } else if (c === p("/")) {
+ this.state = "authority";
+ } else {
+ this.url.username = this.base.username;
+ this.url.password = this.base.password;
+ this.url.host = this.base.host;
+ this.url.port = this.base.port;
+ this.state = "path";
+ --this.pointer;
+ }
+
+ return true;
+ }
+
+ parseSpecialAuthoritySlashes(c: number) {
+ if (c === p("/") && this.input[this.pointer + 1] === p("/")) {
+ this.state = "special authority ignore slashes";
+ ++this.pointer;
+ } else {
+ this.parseError = true;
+ this.state = "special authority ignore slashes";
+ --this.pointer;
+ }
+
+ return true;
+ }
+
+ parseSpecialAuthorityIgnoreSlashes(c: number) {
+ if (c !== p("/") && c !== p("\\")) {
+ this.state = "authority";
+ --this.pointer;
+ } else {
+ this.parseError = true;
+ }
+
+ return true;
+ }
+
+ parseAuthority(c: number, cStr: string) {
+ if (c === p("@")) {
+ this.parseError = true;
+ if (this.atFlag) {
+ this.buffer = `%40${this.buffer}`;
+ }
+ this.atFlag = true;
+
+ // careful, this is based on buffer and has its own pointer (this.pointer != pointer) and inner chars
+ const len = countSymbols(this.buffer);
+ for (let pointer = 0; pointer < len; ++pointer) {
+ const codePoint = this.buffer.codePointAt(pointer);
+
+ if (codePoint === p(":") && !this.passwordTokenSeenFlag) {
+ this.passwordTokenSeenFlag = true;
+ continue;
+ }
+ const encodedCodePoints = utf8PercentEncodeCodePoint(
+ codePoint!,
+ isUserinfoPercentEncode,
+ );
+ if (this.passwordTokenSeenFlag) {
+ this.url.password += encodedCodePoints;
+ } else {
+ this.url.username += encodedCodePoints;
+ }
+ }
+ this.buffer = "";
+ } else if (
+ isNaN(c) ||
+ c === p("/") ||
+ c === p("?") ||
+ c === p("#") ||
+ (isSpecial(this.url) && c === p("\\"))
+ ) {
+ if (this.atFlag && this.buffer === "") {
+ this.parseError = true;
+ return failure;
+ }
+ this.pointer -= countSymbols(this.buffer) + 1;
+ this.buffer = "";
+ this.state = "host";
+ } else {
+ this.buffer += cStr;
+ }
+
+ return true;
+ }
+
+ parseHostName(c: number, cStr: string) {
+ if (this.stateOverride && this.url.scheme === "file") {
+ --this.pointer;
+ this.state = "file host";
+ } else if (c === p(":") && !this.arrFlag) {
+ if (this.buffer === "") {
+ this.parseError = true;
+ return failure;
+ }
+
+ if (this.stateOverride === "hostname") {
+ return false;
+ }
+
+ const host = parseHost(this.buffer, isNotSpecial(this.url));
+ if (host === failure) {
+ return failure;
+ }
+
+ this.url.host = host;
+ this.buffer = "";
+ this.state = "port";
+ } else if (
+ isNaN(c) ||
+ c === p("/") ||
+ c === p("?") ||
+ c === p("#") ||
+ (isSpecial(this.url) && c === p("\\"))
+ ) {
+ --this.pointer;
+ if (isSpecial(this.url) && this.buffer === "") {
+ this.parseError = true;
+ return failure;
+ } else if (
+ this.stateOverride &&
+ this.buffer === "" &&
+ (includesCredentials(this.url) || this.url.port !== null)
+ ) {
+ this.parseError = true;
+ return false;
+ }
+
+ const host = parseHost(this.buffer, isNotSpecial(this.url));
+ if (host === failure) {
+ return failure;
+ }
+
+ this.url.host = host;
+ this.buffer = "";
+ this.state = "path start";
+ if (this.stateOverride) {
+ return false;
+ }
+ } else {
+ if (c === p("[")) {
+ this.arrFlag = true;
+ } else if (c === p("]")) {
+ this.arrFlag = false;
+ }
+ this.buffer += cStr;
+ }
+
+ return true;
+ }
+
+ parsePort(c: number, cStr: any) {
+ if (isASCIIDigit(c)) {
+ this.buffer += cStr;
+ } else if (
+ isNaN(c) ||
+ c === p("/") ||
+ c === p("?") ||
+ c === p("#") ||
+ (isSpecial(this.url) && c === p("\\")) ||
+ this.stateOverride
+ ) {
+ if (this.buffer !== "") {
+ const port = parseInt(this.buffer);
+ if (port > 2 ** 16 - 1) {
+ this.parseError = true;
+ return failure;
+ }
+ this.url.port = port === defaultPort(this.url.scheme) ? null : port;
+ this.buffer = "";
+ }
+ if (this.stateOverride) {
+ return false;
+ }
+ this.state = "path start";
+ --this.pointer;
+ } else {
+ this.parseError = true;
+ return failure;
+ }
+
+ return true;
+ }
+
+ parseFile(c: number) {
+ this.url.scheme = "file";
+ this.url.host = "";
+
+ if (c === p("/") || c === p("\\")) {
+ if (c === p("\\")) {
+ this.parseError = true;
+ }
+ this.state = "file slash";
+ } else if (this.base !== null && this.base.scheme === "file") {
+ this.url.host = this.base.host;
+ this.url.path = this.base.path.slice();
+ this.url.query = this.base.query;
+ if (c === p("?")) {
+ this.url.query = "";
+ this.state = "query";
+ } else if (c === p("#")) {
+ this.url.fragment = "";
+ this.state = "fragment";
+ } else if (!isNaN(c)) {
+ this.url.query = null;
+ if (!startsWithWindowsDriveLetter(this.input, this.pointer)) {
+ shortenPath(this.url);
+ } else {
+ this.parseError = true;
+ this.url.path = [];
+ }
+
+ this.state = "path";
+ --this.pointer;
+ }
+ } else {
+ this.state = "path";
+ --this.pointer;
+ }
+
+ return true;
+ }
+
+ parseFileSlash(c: number) {
+ if (c === p("/") || c === p("\\")) {
+ if (c === p("\\")) {
+ this.parseError = true;
+ }
+ this.state = "file host";
+ } else {
+ if (this.base !== null && this.base.scheme === "file") {
+ if (
+ !startsWithWindowsDriveLetter(this.input, this.pointer) &&
+ isNormalizedWindowsDriveLetterString(this.base.path[0])
+ ) {
+ this.url.path.push(this.base.path[0]);
+ }
+ this.url.host = this.base.host;
+ }
+ this.state = "path";
+ --this.pointer;
+ }
+
+ return true;
+ }
+
+ parseFileHost(c: number, cStr: string) {
+ if (
+ isNaN(c) ||
+ c === p("/") ||
+ c === p("\\") ||
+ c === p("?") ||
+ c === p("#")
+ ) {
+ --this.pointer;
+ if (!this.stateOverride && isWindowsDriveLetterString(this.buffer)) {
+ this.parseError = true;
+ this.state = "path";
+ } else if (this.buffer === "") {
+ this.url.host = "";
+ if (this.stateOverride) {
+ return false;
+ }
+ this.state = "path start";
+ } else {
+ let host = parseHost(this.buffer, isNotSpecial(this.url));
+ if (host === failure) {
+ return failure;
+ }
+ if (host === "localhost") {
+ host = "";
+ }
+ this.url.host = host as any;
+
+ if (this.stateOverride) {
+ return false;
+ }
+
+ this.buffer = "";
+ this.state = "path start";
+ }
+ } else {
+ this.buffer += cStr;
+ }
+
+ return true;
+ }
+
+ parsePathStart(c: number) {
+ if (isSpecial(this.url)) {
+ if (c === p("\\")) {
+ this.parseError = true;
+ }
+ this.state = "path";
+
+ if (c !== p("/") && c !== p("\\")) {
+ --this.pointer;
+ }
+ } else if (!this.stateOverride && c === p("?")) {
+ this.url.query = "";
+ this.state = "query";
+ } else if (!this.stateOverride && c === p("#")) {
+ this.url.fragment = "";
+ this.state = "fragment";
+ } else if (c !== undefined) {
+ this.state = "path";
+ if (c !== p("/")) {
+ --this.pointer;
+ }
+ } else if (this.stateOverride && this.url.host === null) {
+ this.url.path.push("");
+ }
+
+ return true;
+ }
+
+ parsePath(c: number) {
+ if (
+ isNaN(c) ||
+ c === p("/") ||
+ (isSpecial(this.url) && c === p("\\")) ||
+ (!this.stateOverride && (c === p("?") || c === p("#")))
+ ) {
+ if (isSpecial(this.url) && c === p("\\")) {
+ this.parseError = true;
+ }
+
+ if (isDoubleDot(this.buffer)) {
+ shortenPath(this.url);
+ if (c !== p("/") && !(isSpecial(this.url) && c === p("\\"))) {
+ this.url.path.push("");
+ }
+ } else if (
+ isSingleDot(this.buffer) &&
+ c !== p("/") &&
+ !(isSpecial(this.url) && c === p("\\"))
+ ) {
+ this.url.path.push("");
+ } else if (!isSingleDot(this.buffer)) {
+ if (
+ this.url.scheme === "file" &&
+ this.url.path.length === 0 &&
+ isWindowsDriveLetterString(this.buffer)
+ ) {
+ this.buffer = `${this.buffer[0]}:`;
+ }
+ this.url.path.push(this.buffer);
+ }
+ this.buffer = "";
+ if (c === p("?")) {
+ this.url.query = "";
+ this.state = "query";
+ }
+ if (c === p("#")) {
+ this.url.fragment = "";
+ this.state = "fragment";
+ }
+ } else {
+ // TODO: If c is not a URL code point and not "%", parse error.
+
+ if (
+ c === p("%") &&
+ (!isASCIIHex(this.input[this.pointer + 1]) ||
+ !isASCIIHex(this.input[this.pointer + 2]))
+ ) {
+ this.parseError = true;
+ }
+
+ this.buffer += utf8PercentEncodeCodePoint(c, isPathPercentEncode);
+ }
+
+ return true;
+ }
+
+ parseOpaquePath(c: number) {
+ if (c === p("?")) {
+ this.url.query = "";
+ this.state = "query";
+ } else if (c === p("#")) {
+ this.url.fragment = "";
+ this.state = "fragment";
+ } else {
+ // TODO: Add: not a URL code point
+ if (!isNaN(c) && c !== p("%")) {
+ this.parseError = true;
+ }
+
+ if (
+ c === p("%") &&
+ (!isASCIIHex(this.input[this.pointer + 1]) ||
+ !isASCIIHex(this.input[this.pointer + 2]))
+ ) {
+ this.parseError = true;
+ }
+
+ if (!isNaN(c)) {
+ // @ts-ignore
+ this.url.path += utf8PercentEncodeCodePoint(
+ c,
+ isC0ControlPercentEncode,
+ );
+ }
+ }
+
+ return true;
+ }
+
+ parseQuery(c: number, cStr: string) {
+ if (
+ !isSpecial(this.url) ||
+ this.url.scheme === "ws" ||
+ this.url.scheme === "wss"
+ ) {
+ this.encodingOverride = "utf-8";
+ }
+
+ if ((!this.stateOverride && c === p("#")) || isNaN(c)) {
+ const queryPercentEncodePredicate = isSpecial(this.url)
+ ? isSpecialQueryPercentEncode
+ : isQueryPercentEncode;
+ this.url.query += utf8PercentEncodeString(
+ this.buffer,
+ queryPercentEncodePredicate,
+ );
+
+ this.buffer = "";
+
+ if (c === p("#")) {
+ this.url.fragment = "";
+ this.state = "fragment";
+ }
+ } else if (!isNaN(c)) {
+ // TODO: If c is not a URL code point and not "%", parse error.
+
+ if (
+ c === p("%") &&
+ (!isASCIIHex(this.input[this.pointer + 1]) ||
+ !isASCIIHex(this.input[this.pointer + 2]))
+ ) {
+ this.parseError = true;
+ }
+
+ this.buffer += cStr;
+ }
+
+ return true;
+ }
+
+ parseFragment(c: number) {
+ if (!isNaN(c)) {
+ // TODO: If c is not a URL code point and not "%", parse error.
+ if (
+ c === p("%") &&
+ (!isASCIIHex(this.input[this.pointer + 1]) ||
+ !isASCIIHex(this.input[this.pointer + 2]))
+ ) {
+ this.parseError = true;
+ }
+
+ this.url.fragment += utf8PercentEncodeCodePoint(
+ c,
+ isFragmentPercentEncode,
+ );
+ }
+
+ return true;
+ }
+}
+
+const fileOtherwiseCodePoints = new Set([p("/"), p("\\"), p("?"), p("#")]);
+
+function startsWithWindowsDriveLetter(input: number[], pointer: number) {
+ const length = input.length - pointer;
+ return (
+ length >= 2 &&
+ isWindowsDriveLetterCodePoints(input[pointer], input[pointer + 1]) &&
+ (length === 2 || fileOtherwiseCodePoints.has(input[pointer + 2]))
+ );
+}
+
+function serializeURL(url: any, excludeFragment?: boolean) {
+ let output = `${url.scheme}:`;
+ if (url.host !== null) {
+ output += "//";
+
+ if (url.username !== "" || url.password !== "") {
+ output += url.username;
+ if (url.password !== "") {
+ output += `:${url.password}`;
+ }
+ output += "@";
+ }
+
+ output += serializeHost(url.host);
+
+ if (url.port !== null) {
+ output += `:${url.port}`;
+ }
+ }
+
+ if (
+ url.host === null &&
+ !hasAnOpaquePath(url) &&
+ url.path.length > 1 &&
+ url.path[0] === ""
+ ) {
+ output += "/.";
+ }
+ output += serializePath(url);
+
+ if (url.query !== null) {
+ output += `?${url.query}`;
+ }
+
+ if (!excludeFragment && url.fragment !== null) {
+ output += `#${url.fragment}`;
+ }
+
+ return output;
+}
+
+function serializeOrigin(tuple: {
+ scheme: string;
+ port: number;
+ host: number | number[] | string;
+}) {
+ let result = `${tuple.scheme}://`;
+ result += serializeHost(tuple.host);
+
+ if (tuple.port !== null) {
+ result += `:${tuple.port}`;
+ }
+
+ return result;
+}
+
+function serializePath(url: UrlObj): string {
+ if (typeof url.path === "string") {
+ return url.path;
+ }
+
+ let output = "";
+ for (const segment of url.path) {
+ output += `/${segment}`;
+ }
+ return output;
+}
+
+function serializeURLOrigin(url: any): any {
+ // https://url.spec.whatwg.org/#concept-url-origin
+ switch (url.scheme) {
+ case "blob":
+ try {
+ return serializeURLOrigin(parseURL(serializePath(url)));
+ } catch (e) {
+ // serializing an opaque origin returns "null"
+ return "null";
+ }
+ case "ftp":
+ case "http":
+ case "https":
+ case "ws":
+ case "wss":
+ return serializeOrigin({
+ scheme: url.scheme,
+ host: url.host,
+ port: url.port,
+ });
+ case "file":
+ // The spec says:
+ // > Unfortunate as it is, this is left as an exercise to the reader. When in doubt, return a new opaque origin.
+ // Browsers tested so far:
+ // - Chrome says "file://", but treats file: URLs as cross-origin for most (all?) purposes; see e.g.
+ // https://bugs.chromium.org/p/chromium/issues/detail?id=37586
+ // - Firefox says "null", but treats file: URLs as same-origin sometimes based on directory stuff; see
+ // https://developer.mozilla.org/en-US/docs/Archive/Misc_top_level/Same-origin_policy_for_file:_URIs
+ return "null";
+ default:
+ // serializing an opaque origin returns "null"
+ return "null";
+ }
+}
+
+export function basicURLParse(input: string, options?: any) {
+ if (options === undefined) {
+ options = {};
+ }
+
+ const usm = new URLStateMachine(
+ input,
+ options.baseURL,
+ options.encodingOverride,
+ options.url,
+ options.stateOverride,
+ );
+
+ if (usm.failure) {
+ return null;
+ }
+
+ return usm.url;
+}
+
+function setTheUsername(url: UrlObj, username: string) {
+ url.username = utf8PercentEncodeString(username, isUserinfoPercentEncode);
+}
+
+function setThePassword(url: UrlObj, password: string) {
+ url.password = utf8PercentEncodeString(password, isUserinfoPercentEncode);
+}
+
+function serializeInteger(integer: number) {
+ return String(integer);
+}
+
+function parseURL(
+ input: any,
+ options?: { baseURL?: any; encodingOverride?: any },
+) {
+ if (options === undefined) {
+ options = {};
+ }
+
+ // We don't handle blobs, so this just delegates:
+ return basicURLParse(input, {
+ baseURL: options.baseURL,
+ encodingOverride: options.encodingOverride,
+ });
+}
+
+export class URLImpl {
+ //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}`);
+ }
+
+ const query = parsedURL.query !== null ? parsedURL.query : "";
+
+ this._url = parsedURL;
+
+ // We cannot invoke the "new URLSearchParams object" algorithm without going through the constructor, which strips
+ // question mark by default. Therefore the doNotStripQMark hack is used.
+ this._query = new URLSearchParamsImpl(query, {
+ doNotStripQMark: true,
+ });
+ this._query._url = this;
+ }
+
+ get href() {
+ return serializeURL(this._url);
+ }
+
+ set href(v) {
+ const parsedURL = basicURLParse(v);
+ if (parsedURL === null) {
+ throw new TypeError(`Invalid URL: ${v}`);
+ }
+
+ this._url = parsedURL;
+
+ this._query._list.splice(0);
+ const { query } = parsedURL;
+ if (query !== null) {
+ this._query._list = parseUrlencodedString(query);
+ }
+ }
+
+ get origin() {
+ return serializeURLOrigin(this._url);
+ }
+
+ get protocol() {
+ return `${this._url.scheme}:`;
+ }
+
+ set protocol(v) {
+ basicURLParse(`${v}:`, {
+ url: this._url,
+ stateOverride: "scheme start",
+ });
+ }
+
+ get username() {
+ return this._url.username;
+ }
+
+ set username(v) {
+ if (cannotHaveAUsernamePasswordPort(this._url)) {
+ return;
+ }
+
+ setTheUsername(this._url, v);
+ }
+
+ get password() {
+ return this._url.password;
+ }
+
+ set password(v) {
+ if (cannotHaveAUsernamePasswordPort(this._url)) {
+ return;
+ }
+
+ setThePassword(this._url, v);
+ }
+
+ get host() {
+ const url = this._url;
+
+ if (url.host === null) {
+ return "";
+ }
+
+ if (url.port === null) {
+ return serializeHost(url.host);
+ }
+
+ return `${serializeHost(url.host)}:${serializeInteger(url.port)}`;
+ }
+
+ set host(v) {
+ if (hasAnOpaquePath(this._url)) {
+ return;
+ }
+
+ basicURLParse(v, { url: this._url, stateOverride: "host" });
+ }
+
+ get hostname() {
+ if (this._url.host === null) {
+ return "";
+ }
+
+ return serializeHost(this._url.host);
+ }
+
+ set hostname(v) {
+ if (hasAnOpaquePath(this._url)) {
+ return;
+ }
+
+ basicURLParse(v, { url: this._url, stateOverride: "hostname" });
+ }
+
+ get port() {
+ if (this._url.port === null) {
+ return "";
+ }
+
+ return serializeInteger(this._url.port);
+ }
+
+ set port(v) {
+ if (cannotHaveAUsernamePasswordPort(this._url)) {
+ return;
+ }
+
+ if (v === "") {
+ this._url.port = null;
+ } else {
+ basicURLParse(v, { url: this._url, stateOverride: "port" });
+ }
+ }
+
+ get pathname() {
+ return serializePath(this._url);
+ }
+
+ set pathname(v: string) {
+ if (hasAnOpaquePath(this._url)) {
+ return;
+ }
+
+ this._url.path = [];
+ basicURLParse(v, { url: this._url, stateOverride: "path start" });
+ }
+
+ get search() {
+ if (this._url.query === null || this._url.query === "") {
+ return "";
+ }
+
+ return `?${this._url.query}`;
+ }
+
+ set search(v) {
+ const url = this._url;
+
+ if (v === "") {
+ url.query = null;
+ this._query._list = [];
+ return;
+ }
+
+ const input = v[0] === "?" ? v.substring(1) : v;
+ url.query = "";
+ basicURLParse(input, { url, stateOverride: "query" });
+ this._query._list = parseUrlencodedString(input);
+ }
+
+ get searchParams() {
+ return this._query;
+ }
+
+ get hash() {
+ if (this._url.fragment === null || this._url.fragment === "") {
+ return "";
+ }
+
+ return `#${this._url.fragment}`;
+ }
+
+ set hash(v) {
+ if (v === "") {
+ this._url.fragment = null;
+ return;
+ }
+
+ const input = v[0] === "#" ? v.substring(1) : v;
+ this._url.fragment = "";
+ basicURLParse(input, { url: this._url, stateOverride: "fragment" });
+ }
+
+ toJSON() {
+ return this.href;
+ }
+
+ // FIXME: type!
+ _url: any;
+ _query: any;
+}