aboutsummaryrefslogtreecommitdiff
path: root/packages/taler-util/src
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2021-03-17 17:56:37 +0100
committerFlorian Dold <florian@dold.me>2021-03-17 17:56:37 +0100
commit07cdfb2e4ec761021477271776b81f33af0e731d (patch)
treecb62b1d1a04e1e64b8ee47e78196e858727d2c0a /packages/taler-util/src
parent42a4d666f42ce94274995bfdae644444ff5f6d53 (diff)
towards wallet-core / util split
Diffstat (limited to 'packages/taler-util/src')
-rw-r--r--packages/taler-util/src/ReserveStatus.ts57
-rw-r--r--packages/taler-util/src/ReserveTransaction.ts253
-rw-r--r--packages/taler-util/src/amounts.test.ts140
-rw-r--r--packages/taler-util/src/amounts.ts423
-rw-r--r--packages/taler-util/src/backupTypes.ts1306
-rw-r--r--packages/taler-util/src/codec.test.ts78
-rw-r--r--packages/taler-util/src/codec.ts419
-rw-r--r--packages/taler-util/src/helpers.ts152
-rw-r--r--packages/taler-util/src/index.ts19
-rw-r--r--packages/taler-util/src/libtool-version.test.ts48
-rw-r--r--packages/taler-util/src/libtool-version.ts88
-rw-r--r--packages/taler-util/src/notifications.ts271
-rw-r--r--packages/taler-util/src/payto.test.ts31
-rw-r--r--packages/taler-util/src/payto.ts71
-rw-r--r--packages/taler-util/src/taler-error-codes.ts1669
-rw-r--r--packages/taler-util/src/talerTypes.ts1457
-rw-r--r--packages/taler-util/src/talerconfig.test.ts124
-rw-r--r--packages/taler-util/src/talerconfig.ts318
-rw-r--r--packages/taler-util/src/taleruri.test.ts184
-rw-r--r--packages/taler-util/src/taleruri.ts220
-rw-r--r--packages/taler-util/src/time.ts264
-rw-r--r--packages/taler-util/src/transactionsTypes.ts364
-rw-r--r--packages/taler-util/src/types-test.ts93
-rw-r--r--packages/taler-util/src/url.ts74
-rw-r--r--packages/taler-util/src/walletTypes.ts936
25 files changed, 9059 insertions, 0 deletions
diff --git a/packages/taler-util/src/ReserveStatus.ts b/packages/taler-util/src/ReserveStatus.ts
new file mode 100644
index 000000000..cd204f55e
--- /dev/null
+++ b/packages/taler-util/src/ReserveStatus.ts
@@ -0,0 +1,57 @@
+/*
+ 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/>
+ */
+
+/**
+ * @author Florian Dold <dold@taler.net>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ codecForString,
+ buildCodecForObject,
+ codecForList,
+ Codec,
+} from "./codec.js";
+import { AmountString } from "./talerTypes";
+import {
+ ReserveTransaction,
+ codecForReserveTransaction,
+} from "./ReserveTransaction";
+
+/**
+ * Status of a reserve.
+ *
+ * Schema type for the exchange's response to "/reserve/status".
+ */
+export interface ReserveStatus {
+ /**
+ * Balance left in the reserve.
+ */
+ balance: AmountString;
+
+ /**
+ * Transaction history for the reserve.
+ */
+ history: ReserveTransaction[];
+}
+
+export const codecForReserveStatus = (): Codec<ReserveStatus> =>
+ buildCodecForObject<ReserveStatus>()
+ .property("balance", codecForString())
+ .property("history", codecForList(codecForReserveTransaction()))
+ .build("ReserveStatus");
diff --git a/packages/taler-util/src/ReserveTransaction.ts b/packages/taler-util/src/ReserveTransaction.ts
new file mode 100644
index 000000000..f477106b2
--- /dev/null
+++ b/packages/taler-util/src/ReserveTransaction.ts
@@ -0,0 +1,253 @@
+/*
+ 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 declarations for the exchange's reserve transaction information.
+ *
+ * @author Florian Dold <dold@taler.net>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ codecForString,
+ buildCodecForObject,
+ codecForConstString,
+ buildCodecForUnion,
+ Codec,
+ codecForNumber,
+} from "./codec.js";
+import {
+ AmountString,
+ Base32String,
+ EddsaSignatureString,
+ EddsaPublicKeyString,
+ CoinPublicKeyString,
+} from "./talerTypes";
+import { Timestamp, codecForTimestamp } from "./time.js";
+
+export enum ReserveTransactionType {
+ Withdraw = "WITHDRAW",
+ Credit = "CREDIT",
+ Recoup = "RECOUP",
+ Closing = "CLOSING",
+}
+
+export interface ReserveWithdrawTransaction {
+ type: ReserveTransactionType.Withdraw;
+
+ /**
+ * Amount withdrawn.
+ */
+ amount: AmountString;
+
+ /**
+ * Hash of the denomination public key of the coin.
+ */
+ h_denom_pub: Base32String;
+
+ /**
+ * Hash of the blinded coin to be signed
+ */
+ h_coin_envelope: Base32String;
+
+ /**
+ * Signature of 'TALER_WithdrawRequestPS' created with the reserves's
+ * private key.
+ */
+ reserve_sig: EddsaSignatureString;
+
+ /**
+ * Fee that is charged for withdraw.
+ */
+ withdraw_fee: AmountString;
+}
+
+export interface ReserveCreditTransaction {
+ type: ReserveTransactionType.Credit;
+
+ /**
+ * Amount withdrawn.
+ */
+ amount: AmountString;
+
+ /**
+ * Sender account payto://-URL
+ */
+ sender_account_url: string;
+
+ /**
+ * Transfer details uniquely identifying the transfer.
+ */
+ wire_reference: number;
+
+ /**
+ * Timestamp of the incoming wire transfer.
+ */
+ timestamp: Timestamp;
+}
+
+export interface ReserveClosingTransaction {
+ type: ReserveTransactionType.Closing;
+
+ /**
+ * Closing balance.
+ */
+ amount: AmountString;
+
+ /**
+ * Closing fee charged by the exchange.
+ */
+ closing_fee: AmountString;
+
+ /**
+ * Wire transfer subject.
+ */
+ wtid: string;
+
+ /**
+ * Hash of the wire account into which the funds were returned to.
+ */
+ h_wire: string;
+
+ /**
+ * This is a signature over a
+ * struct TALER_ReserveCloseConfirmationPS with purpose
+ * TALER_SIGNATURE_EXCHANGE_RESERVE_CLOSED.
+ */
+ exchange_sig: EddsaSignatureString;
+
+ /**
+ * Public key used to create exchange_sig.
+ */
+ exchange_pub: EddsaPublicKeyString;
+
+ /**
+ * Time when the reserve was closed.
+ */
+ timestamp: Timestamp;
+}
+
+export interface ReserveRecoupTransaction {
+ type: ReserveTransactionType.Recoup;
+
+ /**
+ * Amount paid back.
+ */
+ amount: AmountString;
+
+ /**
+ * This is a signature over
+ * a struct TALER_PaybackConfirmationPS with purpose
+ * TALER_SIGNATURE_EXCHANGE_CONFIRM_PAYBACK.
+ */
+ exchange_sig: EddsaSignatureString;
+
+ /**
+ * Public key used to create exchange_sig.
+ */
+ exchange_pub: EddsaPublicKeyString;
+
+ /**
+ * Time when the funds were paid back into the reserve.
+ */
+ timestamp: Timestamp;
+
+ /**
+ * Public key of the coin that was paid back.
+ */
+ coin_pub: CoinPublicKeyString;
+}
+
+/**
+ * Format of the exchange's transaction history for a reserve.
+ */
+export type ReserveTransaction =
+ | ReserveWithdrawTransaction
+ | ReserveCreditTransaction
+ | ReserveClosingTransaction
+ | ReserveRecoupTransaction;
+
+export const codecForReserveWithdrawTransaction = (): Codec<
+ ReserveWithdrawTransaction
+> =>
+ buildCodecForObject<ReserveWithdrawTransaction>()
+ .property("amount", codecForString())
+ .property("h_coin_envelope", codecForString())
+ .property("h_denom_pub", codecForString())
+ .property("reserve_sig", codecForString())
+ .property("type", codecForConstString(ReserveTransactionType.Withdraw))
+ .property("withdraw_fee", codecForString())
+ .build("ReserveWithdrawTransaction");
+
+export const codecForReserveCreditTransaction = (): Codec<
+ ReserveCreditTransaction
+> =>
+ buildCodecForObject<ReserveCreditTransaction>()
+ .property("amount", codecForString())
+ .property("sender_account_url", codecForString())
+ .property("timestamp", codecForTimestamp)
+ .property("wire_reference", codecForNumber())
+ .property("type", codecForConstString(ReserveTransactionType.Credit))
+ .build("ReserveCreditTransaction");
+
+export const codecForReserveClosingTransaction = (): Codec<
+ ReserveClosingTransaction
+> =>
+ buildCodecForObject<ReserveClosingTransaction>()
+ .property("amount", codecForString())
+ .property("closing_fee", codecForString())
+ .property("exchange_pub", codecForString())
+ .property("exchange_sig", codecForString())
+ .property("h_wire", codecForString())
+ .property("timestamp", codecForTimestamp)
+ .property("type", codecForConstString(ReserveTransactionType.Closing))
+ .property("wtid", codecForString())
+ .build("ReserveClosingTransaction");
+
+export const codecForReserveRecoupTransaction = (): Codec<
+ ReserveRecoupTransaction
+> =>
+ buildCodecForObject<ReserveRecoupTransaction>()
+ .property("amount", codecForString())
+ .property("coin_pub", codecForString())
+ .property("exchange_pub", codecForString())
+ .property("exchange_sig", codecForString())
+ .property("timestamp", codecForTimestamp)
+ .property("type", codecForConstString(ReserveTransactionType.Recoup))
+ .build("ReserveRecoupTransaction");
+
+export const codecForReserveTransaction = (): Codec<ReserveTransaction> =>
+ buildCodecForUnion<ReserveTransaction>()
+ .discriminateOn("type")
+ .alternative(
+ ReserveTransactionType.Withdraw,
+ codecForReserveWithdrawTransaction(),
+ )
+ .alternative(
+ ReserveTransactionType.Closing,
+ codecForReserveClosingTransaction(),
+ )
+ .alternative(
+ ReserveTransactionType.Recoup,
+ codecForReserveRecoupTransaction(),
+ )
+ .alternative(
+ ReserveTransactionType.Credit,
+ codecForReserveCreditTransaction(),
+ )
+ .build<ReserveTransaction>("ReserveTransaction");
diff --git a/packages/taler-util/src/amounts.test.ts b/packages/taler-util/src/amounts.test.ts
new file mode 100644
index 000000000..d9734ce82
--- /dev/null
+++ b/packages/taler-util/src/amounts.test.ts
@@ -0,0 +1,140 @@
+/*
+ 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 test from "ava";
+
+import { Amounts, AmountJson } from "./amounts.js";
+
+const jAmt = (
+ value: number,
+ fraction: number,
+ currency: string,
+): AmountJson => ({ value, fraction, currency });
+
+const sAmt = (s: string): AmountJson => Amounts.parseOrThrow(s);
+
+test("amount addition (simple)", (t) => {
+ const a1 = jAmt(1, 0, "EUR");
+ const a2 = jAmt(1, 0, "EUR");
+ const a3 = jAmt(2, 0, "EUR");
+ t.true(0 === Amounts.cmp(Amounts.add(a1, a2).amount, a3));
+ t.pass();
+});
+
+test("amount addition (saturation)", (t) => {
+ const a1 = jAmt(1, 0, "EUR");
+ const res = Amounts.add(jAmt(Amounts.maxAmountValue, 0, "EUR"), a1);
+ t.true(res.saturated);
+ t.pass();
+});
+
+test("amount subtraction (simple)", (t) => {
+ const a1 = jAmt(2, 5, "EUR");
+ const a2 = jAmt(1, 0, "EUR");
+ const a3 = jAmt(1, 5, "EUR");
+ t.true(0 === Amounts.cmp(Amounts.sub(a1, a2).amount, a3));
+ t.pass();
+});
+
+test("amount subtraction (saturation)", (t) => {
+ const a1 = jAmt(0, 0, "EUR");
+ const a2 = jAmt(1, 0, "EUR");
+ let res = Amounts.sub(a1, a2);
+ t.true(res.saturated);
+ res = Amounts.sub(a1, a1);
+ t.true(!res.saturated);
+ t.pass();
+});
+
+test("amount comparison", (t) => {
+ t.is(Amounts.cmp(jAmt(1, 0, "EUR"), jAmt(1, 0, "EUR")), 0);
+ t.is(Amounts.cmp(jAmt(1, 1, "EUR"), jAmt(1, 0, "EUR")), 1);
+ t.is(Amounts.cmp(jAmt(1, 1, "EUR"), jAmt(1, 2, "EUR")), -1);
+ t.is(Amounts.cmp(jAmt(1, 0, "EUR"), jAmt(0, 0, "EUR")), 1);
+ t.is(Amounts.cmp(jAmt(0, 0, "EUR"), jAmt(1, 0, "EUR")), -1);
+ t.is(Amounts.cmp(jAmt(1, 0, "EUR"), jAmt(0, 100000000, "EUR")), 0);
+ t.throws(() => Amounts.cmp(jAmt(1, 0, "FOO"), jAmt(1, 0, "BAR")));
+ t.pass();
+});
+
+test("amount parsing", (t) => {
+ t.is(
+ Amounts.cmp(Amounts.parseOrThrow("TESTKUDOS:0"), jAmt(0, 0, "TESTKUDOS")),
+ 0,
+ );
+ t.is(
+ Amounts.cmp(Amounts.parseOrThrow("TESTKUDOS:10"), jAmt(10, 0, "TESTKUDOS")),
+ 0,
+ );
+ t.is(
+ Amounts.cmp(
+ Amounts.parseOrThrow("TESTKUDOS:0.1"),
+ jAmt(0, 10000000, "TESTKUDOS"),
+ ),
+ 0,
+ );
+ t.is(
+ Amounts.cmp(
+ Amounts.parseOrThrow("TESTKUDOS:0.00000001"),
+ jAmt(0, 1, "TESTKUDOS"),
+ ),
+ 0,
+ );
+ t.is(
+ Amounts.cmp(
+ Amounts.parseOrThrow("TESTKUDOS:4503599627370496.99999999"),
+ jAmt(4503599627370496, 99999999, "TESTKUDOS"),
+ ),
+ 0,
+ );
+ t.throws(() => Amounts.parseOrThrow("foo:"));
+ t.throws(() => Amounts.parseOrThrow("1.0"));
+ t.throws(() => Amounts.parseOrThrow("42"));
+ t.throws(() => Amounts.parseOrThrow(":1.0"));
+ t.throws(() => Amounts.parseOrThrow(":42"));
+ t.throws(() => Amounts.parseOrThrow("EUR:.42"));
+ t.throws(() => Amounts.parseOrThrow("EUR:42."));
+ t.throws(() => Amounts.parseOrThrow("TESTKUDOS:4503599627370497.99999999"));
+ t.is(
+ Amounts.cmp(
+ Amounts.parseOrThrow("TESTKUDOS:0.99999999"),
+ jAmt(0, 99999999, "TESTKUDOS"),
+ ),
+ 0,
+ );
+ t.throws(() => Amounts.parseOrThrow("TESTKUDOS:0.999999991"));
+ t.pass();
+});
+
+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");
+ // denormalized
+ t.is(Amounts.stringify(jAmt(1, 100000000, "TESTKUDOS")), "TESTKUDOS:2");
+ 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");
+});
diff --git a/packages/taler-util/src/amounts.ts b/packages/taler-util/src/amounts.ts
new file mode 100644
index 000000000..8984db11b
--- /dev/null
+++ b/packages/taler-util/src/amounts.ts
@@ -0,0 +1,423 @@
+/*
+ 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/>
+ */
+
+/**
+ * Types and helper functions for dealing with Taler amounts.
+ */
+
+/**
+ * Imports.
+ */
+import {
+ buildCodecForObject,
+ codecForString,
+ codecForNumber,
+ Codec,
+} from "./codec";
+import { AmountString } from "./talerTypes";
+
+/**
+ * Number of fractional units that one value unit represents.
+ */
+export const fractionalBase = 1e8;
+
+/**
+ * How many digits behind the comma are required to represent the
+ * fractional value in human readable decimal format? Must match
+ * lg(fractionalBase)
+ */
+export const fractionalLength = 8;
+
+/**
+ * Maximum allowed value field of an amount.
+ */
+export const maxAmountValue = 2 ** 52;
+
+/**
+ * Non-negative financial amount. Fractional values are expressed as multiples
+ * of 1e-8.
+ */
+export interface AmountJson {
+ /**
+ * Value, must be an integer.
+ */
+ readonly value: number;
+
+ /**
+ * Fraction, must be an integer. Represent 1/1e8 of a unit.
+ */
+ readonly fraction: number;
+
+ /**
+ * Currency of the amount.
+ */
+ readonly currency: string;
+}
+
+export const codecForAmountJson = (): Codec<AmountJson> =>
+ buildCodecForObject<AmountJson>()
+ .property("currency", codecForString())
+ .property("value", codecForNumber())
+ .property("fraction", codecForNumber())
+ .build("AmountJson");
+
+export const codecForAmountString = (): Codec<AmountString> => codecForString();
+
+/**
+ * Result of a possibly overflowing operation.
+ */
+export interface Result {
+ /**
+ * Resulting, possibly saturated amount.
+ */
+ amount: AmountJson;
+ /**
+ * Was there an over-/underflow?
+ */
+ saturated: boolean;
+}
+
+/**
+ * Get an amount that represents zero units of a currency.
+ */
+export function getZero(currency: string): AmountJson {
+ return {
+ currency,
+ fraction: 0,
+ value: 0,
+ };
+}
+
+export type AmountLike = AmountString | AmountJson;
+
+export function jsonifyAmount(amt: AmountLike): AmountJson {
+ if (typeof amt === "string") {
+ return parseOrThrow(amt);
+ }
+ return amt;
+}
+
+export function sum(amounts: AmountLike[]): Result {
+ if (amounts.length <= 0) {
+ throw Error("can't sum zero amounts");
+ }
+ const jsonAmounts = amounts.map((x) => jsonifyAmount(x));
+ return add(jsonAmounts[0], ...jsonAmounts.slice(1));
+}
+
+/**
+ * Add two amounts. Return the result and whether
+ * the addition overflowed. The overflow is always handled
+ * by saturating and never by wrapping.
+ *
+ * Throws when currencies don't match.
+ */
+export function add(first: AmountJson, ...rest: AmountJson[]): Result {
+ const currency = first.currency;
+ let value = first.value + Math.floor(first.fraction / fractionalBase);
+ if (value > maxAmountValue) {
+ return {
+ amount: { currency, value: maxAmountValue, fraction: fractionalBase - 1 },
+ saturated: true,
+ };
+ }
+ let fraction = first.fraction % fractionalBase;
+ for (const x of rest) {
+ if (x.currency !== currency) {
+ throw Error(`Mismatched currency: ${x.currency} and ${currency}`);
+ }
+
+ value =
+ value + x.value + Math.floor((fraction + x.fraction) / fractionalBase);
+ fraction = Math.floor((fraction + x.fraction) % fractionalBase);
+ if (value > maxAmountValue) {
+ return {
+ amount: {
+ currency,
+ value: maxAmountValue,
+ fraction: fractionalBase - 1,
+ },
+ saturated: true,
+ };
+ }
+ }
+ return { amount: { currency, value, fraction }, saturated: false };
+}
+
+/**
+ * Subtract two amounts. Return the result and whether
+ * the subtraction overflowed. The overflow is always handled
+ * by saturating and never by wrapping.
+ *
+ * Throws when currencies don't match.
+ */
+export function sub(a: AmountJson, ...rest: AmountJson[]): Result {
+ const currency = a.currency;
+ let value = a.value;
+ let fraction = a.fraction;
+
+ for (const b of rest) {
+ if (b.currency !== currency) {
+ throw Error(`Mismatched currency: ${b.currency} and ${currency}`);
+ }
+ if (fraction < b.fraction) {
+ if (value < 1) {
+ return { amount: { currency, value: 0, fraction: 0 }, saturated: true };
+ }
+ value--;
+ fraction += fractionalBase;
+ }
+ console.assert(fraction >= b.fraction);
+ fraction -= b.fraction;
+ if (value < b.value) {
+ return { amount: { currency, value: 0, fraction: 0 }, saturated: true };
+ }
+ value -= b.value;
+ }
+
+ return { amount: { currency, value, fraction }, saturated: false };
+}
+
+/**
+ * Compare two amounts. Returns 0 when equal, -1 when a < b
+ * and +1 when a > b. Throws when currencies don't match.
+ */
+export function cmp(a: AmountLike, b: AmountLike): -1 | 0 | 1 {
+ a = jsonifyAmount(a);
+ b = jsonifyAmount(b);
+ if (a.currency !== b.currency) {
+ throw Error(`Mismatched currency: ${a.currency} and ${b.currency}`);
+ }
+ const av = a.value + Math.floor(a.fraction / fractionalBase);
+ const af = a.fraction % fractionalBase;
+ const bv = b.value + Math.floor(b.fraction / fractionalBase);
+ const bf = b.fraction % fractionalBase;
+ switch (true) {
+ case av < bv:
+ return -1;
+ case av > bv:
+ return 1;
+ case af < bf:
+ return -1;
+ case af > bf:
+ return 1;
+ case af === bf:
+ return 0;
+ default:
+ throw Error("assertion failed");
+ }
+}
+
+/**
+ * Create a copy of an amount.
+ */
+export function copy(a: AmountJson): AmountJson {
+ return {
+ currency: a.currency,
+ fraction: a.fraction,
+ value: a.value,
+ };
+}
+
+/**
+ * Divide an amount. Throws on division by zero.
+ */
+export function divide(a: AmountJson, n: number): AmountJson {
+ if (n === 0) {
+ throw Error(`Division by 0`);
+ }
+ if (n === 1) {
+ return { value: a.value, fraction: a.fraction, currency: a.currency };
+ }
+ const r = a.value % n;
+ return {
+ currency: a.currency,
+ fraction: Math.floor((r * fractionalBase + a.fraction) / n),
+ value: Math.floor(a.value / n),
+ };
+}
+
+/**
+ * Check if an amount is non-zero.
+ */
+export function isNonZero(a: AmountJson): boolean {
+ return a.value > 0 || a.fraction > 0;
+}
+
+export function isZero(a: AmountLike): boolean {
+ a = jsonifyAmount(a);
+ return a.value === 0 && a.fraction === 0;
+}
+
+/**
+ * Parse an amount like 'EUR:20.5' for 20 Euros and 50 ct.
+ */
+export function parse(s: string): AmountJson | undefined {
+ const res = s.match(/^([a-zA-Z0-9_*-]+):([0-9]+)([.][0-9]+)?$/);
+ if (!res) {
+ return undefined;
+ }
+ const tail = res[3] || ".0";
+ if (tail.length > fractionalLength + 1) {
+ return undefined;
+ }
+ const value = Number.parseInt(res[2]);
+ if (value > maxAmountValue) {
+ return undefined;
+ }
+ return {
+ currency: res[1],
+ fraction: Math.round(fractionalBase * Number.parseFloat(tail)),
+ value,
+ };
+}
+
+/**
+ * Parse amount in standard string form (like 'EUR:20.5'),
+ * throw if the input is not a valid amount.
+ */
+export function parseOrThrow(s: string): AmountJson {
+ const res = parse(s);
+ if (!res) {
+ throw Error(`Can't parse amount: "${s}"`);
+ }
+ return res;
+}
+
+/**
+ * Convert a float to a Taler amount.
+ * Loss of precision possible.
+ */
+export function fromFloat(floatVal: number, currency: string): AmountJson {
+ return {
+ currency,
+ fraction: Math.floor((floatVal - Math.floor(floatVal)) * fractionalBase),
+ value: Math.floor(floatVal),
+ };
+}
+
+/**
+ * Convert to standard human-readable string representation that's
+ * also used in JSON formats.
+ */
+export function stringify(a: AmountLike): string {
+ a = jsonifyAmount(a);
+ const av = a.value + Math.floor(a.fraction / fractionalBase);
+ const af = a.fraction % fractionalBase;
+ let s = av.toString();
+
+ if (af) {
+ s = s + ".";
+ let n = af;
+ for (let i = 0; i < fractionalLength; i++) {
+ if (!n) {
+ break;
+ }
+ s = s + Math.floor((n / fractionalBase) * 10).toString();
+ n = (n * 10) % fractionalBase;
+ }
+ }
+
+ return `${a.currency}:${s}`;
+}
+
+/**
+ * Check if the argument is a valid amount in string form.
+ */
+function check(a: any): boolean {
+ if (typeof a !== "string") {
+ return false;
+ }
+ try {
+ const parsedAmount = parse(a);
+ return !!parsedAmount;
+ } catch {
+ return false;
+ }
+}
+
+function mult(a: AmountJson, n: number): Result {
+ if (!Number.isInteger(n)) {
+ throw Error("amount can only be multipied by an integer");
+ }
+ if (n < 0) {
+ throw Error("amount can only be multiplied by a positive integer");
+ }
+ if (n == 0) {
+ return { amount: getZero(a.currency), saturated: false };
+ }
+ let x = a;
+ let acc = getZero(a.currency);
+ while (n > 1) {
+ if (n % 2 == 0) {
+ n = n / 2;
+ } else {
+ n = (n - 1) / 2;
+ const r2 = add(acc, x);
+ if (r2.saturated) {
+ return r2;
+ }
+ acc = r2.amount;
+ }
+ const r2 = add(x, x);
+ if (r2.saturated) {
+ return r2;
+ }
+ x = r2.amount;
+ }
+ return add(acc, x);
+}
+
+function max(a: AmountLike, b: AmountLike): AmountJson {
+ const cr = Amounts.cmp(a, b);
+ if (cr >= 0) {
+ return jsonifyAmount(a);
+ } else {
+ return jsonifyAmount(b);
+ }
+}
+
+function min(a: AmountLike, b: AmountLike): AmountJson {
+ const cr = Amounts.cmp(a, b);
+ if (cr >= 0) {
+ return jsonifyAmount(b);
+ } else {
+ return jsonifyAmount(a);
+ }
+}
+
+
+// Export all amount-related functions here for better IDE experience.
+export const Amounts = {
+ stringify: stringify,
+ parse: parse,
+ parseOrThrow: parseOrThrow,
+ cmp: cmp,
+ add: add,
+ sum: sum,
+ sub: sub,
+ mult: mult,
+ max: max,
+ min: min,
+ check: check,
+ getZero: getZero,
+ isZero: isZero,
+ maxAmountValue: maxAmountValue,
+ fromFloat: fromFloat,
+ copy: copy,
+ fractionalBase: fractionalBase,
+ divide: divide,
+};
diff --git a/packages/taler-util/src/backupTypes.ts b/packages/taler-util/src/backupTypes.ts
new file mode 100644
index 000000000..b31b8e58b
--- /dev/null
+++ b/packages/taler-util/src/backupTypes.ts
@@ -0,0 +1,1306 @@
+/*
+ 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 { Duration, Timestamp } 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;
+
+/**
+ * Lamport clock timestamp.
+ */
+export interface ClockStamp {
+ deviceId: string;
+ value: number;
+}
+
+/**
+ * Contract terms JSON.
+ */
+type RawContractTerms = any;
+
+/**
+ * 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;
+
+ /**
+ * Monotonically increasing clock of the wallet,
+ * used to determine causality when merging backups.
+ *
+ * Information about other clocks, used to delete
+ * tombstones in the hopefully rare case that multiple wallets
+ * are connected to the same sync server.
+ */
+ clocks: { [device_id: string]: number };
+
+ /**
+ * Timestamp of the backup.
+ *
+ * This timestamp should only be advanced if the content
+ * of the backup changes.
+ */
+ timestamp: Timestamp;
+
+ /**
+ * Per-exchange data sorted by exchange master public key.
+ *
+ * Sorted by the exchange public key.
+ */
+ exchanges: BackupExchange[];
+
+ /**
+ * 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.
+ *
+ * Sorted by the provider base URL.
+ */
+ backup_providers: BackupBackupProvider[];
+
+ /**
+ * Recoup groups.
+ */
+ recoup_groups: BackupRecoupGroup[];
+
+ /**
+ * Tombstones for deleting purchases.
+ */
+ purchase_tombstones: {
+ /**
+ * Clock when the purchase was deleted
+ */
+ clock_deleted: ClockStamp;
+
+ /**
+ * Proposal ID identifying the purchase.
+ */
+ proposal_id: string;
+ }[];
+
+ /**
+ * 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[];
+}
+
+/**
+ * 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;
+
+ /**
+ * Clock when the auditor trust has been added.
+ *
+ * Can be undefined if this entry represents a removal delta
+ * from the wallet's defaults.
+ */
+ clock_added?: ClockStamp;
+
+ /**
+ * Clock for when the auditor trust has been removed.
+ */
+ clock_removed?: ClockStamp;
+}
+
+/**
+ * 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;
+
+ /**
+ * Clock when the exchange trust has been added.
+ *
+ * Can be undefined if this entry represents a removal delta
+ * from the wallet's defaults.
+ */
+ clock_added?: ClockStamp;
+
+ /**
+ * Clock for when the exchange trust has been removed.
+ */
+ clock_removed?: ClockStamp;
+}
+
+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[];
+}
+
+/**
+ * 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: Timestamp;
+
+ timestamp_finish?: Timestamp;
+ finish_clock?: Timestamp;
+ 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: string;
+
+ /**
+ * 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;
+
+ /**
+ * Clock for the last update to current_amount/fresh.
+ */
+ last_clock?: ClockStamp;
+}
+
+/**
+ * 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: Timestamp | undefined;
+
+ /**
+ * When was the tip first scanned by the wallet?
+ */
+ timestamp_created: Timestamp;
+
+ timestamp_finished?: Timestamp;
+ finish_clock?: ClockStamp;
+ 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: Timestamp;
+
+ /**
+ * 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;
+
+ selected_denoms_clock?: ClockStamp;
+}
+
+/**
+ * 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: Timestamp;
+
+ timestamp_finish?: Timestamp;
+ finish_clock?: ClockStamp;
+ 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: Timestamp;
+
+ timestamp_finish?: Timestamp;
+ finish_clock?: ClockStamp;
+ 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_clock?: ClockStamp;
+}
+
+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: Timestamp;
+
+ /**
+ * Time when the wallet became aware of the refund.
+ */
+ obtained_time: Timestamp;
+
+ /**
+ * 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;
+
+ last_clock?: ClockStamp;
+}
+
+/**
+ * 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;
+ }[];
+
+ /**
+ * Clock when the pay coin selection was made/updated.
+ */
+ pay_coins_clock?: ClockStamp;
+
+ /**
+ * 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: Timestamp | 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: Timestamp;
+
+ /**
+ * Pending refunds for the purchase. A refund is pending
+ * when the merchant reports a transient error from the exchange.
+ */
+ refunds: BackupRefundItem[];
+
+ /**
+ * Is the purchase considered defunct (either during payment
+ * or during abort if abort_status is set).
+ */
+ defunct?: boolean;
+
+ /**
+ * Clock for last update to defunct status.
+ */
+ defunct_clock?: ClockStamp;
+
+ /**
+ * Abort status of the payment.
+ */
+ abort_status?: "abort-refund" | "abort-finished";
+
+ /**
+ * Continue querying the refund status until this deadline has expired.
+ */
+ auto_refund_deadline: Timestamp | 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: string;
+
+ /**
+ * 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: Timestamp;
+
+ /**
+ * Date after which the currency can't be withdrawn anymore.
+ */
+ stamp_expire_withdraw: Timestamp;
+
+ /**
+ * Date after the denomination officially doesn't exist anymore.
+ */
+ stamp_expire_legal: Timestamp;
+
+ /**
+ * Data after which coins of this denomination can't be deposited anymore.
+ */
+ stamp_expire_deposit: Timestamp;
+
+ /**
+ * 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[];
+}
+
+/**
+ * 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: Timestamp;
+
+ /**
+ * Timestamp of the last observed activity.
+ *
+ * Used to compute when to give up querying the exchange.
+ */
+ timestamp_last_activity: Timestamp;
+
+ /**
+ * Timestamp of when the reserve closed.
+ *
+ * Note that the last activity can be after the closing time
+ * due to recouping.
+ */
+ timestamp_closed?: Timestamp;
+
+ /**
+ * Wire information (as payto URI) for the bank account that
+ * transfered 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: Timestamp | undefined;
+
+ /**
+ * Time when the reserve was confirmed by the bank.
+ *
+ * Set to undefined if not confirmed yet.
+ */
+ timestamp_bank_confirmed: Timestamp | 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[];
+
+ defective?: boolean;
+ defective_clock?: ClockStamp;
+}
+
+/**
+ * Wire fee for one wire payment target type as stored in the
+ * wallet's database.
+ *
+ * (Flattened to a list to make the declaration simpler).
+ */
+export interface BackupExchangeWireFee {
+ wire_type: string;
+
+ /**
+ * Fee for wire transfers.
+ */
+ wire_fee: string;
+
+ /**
+ * Fees to close and refund a reserve.
+ */
+ closing_fee: string;
+
+ /**
+ * Start date of the fee.
+ */
+ start_stamp: Timestamp;
+
+ /**
+ * End date of the fee.
+ */
+ end_stamp: Timestamp;
+
+ /**
+ * Signature made by the exchange master key.
+ */
+ sig: string;
+}
+
+/**
+ * Structure of one exchange signing key in the /keys response.
+ */
+export class BackupExchangeSignKey {
+ stamp_start: Timestamp;
+ stamp_expire: Timestamp;
+ stamp_end: Timestamp;
+ 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 about an exchange.
+ */
+export interface BackupExchange {
+ /**
+ * 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: Duration;
+
+ /**
+ * 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_etag_last: string | undefined;
+
+ /**
+ * ETag for last terms of service download.
+ */
+ tos_etag_accepted: string | undefined;
+
+ /**
+ * Clock value of the last update.
+ */
+ last_clock?: ClockStamp;
+
+ /**
+ * Should this exchange be considered defective?
+ */
+ defective?: boolean;
+
+ defective_clock?: ClockStamp;
+}
+
+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: Timestamp;
+
+ /**
+ * 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_status_clock?: ClockStamp;
+
+ /**
+ * 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;
+ }[];
+} \ No newline at end of file
diff --git a/packages/taler-util/src/codec.test.ts b/packages/taler-util/src/codec.test.ts
new file mode 100644
index 000000000..f8f4c797c
--- /dev/null
+++ b/packages/taler-util/src/codec.test.ts
@@ -0,0 +1,78 @@
+/*
+ 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 {
+ Codec,
+ buildCodecForObject,
+ codecForConstString,
+ codecForString,
+ buildCodecForUnion,
+} from "./codec";
+
+interface MyObj {
+ foo: string;
+}
+
+interface AltOne {
+ type: "one";
+ foo: string;
+}
+
+interface AltTwo {
+ type: "two";
+ bar: string;
+}
+
+type MyUnion = AltOne | AltTwo;
+
+test("basic codec", (t) => {
+ const myObjCodec = buildCodecForObject<MyObj>()
+ .property("foo", codecForString())
+ .build("MyObj");
+ const res = myObjCodec.decode({ foo: "hello" });
+ t.assert(res.foo === "hello");
+
+ t.throws(() => {
+ myObjCodec.decode({ foo: 123 });
+ });
+});
+
+test("union", (t) => {
+ const altOneCodec: Codec<AltOne> = buildCodecForObject<AltOne>()
+ .property("type", codecForConstString("one"))
+ .property("foo", codecForString())
+ .build("AltOne");
+ const altTwoCodec: Codec<AltTwo> = buildCodecForObject<AltTwo>()
+ .property("type", codecForConstString("two"))
+ .property("bar", codecForString())
+ .build("AltTwo");
+ const myUnionCodec: Codec<MyUnion> = buildCodecForUnion<MyUnion>()
+ .discriminateOn("type")
+ .alternative("one", altOneCodec)
+ .alternative("two", altTwoCodec)
+ .build<MyUnion>("MyUnion");
+
+ const res = myUnionCodec.decode({ type: "one", foo: "bla" });
+ t.is(res.type, "one");
+ if (res.type == "one") {
+ t.is(res.foo, "bla");
+ }
+});
diff --git a/packages/taler-util/src/codec.ts b/packages/taler-util/src/codec.ts
new file mode 100644
index 000000000..8605ff335
--- /dev/null
+++ b/packages/taler-util/src/codec.ts
@@ -0,0 +1,419 @@
+/*
+ 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.
+ */
+
+/* eslint-disable @typescript-eslint/ban-types */
+
+/**
+ * Error thrown when decoding fails.
+ */
+export class DecodingError extends Error {
+ constructor(message: string) {
+ super(message);
+ Object.setPrototypeOf(this, DecodingError.prototype);
+ this.name = "DecodingError";
+ }
+}
+
+/**
+ * Context information to show nicer error messages when decoding fails.
+ */
+export interface Context {
+ readonly path?: string[];
+}
+
+export function renderContext(c?: Context): string {
+ const p = c?.path;
+ if (p) {
+ return p.join(".");
+ } else {
+ return "(unknown)";
+ }
+}
+
+function joinContext(c: Context | undefined, part: string): Context {
+ const path = c?.path ?? [];
+ return {
+ path: path.concat([part]),
+ };
+}
+
+/**
+ * A codec converts untyped JSON to a typed object.
+ */
+export interface Codec<V> {
+ /**
+ * Decode untyped JSON to an object of type [[V]].
+ */
+ readonly decode: (x: any, c?: Context) => V;
+}
+
+type SingletonRecord<K extends keyof any, V> = { [Y in K]: V };
+
+interface Prop {
+ name: string;
+ codec: Codec<any>;
+}
+
+interface Alternative {
+ tagValue: any;
+ codec: Codec<any>;
+}
+
+class ObjectCodecBuilder<OutputType, PartialOutputType> {
+ private propList: Prop[] = [];
+
+ /**
+ * Define a property for the object.
+ */
+ property<K extends keyof OutputType & string, V extends OutputType[K]>(
+ x: K,
+ codec: Codec<V>,
+ ): ObjectCodecBuilder<OutputType, PartialOutputType & SingletonRecord<K, V>> {
+ if (!codec) {
+ throw Error("inner codec must be defined");
+ }
+ this.propList.push({ name: x, codec: codec });
+ return this as any;
+ }
+
+ /**
+ * Return the built codec.
+ *
+ * @param objectDisplayName name of the object that this codec operates on,
+ * used in error messages.
+ */
+ build(objectDisplayName: string): Codec<PartialOutputType> {
+ const propList = this.propList;
+ return {
+ decode(x: any, c?: Context): PartialOutputType {
+ if (!c) {
+ c = {
+ path: [`(${objectDisplayName})`],
+ };
+ }
+ if (typeof x !== "object") {
+ throw new DecodingError(
+ `expected object for ${objectDisplayName} at ${renderContext(
+ c,
+ )} but got ${typeof x}`,
+ );
+ }
+ const obj: any = {};
+ for (const prop of propList) {
+ const propRawVal = x[prop.name];
+ const propVal = prop.codec.decode(
+ propRawVal,
+ joinContext(c, prop.name),
+ );
+ obj[prop.name] = propVal;
+ }
+ return obj as PartialOutputType;
+ },
+ };
+ }
+}
+
+class UnionCodecBuilder<
+ TargetType,
+ TagPropertyLabel extends keyof TargetType,
+ CommonBaseType,
+ PartialTargetType
+> {
+ private alternatives = new Map<any, Alternative>();
+
+ constructor(
+ private discriminator: TagPropertyLabel,
+ private baseCodec?: Codec<CommonBaseType>,
+ ) {}
+
+ /**
+ * Define a property for the object.
+ */
+ alternative<V>(
+ tagValue: TargetType[TagPropertyLabel],
+ codec: Codec<V>,
+ ): UnionCodecBuilder<
+ TargetType,
+ TagPropertyLabel,
+ CommonBaseType,
+ PartialTargetType | V
+ > {
+ if (!codec) {
+ throw Error("inner codec must be defined");
+ }
+ this.alternatives.set(tagValue, { codec, tagValue });
+ return this as any;
+ }
+
+ /**
+ * Return the built codec.
+ *
+ * @param objectDisplayName name of the object that this codec operates on,
+ * used in error messages.
+ */
+ build<R extends PartialTargetType & CommonBaseType = never>(
+ objectDisplayName: string,
+ ): Codec<R> {
+ const alternatives = this.alternatives;
+ const discriminator = this.discriminator;
+ const baseCodec = this.baseCodec;
+ return {
+ decode(x: any, c?: Context): R {
+ if (!c) {
+ c = {
+ path: [`(${objectDisplayName})`],
+ };
+ }
+ const d = x[discriminator];
+ if (d === undefined) {
+ throw new DecodingError(
+ `expected tag for ${objectDisplayName} at ${renderContext(
+ c,
+ )}.${discriminator}`,
+ );
+ }
+ const alt = alternatives.get(d);
+ if (!alt) {
+ throw new DecodingError(
+ `unknown tag for ${objectDisplayName} ${d} at ${renderContext(
+ c,
+ )}.${discriminator}`,
+ );
+ }
+ const altDecoded = alt.codec.decode(x);
+ if (baseCodec) {
+ const baseDecoded = baseCodec.decode(x, c);
+ return { ...baseDecoded, ...altDecoded };
+ } else {
+ return altDecoded;
+ }
+ },
+ };
+ }
+}
+
+export class UnionCodecPreBuilder<T> {
+ discriminateOn<D extends keyof T, B = {}>(
+ discriminator: D,
+ baseCodec?: Codec<B>,
+ ): UnionCodecBuilder<T, D, B, never> {
+ return new UnionCodecBuilder<T, D, B, never>(discriminator, baseCodec);
+ }
+}
+
+/**
+ * Return a builder for a codec that decodes an object with properties.
+ */
+export function buildCodecForObject<T>(): ObjectCodecBuilder<T, {}> {
+ return new ObjectCodecBuilder<T, {}>();
+}
+
+export function buildCodecForUnion<T>(): UnionCodecPreBuilder<T> {
+ return new UnionCodecPreBuilder<T>();
+}
+
+/**
+ * Return a codec for a mapping from a string to values described by the inner codec.
+ */
+export function codecForMap<T>(
+ innerCodec: Codec<T>,
+): Codec<{ [x: string]: T }> {
+ if (!innerCodec) {
+ throw Error("inner codec must be defined");
+ }
+ return {
+ decode(x: any, c?: Context): { [x: string]: T } {
+ const map: { [x: string]: T } = {};
+ if (typeof x !== "object") {
+ throw new DecodingError(`expected object at ${renderContext(c)}`);
+ }
+ for (const i in x) {
+ map[i] = innerCodec.decode(x[i], joinContext(c, `[${i}]`));
+ }
+ return map;
+ },
+ };
+}
+
+/**
+ * Return a codec for a list, containing values described by the inner codec.
+ */
+export function codecForList<T>(innerCodec: Codec<T>): Codec<T[]> {
+ if (!innerCodec) {
+ throw Error("inner codec must be defined");
+ }
+ return {
+ decode(x: any, c?: Context): T[] {
+ const arr: T[] = [];
+ if (!Array.isArray(x)) {
+ throw new DecodingError(`expected array at ${renderContext(c)}`);
+ }
+ for (const i in x) {
+ arr.push(innerCodec.decode(x[i], joinContext(c, `[${i}]`)));
+ }
+ return arr;
+ },
+ };
+}
+
+/**
+ * Return a codec for a value that must be a number.
+ */
+export function codecForNumber(): Codec<number> {
+ return {
+ decode(x: any, c?: Context): number {
+ if (typeof x === "number") {
+ return x;
+ }
+ throw new DecodingError(
+ `expected number at ${renderContext(c)} but got ${typeof x}`,
+ );
+ },
+ };
+}
+
+/**
+ * Return a codec for a value that must be a number.
+ */
+export function codecForBoolean(): Codec<boolean> {
+ return {
+ decode(x: any, c?: Context): boolean {
+ if (typeof x === "boolean") {
+ return x;
+ }
+ throw new DecodingError(
+ `expected boolean at ${renderContext(c)} but got ${typeof x}`,
+ );
+ },
+ };
+}
+
+/**
+ * Return a codec for a value that must be a string.
+ */
+export function codecForString(): Codec<string> {
+ return {
+ decode(x: any, c?: Context): string {
+ if (typeof x === "string") {
+ return x;
+ }
+ throw new DecodingError(
+ `expected string at ${renderContext(c)} but got ${typeof x}`,
+ );
+ },
+ };
+}
+
+/**
+ * Codec that allows any value.
+ */
+export function codecForAny(): Codec<any> {
+ return {
+ decode(x: any, c?: Context): any {
+ return x;
+ },
+ };
+}
+
+/**
+ * Return a codec for a value that must be a string.
+ */
+export function codecForConstString<V extends string>(s: V): Codec<V> {
+ return {
+ decode(x: any, c?: Context): V {
+ if (x === s) {
+ return x;
+ }
+ if (typeof x !== "string") {
+ throw new DecodingError(
+ `expected string constant "${s}" at ${renderContext(
+ c,
+ )} but got ${typeof x}`,
+ );
+ }
+ throw new DecodingError(
+ `expected string constant "${s}" at ${renderContext(
+ c,
+ )} but got string value "${x}"`,
+ );
+ },
+ };
+}
+
+/**
+ * Return a codec for a boolean true constant.
+ */
+export function codecForConstTrue(): Codec<true> {
+ return {
+ decode(x: any, c?: Context): true {
+ if (x === true) {
+ return x;
+ }
+ throw new DecodingError(
+ `expected boolean true at ${renderContext(c)} but got ${typeof x}`,
+ );
+ },
+ };
+}
+
+/**
+ * Return a codec for a boolean true constant.
+ */
+export function codecForConstFalse(): Codec<false> {
+ return {
+ decode(x: any, c?: Context): false {
+ if (x === false) {
+ return x;
+ }
+ throw new DecodingError(
+ `expected boolean false at ${renderContext(c)} but got ${typeof x}`,
+ );
+ },
+ };
+}
+
+/**
+ * Return a codec for a value that must be a constant number.
+ */
+export function codecForConstNumber<V extends number>(n: V): Codec<V> {
+ return {
+ decode(x: any, c?: Context): V {
+ if (x === n) {
+ return x;
+ }
+ throw new DecodingError(
+ `expected number constant "${n}" at ${renderContext(
+ c,
+ )} but got ${typeof x}`,
+ );
+ },
+ };
+}
+
+export function codecOptional<V>(innerCodec: Codec<V>): Codec<V | undefined> {
+ return {
+ decode(x: any, c?: Context): V | undefined {
+ if (x === undefined || x === null) {
+ return undefined;
+ }
+ return innerCodec.decode(x, c);
+ },
+ };
+}
diff --git a/packages/taler-util/src/helpers.ts b/packages/taler-util/src/helpers.ts
new file mode 100644
index 000000000..f5c204310
--- /dev/null
+++ b/packages/taler-util/src/helpers.ts
@@ -0,0 +1,152 @@
+/*
+ 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/>
+ */
+
+/**
+ * Small helper functions that don't fit anywhere else.
+ */
+
+/**
+ * Imports.
+ */
+import { AmountJson } from "./amounts";
+import * as Amounts from "./amounts";
+import { URL } from "./url";
+
+/**
+ * Show an amount in a form suitable for the user.
+ * FIXME: In the future, this should consider currency-specific
+ * settings such as significant digits or currency symbols.
+ */
+export function amountToPretty(amount: AmountJson): string {
+ const x = amount.value + amount.fraction / Amounts.fractionalBase;
+ return `${x} ${amount.currency}`;
+}
+
+/**
+ * Canonicalize a base url, typically for the exchange.
+ *
+ * See http://api.taler.net/wallet.html#general
+ */
+export function canonicalizeBaseUrl(url: string): string {
+ if (!url.startsWith("http") && !url.startsWith("https")) {
+ url = "https://" + url;
+ }
+ const x = new URL(url);
+ if (!x.pathname.endsWith("/")) {
+ x.pathname = x.pathname + "/";
+ }
+ x.search = "";
+ x.hash = "";
+ return x.href;
+}
+
+/**
+ * Convert object to JSON with canonical ordering of keys
+ * and whitespace omitted.
+ */
+export function canonicalJson(obj: any): string {
+ // Check for cycles, etc.
+ obj = JSON.parse(JSON.stringify(obj));
+ if (typeof obj === "string" || typeof obj === "number" || obj === null) {
+ return JSON.stringify(obj);
+ }
+ if (Array.isArray(obj)) {
+ const objs: string[] = obj.map((e) => canonicalJson(e));
+ return `[${objs.join(",")}]`;
+ }
+ const keys: string[] = [];
+ for (const key in obj) {
+ keys.push(key);
+ }
+ keys.sort();
+ let s = "{";
+ for (let i = 0; i < keys.length; i++) {
+ const key = keys[i];
+ s += JSON.stringify(key) + ":" + canonicalJson(obj[key]);
+ if (i !== keys.length - 1) {
+ s += ",";
+ }
+ }
+ return s + "}";
+}
+
+/**
+ * Check for deep equality of two objects.
+ * Only arrays, objects and primitives are supported.
+ */
+export function deepEquals(x: any, y: any): boolean {
+ if (x === y) {
+ return true;
+ }
+
+ if (Array.isArray(x) && x.length !== y.length) {
+ return false;
+ }
+
+ const p = Object.keys(x);
+ return (
+ Object.keys(y).every((i) => p.indexOf(i) !== -1) &&
+ p.every((i) => deepEquals(x[i], y[i]))
+ );
+}
+
+export function deepCopy(x: any): any {
+ // FIXME: this has many issues ...
+ return JSON.parse(JSON.stringify(x));
+}
+
+/**
+ * Map from a collection to a list or results and then
+ * concatenate the results.
+ */
+export function flatMap<T, U>(xs: T[], f: (x: T) => U[]): U[] {
+ return xs.reduce((acc: U[], next: T) => [...f(next), ...acc], []);
+}
+
+/**
+ * Compute the hash function of a JSON object.
+ */
+export function hash(val: any): number {
+ const str = canonicalJson(val);
+ // https://github.com/darkskyapp/string-hash
+ let h = 5381;
+ let i = str.length;
+ while (i) {
+ h = (h * 33) ^ str.charCodeAt(--i);
+ }
+
+ /* JavaScript does bitwise operations (like XOR, above) on 32-bit signed
+ * integers. Since we want the results to be always positive, convert the
+ * signed int to an unsigned by doing an unsigned bitshift. */
+ return h >>> 0;
+}
+
+/**
+ * Lexically compare two strings.
+ */
+export function strcmp(s1: string, s2: string): number {
+ if (s1 < s2) {
+ return -1;
+ }
+ if (s1 > s2) {
+ return 1;
+ }
+ return 0;
+}
+
+export function j2s(x: any): string {
+ return JSON.stringify(x, undefined, 2);
+}
diff --git a/packages/taler-util/src/index.ts b/packages/taler-util/src/index.ts
new file mode 100644
index 000000000..5a82ea081
--- /dev/null
+++ b/packages/taler-util/src/index.ts
@@ -0,0 +1,19 @@
+import { TalerErrorCode } from "./taler-error-codes.js";
+
+export { TalerErrorCode };
+
+export * from "./codec.js";
+export * from "./amounts.js";
+export * from "./talerconfig.js";
+export * from "./time.js";
+export * from "./walletTypes";
+export * from "./transactionsTypes";
+export * from "./notifications";
+export * from "./talerTypes";
+export * from "./talerconfig";
+export * from "./taleruri";
+export * from "./ReserveStatus";
+export * from "./ReserveTransaction";
+export * from "./backupTypes";
+export * from "./payto.js";
+export * from "./libtool-version"; \ No newline at end of file
diff --git a/packages/taler-util/src/libtool-version.test.ts b/packages/taler-util/src/libtool-version.test.ts
new file mode 100644
index 000000000..19f997722
--- /dev/null
+++ b/packages/taler-util/src/libtool-version.test.ts
@@ -0,0 +1,48 @@
+/*
+ This file is part of TALER
+ (C) 2017 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/>
+ */
+
+import * as LibtoolVersion from "./libtool-version";
+
+import test from "ava";
+
+test("version comparison", (t) => {
+ t.deepEqual(LibtoolVersion.compare("0:0:0", "0:0:0"), {
+ compatible: true,
+ currentCmp: 0,
+ });
+ t.deepEqual(LibtoolVersion.compare("0:0:0", ""), undefined);
+ t.deepEqual(LibtoolVersion.compare("foo", "0:0:0"), undefined);
+ t.deepEqual(LibtoolVersion.compare("0:0:0", "1:0:1"), {
+ compatible: true,
+ currentCmp: -1,
+ });
+ t.deepEqual(LibtoolVersion.compare("0:0:0", "1:5:1"), {
+ compatible: true,
+ currentCmp: -1,
+ });
+ t.deepEqual(LibtoolVersion.compare("0:0:0", "1:5:0"), {
+ compatible: false,
+ currentCmp: -1,
+ });
+ t.deepEqual(LibtoolVersion.compare("1:0:0", "0:5:0"), {
+ compatible: false,
+ currentCmp: 1,
+ });
+ t.deepEqual(LibtoolVersion.compare("1:0:1", "1:5:1"), {
+ compatible: true,
+ currentCmp: 0,
+ });
+});
diff --git a/packages/taler-util/src/libtool-version.ts b/packages/taler-util/src/libtool-version.ts
new file mode 100644
index 000000000..5e9d0b74e
--- /dev/null
+++ b/packages/taler-util/src/libtool-version.ts
@@ -0,0 +1,88 @@
+/*
+ This file is part of TALER
+ (C) 2017 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/>
+ */
+
+/**
+ * Semantic versioning, but libtool-style.
+ * See https://www.gnu.org/software/libtool/manual/html_node/Libtool-versioning.html
+ */
+
+/**
+ * Result of comparing two libtool versions.
+ */
+export interface VersionMatchResult {
+ /**
+ * Is the first version compatible with the second?
+ */
+ compatible: boolean;
+ /**
+ * Is the first version older (-1), newser (+1) or
+ * identical (0)?
+ */
+ currentCmp: number;
+}
+
+interface Version {
+ current: number;
+ revision: number;
+ age: number;
+}
+
+/**
+ * Compare two libtool-style version strings.
+ */
+export function compare(
+ me: string,
+ other: string,
+): VersionMatchResult | undefined {
+ const meVer = parseVersion(me);
+ const otherVer = parseVersion(other);
+
+ if (!(meVer && otherVer)) {
+ return undefined;
+ }
+
+ const compatible =
+ meVer.current - meVer.age <= otherVer.current &&
+ meVer.current >= otherVer.current - otherVer.age;
+
+ const currentCmp = Math.sign(meVer.current - otherVer.current);
+
+ return { compatible, currentCmp };
+}
+
+function parseVersion(v: string): Version | undefined {
+ const [currentStr, revisionStr, ageStr, ...rest] = v.split(":");
+ if (rest.length !== 0) {
+ return undefined;
+ }
+ const current = Number.parseInt(currentStr);
+ const revision = Number.parseInt(revisionStr);
+ const age = Number.parseInt(ageStr);
+
+ if (Number.isNaN(current)) {
+ return undefined;
+ }
+
+ if (Number.isNaN(revision)) {
+ return undefined;
+ }
+
+ if (Number.isNaN(age)) {
+ return undefined;
+ }
+
+ return { current, revision, age };
+}
diff --git a/packages/taler-util/src/notifications.ts b/packages/taler-util/src/notifications.ts
new file mode 100644
index 000000000..edfb377b9
--- /dev/null
+++ b/packages/taler-util/src/notifications.ts
@@ -0,0 +1,271 @@
+/*
+ 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/>
+ */
+
+/**
+ * Type and schema definitions for notifications from the wallet to clients
+ * of the wallet.
+ */
+
+/**
+ * Imports.
+ */
+import { TalerErrorDetails } from "./walletTypes";
+
+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",
+ RefreshOperationError = "refresh-operation-error",
+ RecoupOperationError = "recoup-operation-error",
+ RefundApplyOperationError = "refund-apply-error",
+ RefundStatusOperationError = "refund-status-error",
+ ProposalOperationError = "proposal-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 ExchangeOperationErrorNotification {
+ type: NotificationType.ExchangeOperationError;
+ error: TalerErrorDetails;
+}
+
+export interface RefreshOperationErrorNotification {
+ type: NotificationType.RefreshOperationError;
+ error: TalerErrorDetails;
+}
+
+export interface RefundStatusOperationErrorNotification {
+ type: NotificationType.RefundStatusOperationError;
+ error: TalerErrorDetails;
+}
+
+export interface RefundApplyOperationErrorNotification {
+ type: NotificationType.RefundApplyOperationError;
+ error: TalerErrorDetails;
+}
+
+export interface PayOperationErrorNotification {
+ type: NotificationType.PayOperationError;
+ error: TalerErrorDetails;
+}
+
+export interface ProposalOperationErrorNotification {
+ type: NotificationType.ProposalOperationError;
+ error: TalerErrorDetails;
+}
+
+export interface TipOperationErrorNotification {
+ type: NotificationType.TipOperationError;
+ error: TalerErrorDetails;
+}
+
+export interface WithdrawOperationErrorNotification {
+ type: NotificationType.WithdrawOperationError;
+ error: TalerErrorDetails;
+}
+
+export interface RecoupOperationErrorNotification {
+ type: NotificationType.RecoupOperationError;
+ error: TalerErrorDetails;
+}
+
+export interface DepositOperationErrorNotification {
+ type: NotificationType.DepositOperationError;
+ error: TalerErrorDetails;
+}
+
+export interface ReserveOperationErrorNotification {
+ type: NotificationType.ReserveOperationError;
+ error: TalerErrorDetails;
+}
+
+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.
+ *
+ * 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.
+ */
+export interface PayOperationSuccessNotification {
+ type: NotificationType.PayOperationSuccess;
+ proposalId: string;
+}
+
+export type WalletNotification =
+ | WithdrawOperationErrorNotification
+ | ReserveOperationErrorNotification
+ | 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;
diff --git a/packages/taler-util/src/payto.test.ts b/packages/taler-util/src/payto.test.ts
new file mode 100644
index 000000000..01280b650
--- /dev/null
+++ b/packages/taler-util/src/payto.test.ts
@@ -0,0 +1,31 @@
+/*
+ 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 { parsePaytoUri } from "./payto";
+
+test("basic payto parsing", (t) => {
+ const r1 = parsePaytoUri("https://example.com/");
+ t.is(r1, undefined);
+
+ const r2 = parsePaytoUri("payto:blabla");
+ t.is(r2, undefined);
+
+ const r3 = parsePaytoUri("payto://x-taler-bank/123");
+ t.is(r3?.targetType, "x-taler-bank");
+ t.is(r3?.targetPath, "123");
+});
diff --git a/packages/taler-util/src/payto.ts b/packages/taler-util/src/payto.ts
new file mode 100644
index 000000000..504db533b
--- /dev/null
+++ b/packages/taler-util/src/payto.ts
@@ -0,0 +1,71 @@
+/*
+ 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 { URLSearchParams } from "./url.js";
+
+interface PaytoUri {
+ targetType: string;
+ targetPath: string;
+ params: { [name: string]: string };
+}
+
+const paytoPfx = "payto://";
+
+/**
+ * Add query parameters to a payto URI
+ */
+export function addPaytoQueryParams(
+ s: string,
+ params: { [name: string]: string },
+): string {
+ const [acct, search] = s.slice(paytoPfx.length).split("?");
+ const searchParams = new URLSearchParams(search || "");
+ for (const k of Object.keys(params)) {
+ searchParams.set(k, params[k]);
+ }
+ return paytoPfx + acct + "?" + searchParams.toString();
+}
+
+export function parsePaytoUri(s: string): PaytoUri | undefined {
+ if (!s.startsWith(paytoPfx)) {
+ return undefined;
+ }
+
+ const [acct, search] = s.slice(paytoPfx.length).split("?");
+
+ const firstSlashPos = acct.indexOf("/");
+
+ if (firstSlashPos === -1) {
+ return undefined;
+ }
+
+ const targetType = acct.slice(0, firstSlashPos);
+ const targetPath = acct.slice(firstSlashPos + 1);
+
+ const params: { [k: string]: string } = {};
+
+ const searchParams = new URLSearchParams(search || "");
+
+ searchParams.forEach((v, k) => {
+ params[v] = k;
+ });
+
+ return {
+ targetPath,
+ targetType,
+ params,
+ };
+}
diff --git a/packages/taler-util/src/taler-error-codes.ts b/packages/taler-util/src/taler-error-codes.ts
new file mode 100644
index 000000000..a78b04f25
--- /dev/null
+++ b/packages/taler-util/src/taler-error-codes.ts
@@ -0,0 +1,1669 @@
+/*
+ This file is part of GNU Taler
+ Copyright (C) 2012-2020 Taler Systems SA
+
+ GNU Taler is free software: you can redistribute it and/or modify it
+ under the terms of the GNU Lesser General Public License as published
+ by the Free Software Foundation, either version 3 of the License,
+ 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
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+ SPDX-License-Identifier: LGPL3.0-or-later
+
+ Note: the LGPL does not apply to all components of GNU Taler,
+ but it does apply to this file.
+ */
+
+export enum TalerErrorCode {
+ /**
+ * Special code to indicate success (no error).
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ NONE = 0,
+
+ /**
+ * A non-integer error code was returned in the JSON response.
+ * 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,
+
+ /**
+ * The response we got from the server was not even in JSON format.
+ * 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.
+ * 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.
+ * 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.
+ * 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,
+
+ /**
+ * The HTTP method used is invalid for this endpoint.
+ * 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.
+ * 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).
+ * 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,
+
+ /**
+ * The payto:// URI provided by the client is 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_PAYTO_URI_MALFORMED = 24,
+
+ /**
+ * A required parameter in the request was missing.
+ * 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.
+ * 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.
+ * 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.
+ * 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GENERIC_UPLOAD_EXCEEDS_LIMIT = 32,
+
+ /**
+ * The service failed initialize its connection to the database.
+ * 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.
+ * 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.
+ * 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.
+ * 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).
+ * 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.)
+ * 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.
+ * 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).
+ * 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.
+ * 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 HTTP server had insufficient memory to parse the request.
+ * 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.
+ * 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.
+ * 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,
+
+ /**
+ * Exchange is badly configured and thus cannot operate.
+ * 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_BAD_CONFIGURATION = 1000,
+
+ /**
+ * Operation specified unknown for this endpoint.
+ * 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_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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_GENERIC_DENOMINATION_EXPIRED = 1009,
+
+ /**
+ * 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_DEPOSITS_GET_MERCHANT_SIGNATURE_INVALID = 1106,
+
+ /**
+ * 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_WITHDRAW_RESERVE_UNKNOWN = 1151,
+
+ /**
+ * The amount to withdraw together with the fee exceeds the numeric range for Taler amounts. This is not a client failure, as the coin value and fees come from the exchange's configuration.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_WITHDRAW_SIGNATURE_FAILED = 1153,
+
+ /**
+ * The signature of the reserve 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).
+ */
+ 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,
+
+ /**
+ * Withdraw period of the coin to be withdrawn is in the past.
+ * Returned with an HTTP status code of #MHD_HTTP_GONE (410).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_WITHDRAW_VALIDITY_IN_PAST = 1157,
+
+ /**
+ * Withdraw period of the coin to be withdrawn is in the past.
+ * Returned with an HTTP status code of #MHD_HTTP_GONE (410).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_WITHDRAW_UNBLIND_FAILURE = 1159,
+
+ /**
+ * The respective coin did not have sufficient residual value for the /deposit operation (i.e. due to double spending). The "history" in the response provides the transaction history of the coin proving this fact.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_DEPOSIT_INSUFFICIENT_FUNDS = 1200,
+
+ /**
+ * The signature made by the coin over the deposit permission 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).
+ */
+ EXCHANGE_DEPOSIT_COIN_SIGNATURE_INVALID = 1205,
+
+ /**
+ * 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_DEPOSIT_REFUND_DEADLINE_AFTER_WIRE_DEADLINE = 1208,
+
+ /**
+ * 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_DEPOSIT_INVALID_SIGNATURE_BY_EXCHANGE = 1221,
+
+ /**
+ * The reserve status was requested using a unknown key.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_RESERVES_GET_STATUS_UNKNOWN = 1250,
+
+ /**
+ * The respective coin did not have sufficient residual value for the /refresh/melt operation. The "history" in this response provdes 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_MELT_INSUFFICIENT_FUNDS = 1300,
+
+ /**
+ * The exchange had an internal error reconstructing the transaction history of the coin that was being melted.
+ * 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_MELT_COIN_HISTORY_COMPUTATION_FAILED = 1301,
+
+ /**
+ * The exchange encountered melt fees exceeding the melted coin's contribution.
+ * 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_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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_MELT_COIN_SIGNATURE_INVALID = 1303,
+
+ /**
+ * 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_MELT_HISTORY_DB_ERROR_INSUFFICIENT_FUNDS = 1304,
+
+ /**
+ * 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_REFRESHES_REVEAL_CNC_TRANSFER_ARRAY_SIZE_INVALID = 1356,
+
+ /**
+ * The number of coins to be created in refresh exceeds the limits of the exchange. private transfer keys request does not match #TALER_CNC_KAPPA - 1.
+ * 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_REFRESHES_REVEAL_NEW_DENOMS_ARRAY_SIZE_EXCESSIVE = 1357,
+
+ /**
+ * 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_REFRESHES_REVEAL_INVALID_RCH = 1362,
+
+ /**
+ * Operation specified invalid for this endpoint.
+ * 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_REFRESHES_REVEAL_OPERATION_INVALID = 1363,
+
+ /**
+ * The coin specified in the link request is unknown 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_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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_TRANSFERS_GET_WIRE_FEE_INCONSISTENT = 1453,
+
+ /**
+ * 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_REFUND_FEE_ABOVE_AMOUNT = 1505,
+
+ /**
+ * The signature of the merchant 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_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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_RECOUP_COIN_BALANCE_NEGATIVE = 1554,
+
+ /**
+ * 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_WIRE_SIGNATURE_INVALID = 1650,
+
+ /**
+ * 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_INSTANCE_UNKNOWN = 2000,
+
+ /**
+ * The start and end-times in the wire fee structure leave a hole. This is not allowed.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_GENERIC_HOLE_IN_WIRE_FEE_STRUCTURE = 2001,
+
+ /**
+ * The reserve key of given to a /reserves/ handler 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_RESERVE_PUB_MALFORMED = 2002,
+
+ /**
+ * 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_GENERIC_FAILED_TO_LOAD_TEMPLATE = 2003,
+
+ /**
+ * 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_GENERIC_FAILED_TO_EXPAND_TEMPLATE = 2004,
+
+ /**
+ * The proposal 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_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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_GENERIC_PRODUCT_UNKNOWN = 2006,
+
+ /**
+ * The tip ID is unknown. This could happen if the tip 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,
+
+ /**
+ * The contract obtained from the merchant backend was malformed.
+ * 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_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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_GENERIC_EXCHANGE_CONNECT_FAILURE = 2012,
+
+ /**
+ * The exchange returned a maformed response.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_GENERIC_EXCHANGE_REPLY_MALFORMED = 2013,
+
+ /**
+ * The exchange returned an unexpected response status.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_GATEWAY (502).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_GENERIC_EXCHANGE_UNEXPECTED_STATUS = 2014,
+
+ /**
+ * 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_GET_ORDERS_ID_EXCHANGE_LOOKUP_START_FAILURE = 2104,
+
+ /**
+ * The token used to authenticate the client is invalid 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_GET_ORDERS_ID_INVALID_TOKEN = 2105,
+
+ /**
+ * 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (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).
+ * (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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_ORDERS_ID_PAY_REFUND_DEADLINE_PAST_WIRE_TRANSFER_DEADLINE = 2159,
+
+ /**
+ * The payment is too late, the offer 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_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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_ORDERS_ID_PAY_EXCHANGE_FAILED = 2170,
+
+ /**
+ * The contract hash does not match the given order ID.
+ * 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_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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_ORDERS_ID_PAID_COIN_SIGNATURE_INVALID = 2201,
+
+ /**
+ * The merchant failed to send the exchange the refund request.
+ * 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_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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_POST_ORDERS_ID_ABORT_COINS_ARRAY_EMPTY = 2256,
+
+ /**
+ * 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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,
+
+ /**
+ * 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,
+
+ /**
+ * 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,
+
+ /**
+ * The tip 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,
+
+ /**
+ * 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,
+
+ /**
+ * 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,
+
+ /**
+ * The backend lacks a wire transfer method configuration option for the given instance. Thus, this instance is unavailable (not findable for creating 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.
+ * 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.
+ * 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.
+ * 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,
+
+ /**
+ * One of the paths to forget is 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_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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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.
+ * 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 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.
+ * 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.
+ * 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.
+ * 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,
+
+ /**
+ * We internally failed to execute the /track/transfer request.
+ * 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_TRANSFERS_REQUEST_ERROR = 2551,
+
+ /**
+ * The exchange gave conflicting information about a coin which has been wire transferred.
+ * 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_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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_POST_TRANSFERS_ACCOUNT_NOT_FOUND = 2555,
+
+ /**
+ * 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_POST_INSTANCES_ALREADY_EXISTS = 2600,
+
+ /**
+ * The product ID 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_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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_PATCH_PRODUCTS_TOTAL_STOCKED_REDUCED = 2662,
+
+ /**
+ * 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_POST_RESERVES_UNSUPPORTED_WIRE_METHOD = 2700,
+
+ /**
+ * The reserve could not be deleted because it 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).
+ */
+ MERCHANT_PRIVATE_DELETE_RESERVES_NO_SUCH_RESERVE = 2710,
+
+ /**
+ * The reserve that was used to fund the tips 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,
+
+ /**
+ * The reserve that was used to fund the tips 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,
+
+ /**
+ * 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.
+ * 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,
+
+ /**
+ * The backend failed to find a reserve needed to authorize the tip.
+ * 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,
+
+ /**
+ * The merchant backend encountered a failure in computing the deposit total.
+ * Returned with an HTTP status code of #MHD_HTTP_OK (200).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_PRIVATE_GET_ORDERS_ID_AMOUNT_ARITHMETIC_FAILURE = 2800,
+
+ /**
+ * The signature from the exchange on the deposit confirmation 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).
+ */
+ AUDITOR_DEPOSIT_CONFIRMATION_SIGNATURE_INVALID = 3100,
+
+ /**
+ * Wire transfer attempted with credit and debit party being the same bank account.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (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).
+ * (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).
+ * (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 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).
+ * (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).
+ * (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).
+ * (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).
+ * (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).
+ * (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).
+ * (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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_WITHDRAWAL_OPERATION_RESERVE_SELECTION_CONFLICT = 5113,
+
+ /**
+ * The sync service failed find the account in its 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).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (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).
+ * (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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ SYNC_MISSING_CONTENT_LENGTH = 6112,
+
+ /**
+ * 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_CORE_API_OPERATION_UNKNOWN = 7007,
+
+ /**
+ * The given taler://pay URI is invalid.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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.
+ * 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,
+
+ /**
+ * The wallet does not implement a version of the bank integration API that is compatible with the version offered by the bank.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ 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).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_CONTRACT_TERMS_SIGNATURE_INVALID = 7019,
+
+ /**
+ * The given action is invalid for the current state of the reducer.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ANASTASIS_REDUCER_ACTION_INVALID = 8000,
+
+ /**
+ * End of error code range.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ END = 9999,
+}
diff --git a/packages/taler-util/src/talerTypes.ts b/packages/taler-util/src/talerTypes.ts
new file mode 100644
index 000000000..bef7ab223
--- /dev/null
+++ b/packages/taler-util/src/talerTypes.ts
@@ -0,0 +1,1457 @@
+/*
+ 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/>
+ */
+
+/**
+ * Type and schema definitions and helpers for the core GNU Taler protocol.
+ *
+ * All types here should be "@Checkable".
+ *
+ * Even though the rest of the wallet uses camelCase for fields, use snake_case
+ * here, since that's the convention for the Taler JSON+HTTP API.
+ */
+
+/**
+ * Imports.
+ */
+
+import {
+ buildCodecForObject,
+ codecForString,
+ codecForList,
+ codecOptional,
+ codecForAny,
+ codecForNumber,
+ codecForBoolean,
+ codecForMap,
+ Codec,
+ codecForConstNumber,
+ buildCodecForUnion,
+ codecForConstString,
+} from "./codec";
+import {
+ Timestamp,
+ codecForTimestamp,
+ Duration,
+ codecForDuration,
+} from "./time";
+import { codecForAmountString } from "./amounts";
+
+/**
+ * Denomination as found in the /keys response from the exchange.
+ */
+export class Denomination {
+ /**
+ * Value of one coin of the denomination.
+ */
+ value: string;
+
+ /**
+ * Public signing key of the denomination.
+ */
+ denom_pub: string;
+
+ /**
+ * Fee for withdrawing.
+ */
+ fee_withdraw: string;
+
+ /**
+ * Fee for depositing.
+ */
+ fee_deposit: string;
+
+ /**
+ * Fee for refreshing.
+ */
+ fee_refresh: string;
+
+ /**
+ * Fee for refunding.
+ */
+ fee_refund: string;
+
+ /**
+ * Start date from which withdraw is allowed.
+ */
+ stamp_start: Timestamp;
+
+ /**
+ * End date for withdrawing.
+ */
+ stamp_expire_withdraw: Timestamp;
+
+ /**
+ * Expiration date after which the exchange can forget about
+ * the currency.
+ */
+ stamp_expire_legal: Timestamp;
+
+ /**
+ * Date after which the coins of this denomination can't be
+ * deposited anymore.
+ */
+ stamp_expire_deposit: Timestamp;
+
+ /**
+ * Signature over the denomination information by the exchange's master
+ * signing key.
+ */
+ master_sig: string;
+}
+
+/**
+ * Signature by the auditor that a particular denomination key is audited.
+ */
+export class AuditorDenomSig {
+ /**
+ * 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 Auditor {
+ /**
+ * 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: AuditorDenomSig[];
+}
+
+/**
+ * Request that we send to the exchange to get a payback.
+ */
+export interface RecoupRequest {
+ /**
+ * Hashed enomination public key of the coin we want to get
+ * paid back.
+ */
+ denom_pub_hash: string;
+
+ /**
+ * Signature over the coin public key by the denomination.
+ */
+ denom_sig: string;
+
+ /**
+ * Coin public key of the coin we want to refund.
+ */
+ coin_pub: string;
+
+ /**
+ * Blinding key that was used during withdraw,
+ * used to prove that we were actually withdrawing the coin.
+ */
+ coin_blind_key_secret: string;
+
+ /**
+ * Signature made by the coin, authorizing the payback.
+ */
+ coin_sig: string;
+
+ /**
+ * Was the coin refreshed (and thus the recoup should go to the old coin)?
+ */
+ refreshed: boolean;
+}
+
+/**
+ * Response that we get from the exchange for a payback request.
+ */
+export class RecoupConfirmation {
+ /**
+ * Public key of the reserve that will receive the payback.
+ */
+ reserve_pub?: string;
+
+ /**
+ * Public key of the old coin that will receive the recoup,
+ * provided if refreshed was true.
+ */
+ old_coin_pub?: string;
+}
+
+/**
+ * Deposit permission for a single coin.
+ */
+export interface CoinDepositPermission {
+ /**
+ * Signature by the coin.
+ */
+ coin_sig: string;
+ /**
+ * Public key of the coin being spend.
+ */
+ coin_pub: string;
+ /**
+ * Signature made by the denomination public key.
+ */
+ ub_sig: string;
+ /**
+ * The denomination public key associated with this coin.
+ */
+ h_denom: string;
+ /**
+ * The amount that is subtracted from this coin with this payment.
+ */
+ contribution: string;
+
+ /**
+ * URL of the exchange this coin was withdrawn from.
+ */
+ exchange_url: string;
+}
+
+/**
+ * Information about an exchange as stored inside a
+ * merchant's contract terms.
+ */
+export class ExchangeHandle {
+ /**
+ * Master public signing key of the exchange.
+ */
+ master_pub: string;
+
+ /**
+ * Base URL of the exchange.
+ */
+ url: string;
+}
+
+export class AuditorHandle {
+ /**
+ * Official name of the auditor.
+ */
+ name: string;
+
+ /**
+ * Master public signing key of the auditor.
+ */
+ auditor_pub: string;
+
+ /**
+ * Base URL of the auditor.
+ */
+ url: string;
+}
+
+// Delivery location, losely 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[];
+}
+
+export interface MerchantInfo {
+ name: string;
+ jurisdiction?: Location;
+ address?: Location;
+}
+
+export interface Tax {
+ // the name of the tax
+ name: string;
+
+ // amount paid in tax
+ tax: AmountString;
+}
+
+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?: number;
+
+ // The 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?: string;
+
+ // 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 InternationalizedString {
+ [lang_tag: string]: string;
+}
+
+/**
+ * Contract terms from a merchant.
+ */
+export class ContractTerms {
+ /**
+ * Hash of the merchant's wire details.
+ */
+ h_wire: string;
+
+ /**
+ * Hash of the merchant's wire details.
+ */
+ auto_refund?: Duration;
+
+ /**
+ * Wire method the merchant wants to use.
+ */
+ wire_method: string;
+
+ /**
+ * Human-readable short summary of the contract.
+ */
+ summary: string;
+
+ summary_i18n?: InternationalizedString;
+
+ /**
+ * Nonce used to ensure freshness.
+ */
+ nonce: string;
+
+ /**
+ * Total amount payable.
+ */
+ amount: string;
+
+ /**
+ * Auditors accepted by the merchant.
+ */
+ auditors: AuditorHandle[];
+
+ /**
+ * Deadline to pay for the contract.
+ */
+ pay_deadline: Timestamp;
+
+ /**
+ * Maximum deposit fee covered by the merchant.
+ */
+ max_fee: string;
+
+ /**
+ * Information about the merchant.
+ */
+ merchant: MerchantInfo;
+
+ /**
+ * Public key of the merchant.
+ */
+ merchant_pub: string;
+
+ /**
+ * Time indicating when the order should be delivered.
+ * May be overwritten by individual products.
+ */
+ delivery_date?: Timestamp;
+
+ /**
+ * Delivery location for (all!) products.
+ */
+ delivery_location?: Location;
+
+ /**
+ * List of accepted exchanges.
+ */
+ exchanges: ExchangeHandle[];
+
+ /**
+ * Products that are sold in this contract.
+ */
+ products?: Product[];
+
+ /**
+ * Deadline for refunds.
+ */
+ refund_deadline: Timestamp;
+
+ /**
+ * Deadline for the wire transfer.
+ */
+ wire_transfer_deadline: Timestamp;
+
+ /**
+ * Time when the contract was generated by the merchant.
+ */
+ timestamp: Timestamp;
+
+ /**
+ * Order id to uniquely identify the purchase within
+ * one merchant instance.
+ */
+ order_id: string;
+
+ /**
+ * Base URL of the merchant's backend.
+ */
+ merchant_base_url: string;
+
+ /**
+ * Fulfillment URL to view the product or
+ * delivery status.
+ */
+ fulfillment_url?: string;
+
+ /**
+ * Plain text fulfillment message in the merchant's default language.
+ */
+ fulfillment_message?: string;
+
+ /**
+ * Internationalized 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;
+
+ /**
+ * Extra data, interpreted by the mechant only.
+ */
+ extra?: any;
+}
+
+/**
+ * Refund permission in the format that the merchant gives it to us.
+ */
+export class MerchantAbortPayRefundDetails {
+ /**
+ * Amount to be refunded.
+ */
+ refund_amount: string;
+
+ /**
+ * Fee for the refund.
+ */
+ refund_fee: string;
+
+ /**
+ * Public key of the coin being refunded.
+ */
+ coin_pub: string;
+
+ /**
+ * Refund transaction ID between merchant and exchange.
+ */
+ rtransaction_id: number;
+
+ /**
+ * Exchange's key used for the signature.
+ */
+ exchange_pub?: string;
+
+ /**
+ * Exchange's signature to confirm the refund.
+ */
+ exchange_sig?: string;
+
+ /**
+ * Error replay from the exchange (if any).
+ */
+ exchange_reply?: any;
+
+ /**
+ * Error code from the exchange (if any).
+ */
+ exchange_code?: number;
+
+ /**
+ * HTTP status code of the exchange's response
+ * to the merchant's refund request.
+ */
+ exchange_http_status: number;
+}
+
+/**
+ * Response for a refund pickup or a /pay in abort mode.
+ */
+export class 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 {
+ /**
+ * Hashed denomination public key.
+ */
+ denom_pub_hash: string;
+
+ /**
+ * Coin's blinded public key.
+ */
+ coin_ev: string;
+}
+
+/**
+ * Request sent to the merchant to pick up a tip.
+ */
+export interface TipPickupRequest {
+ /**
+ * Identifier of the tip.
+ */
+ tip_id: string;
+
+ /**
+ * List of planchets the wallet wants to use for the tip.
+ */
+ planchets: TipPlanchetDetail[];
+}
+
+/**
+ * Reserve signature, defined as separate class to facilitate
+ * schema validation with "@Checkable".
+ */
+export class BlindSigWrapper {
+ /**
+ * Reserve signature.
+ */
+ blind_sig: string;
+}
+
+/**
+ * Response of the merchant
+ * to the TipPickupRequest.
+ */
+export class TipResponse {
+ /**
+ * The order of the signatures matches the planchets list.
+ */
+ blind_sigs: BlindSigWrapper[];
+}
+
+/**
+ * Element of the payback list that the
+ * exchange gives us in /keys.
+ */
+export class Recoup {
+ /**
+ * The hash of the denomination public key for which the payback is offered.
+ */
+ h_denom_pub: string;
+}
+
+/**
+ * Structure of one exchange signing key in the /keys response.
+ */
+export class ExchangeSignKeyJson {
+ stamp_start: Timestamp;
+ stamp_expire: Timestamp;
+ stamp_end: Timestamp;
+ key: EddsaPublicKeyString;
+ master_sig: EddsaSignatureString;
+}
+
+/**
+ * Structure that the exchange gives us in /keys.
+ */
+export class ExchangeKeysJson {
+ /**
+ * List of offered denominations.
+ */
+ denoms: Denomination[];
+
+ /**
+ * The exchange's master public key.
+ */
+ master_public_key: string;
+
+ /**
+ * The list of auditors (partially) auditing the exchange.
+ */
+ auditors: Auditor[];
+
+ /**
+ * Timestamp when this response was issued.
+ */
+ list_issue_date: Timestamp;
+
+ /**
+ * List of revoked denominations.
+ */
+ recoup?: Recoup[];
+
+ /**
+ * Short-lived signing keys used to sign online
+ * responses.
+ */
+ signkeys: ExchangeSignKeyJson[];
+
+ /**
+ * Protocol version.
+ */
+ version: string;
+
+ reserve_closing_delay: Duration;
+}
+
+/**
+ * Wire fees as anounced by the exchange.
+ */
+export class WireFeesJson {
+ /**
+ * Cost of a wire transfer.
+ */
+ wire_fee: string;
+
+ /**
+ * Cost of clising a reserve.
+ */
+ closing_fee: string;
+
+ /**
+ * Signature made with the exchange's master key.
+ */
+ sig: string;
+
+ /**
+ * Date from which the fee applies.
+ */
+ start_date: Timestamp;
+
+ /**
+ * Data after which the fee doesn't apply anymore.
+ */
+ end_date: Timestamp;
+}
+
+export class AccountInfo {
+ payto_uri: string;
+ master_sig: string;
+}
+
+export class ExchangeWireJson {
+ accounts: AccountInfo[];
+ fees: { [methodName: string]: WireFeesJson[] };
+}
+
+/**
+ * Proposal returned from the contract URL.
+ */
+export class Proposal {
+ /**
+ * Contract terms for the propoal.
+ * Raw, un-decoded JSON object.
+ */
+ contract_terms: any;
+
+ /**
+ * Signature over contract, made by the merchant. The public key used for signing
+ * must be contract_terms.merchant_pub.
+ */
+ sig: string;
+}
+
+/**
+ * Response from the internal merchant API.
+ */
+export class CheckPaymentResponse {
+ order_status: string;
+ refunded: boolean | undefined;
+ refunded_amount: string | undefined;
+ contract_terms: any | undefined;
+ taler_pay_uri: string | undefined;
+ contract_url: string | undefined;
+}
+
+/**
+ * Response from the bank.
+ */
+export class WithdrawOperationStatusResponse {
+ selection_done: boolean;
+
+ transfer_done: boolean;
+
+ aborted: boolean;
+
+ amount: string;
+
+ sender_wire?: string;
+
+ suggested_exchange?: string;
+
+ confirm_transfer_url?: string;
+
+ wire_types: string[];
+}
+
+/**
+ * Response from the merchant.
+ */
+export class TipPickupGetResponse {
+ tip_amount: string;
+
+ exchange_url: string;
+
+ expiration: Timestamp;
+}
+
+export class WithdrawResponse {
+ ev_sig: string;
+}
+
+/**
+ * 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: string;
+ /**
+ * 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;
+ }>;
+}
+
+export interface MerchantPayResponse {
+ sig: string;
+}
+
+export interface ExchangeMeltResponse {
+ /**
+ * Which of the kappa indices does the client not have to reveal.
+ */
+ noreveal_index: number;
+
+ /**
+ * Signature of TALER_RefreshMeltConfirmationPS whereby the exchange
+ * affirms the successful melt and confirming the noreveal_index
+ */
+ 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;
+
+ /*
+ * Base URL to use for operations on the refresh context
+ * (so the reveal operation). If not given,
+ * the base URL is the same as the one used for this request.
+ * Can be used if the base URL for /refreshes/ differs from that
+ * for /coins/, i.e. for load balancing. Clients SHOULD
+ * respect the refresh_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 melt.
+ *
+ * When melting the same coin twice (technically allowed
+ * as the response might have been lost on the network),
+ * the exchange may return different values for the refresh_base_url.
+ */
+ refresh_base_url?: string;
+}
+
+export interface ExchangeRevealItem {
+ ev_sig: string;
+}
+
+export interface ExchangeRevealResponse {
+ // List of the exchange's blinded RSA signatures on the new coins.
+ ev_sigs: ExchangeRevealItem[];
+}
+
+interface MerchantOrderStatusPaid {
+ /**
+ * Was the payment refunded (even partially, via refund or abort)?
+ */
+ refunded: boolean;
+
+ /**
+ * Amount that was refunded in total.
+ */
+ refund_amount: AmountString;
+}
+
+interface MerchantOrderRefundResponse {
+ /**
+ * 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: EddsaPublicKeyString;
+}
+
+export type MerchantCoinRefundStatus =
+ | MerchantCoinRefundSuccessStatus
+ | MerchantCoinRefundFailureStatus;
+
+export interface MerchantCoinRefundSuccessStatus {
+ 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: 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;
+
+ // Refund transaction ID.
+ rtransaction_id: number;
+
+ // public key of a coin that was refunded
+ coin_pub: EddsaPublicKeyString;
+
+ // Amount that was refunded, including refund fee charged by the exchange
+ // to the customer.
+ refund_amount: AmountString;
+
+ execution_time: Timestamp;
+}
+
+export interface MerchantCoinRefundFailureStatus {
+ type: "failure";
+
+ // HTTP status of the exchange request, must NOT be 200.
+ exchange_status: number;
+
+ // Taler error code from the exchange reply, if available.
+ exchange_code?: number;
+
+ // If available, HTTP reply from the exchange.
+ exchange_reply?: any;
+
+ // Refund transaction ID.
+ rtransaction_id: number;
+
+ // public key of a coin that was refunded
+ coin_pub: EddsaPublicKeyString;
+
+ // Amount that was refunded, including refund fee charged by the exchange
+ // to the customer.
+ refund_amount: AmountString;
+
+ execution_time: Timestamp;
+}
+
+export interface MerchantOrderStatusUnpaid {
+ /**
+ * URI that the wallet must process to complete the payment.
+ */
+ taler_pay_uri: 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;
+}
+
+/**
+ * Response body for the following endpoint:
+ *
+ * POST {talerBankIntegrationApi}/withdrawal-operation/{wopid}
+ */
+export interface BankWithdrawalOperationPostResponse {
+ transfer_done: boolean;
+}
+
+export const codecForBankWithdrawalOperationPostResponse = (): Codec<
+ BankWithdrawalOperationPostResponse
+> =>
+ buildCodecForObject<BankWithdrawalOperationPostResponse>()
+ .property("transfer_done", codecForBoolean())
+ .build("BankWithdrawalOperationPostResponse");
+
+export type AmountString = string;
+export type Base32String = string;
+export type EddsaSignatureString = string;
+export type EddsaPublicKeyString = string;
+export type CoinPublicKeyString = string;
+
+export const codecForDenomination = (): Codec<Denomination> =>
+ buildCodecForObject<Denomination>()
+ .property("value", codecForString())
+ .property("denom_pub", codecForString())
+ .property("fee_withdraw", codecForString())
+ .property("fee_deposit", codecForString())
+ .property("fee_refresh", codecForString())
+ .property("fee_refund", codecForString())
+ .property("stamp_start", codecForTimestamp)
+ .property("stamp_expire_withdraw", codecForTimestamp)
+ .property("stamp_expire_legal", codecForTimestamp)
+ .property("stamp_expire_deposit", codecForTimestamp)
+ .property("master_sig", codecForString())
+ .build("Denomination");
+
+export const codecForAuditorDenomSig = (): Codec<AuditorDenomSig> =>
+ buildCodecForObject<AuditorDenomSig>()
+ .property("denom_pub_h", codecForString())
+ .property("auditor_sig", codecForString())
+ .build("AuditorDenomSig");
+
+export const codecForAuditor = (): Codec<Auditor> =>
+ buildCodecForObject<Auditor>()
+ .property("auditor_pub", codecForString())
+ .property("auditor_url", codecForString())
+ .property("denomination_keys", codecForList(codecForAuditorDenomSig()))
+ .build("Auditor");
+
+export const codecForExchangeHandle = (): Codec<ExchangeHandle> =>
+ buildCodecForObject<ExchangeHandle>()
+ .property("master_pub", codecForString())
+ .property("url", codecForString())
+ .build("ExchangeHandle");
+
+export const codecForAuditorHandle = (): Codec<AuditorHandle> =>
+ buildCodecForObject<AuditorHandle>()
+ .property("name", codecForString())
+ .property("auditor_pub", codecForString())
+ .property("url", codecForString())
+ .build("AuditorHandle");
+
+export const codecForLocation = (): Codec<Location> =>
+ buildCodecForObject<Location>()
+ .property("country", codecOptional(codecForString()))
+ .property("country_subdivision", codecOptional(codecForString()))
+ .property("building_name", codecOptional(codecForString()))
+ .property("building_number", codecOptional(codecForString()))
+ .property("district", codecOptional(codecForString()))
+ .property("street", codecOptional(codecForString()))
+ .property("post_code", codecOptional(codecForString()))
+ .property("town", codecOptional(codecForString()))
+ .property("town_location", codecOptional(codecForString()))
+ .property("address_lines", codecOptional(codecForList(codecForString())))
+ .build("Location");
+
+export const codecForMerchantInfo = (): Codec<MerchantInfo> =>
+ buildCodecForObject<MerchantInfo>()
+ .property("name", codecForString())
+ .property("address", codecOptional(codecForLocation()))
+ .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>()
+ .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", codecForString())
+ .property("auditors", codecForList(codecForAuditorHandle()))
+ .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("merchant", codecForMerchantInfo())
+ .property("merchant_pub", codecForString())
+ .property("exchanges", codecForList(codecForExchangeHandle()))
+ .property("products", codecOptional(codecForList(codecForProduct())))
+ .property("extra", codecForAny())
+ .build("ContractTerms");
+
+export const codecForMerchantRefundPermission = (): Codec<
+ MerchantAbortPayRefundDetails
+> =>
+ buildCodecForObject<MerchantAbortPayRefundDetails>()
+ .property("refund_amount", codecForAmountString())
+ .property("refund_fee", codecForAmountString())
+ .property("coin_pub", codecForString())
+ .property("rtransaction_id", codecForNumber())
+ .property("exchange_http_status", codecForNumber())
+ .property("exchange_code", codecOptional(codecForNumber()))
+ .property("exchange_reply", codecOptional(codecForAny()))
+ .property("exchange_sig", codecOptional(codecForString()))
+ .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 codecForBlindSigWrapper = (): Codec<BlindSigWrapper> =>
+ buildCodecForObject<BlindSigWrapper>()
+ .property("blind_sig", codecForString())
+ .build("BlindSigWrapper");
+
+export const codecForTipResponse = (): Codec<TipResponse> =>
+ buildCodecForObject<TipResponse>()
+ .property("blind_sigs", codecForList(codecForBlindSigWrapper()))
+ .build("TipResponse");
+
+export const codecForRecoup = (): Codec<Recoup> =>
+ buildCodecForObject<Recoup>()
+ .property("h_denom_pub", codecForString())
+ .build("Recoup");
+
+export const codecForExchangeSigningKey = (): Codec<ExchangeSignKeyJson> =>
+ buildCodecForObject<ExchangeSignKeyJson>()
+ .property("key", codecForString())
+ .property("master_sig", codecForString())
+ .property("stamp_end", codecForTimestamp)
+ .property("stamp_start", codecForTimestamp)
+ .property("stamp_expire", codecForTimestamp)
+ .build("ExchangeSignKeyJson");
+
+export const codecForExchangeKeysJson = (): Codec<ExchangeKeysJson> =>
+ buildCodecForObject<ExchangeKeysJson>()
+ .property("denoms", codecForList(codecForDenomination()))
+ .property("master_public_key", codecForString())
+ .property("auditors", codecForList(codecForAuditor()))
+ .property("list_issue_date", codecForTimestamp)
+ .property("recoup", codecOptional(codecForList(codecForRecoup())))
+ .property("signkeys", codecForList(codecForExchangeSigningKey()))
+ .property("version", codecForString())
+ .property("reserve_closing_delay", codecForDuration)
+ .build("KeysJson");
+
+export const codecForWireFeesJson = (): Codec<WireFeesJson> =>
+ buildCodecForObject<WireFeesJson>()
+ .property("wire_fee", codecForString())
+ .property("closing_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())
+ .property("sig", codecForString())
+ .build("Proposal");
+
+export const codecForCheckPaymentResponse = (): Codec<CheckPaymentResponse> =>
+ buildCodecForObject<CheckPaymentResponse>()
+ .property("order_status", codecForString())
+ .property("refunded", codecOptional(codecForBoolean()))
+ .property("refunded_amount", codecOptional(codecForString()))
+ .property("contract_terms", codecOptional(codecForAny()))
+ .property("taler_pay_uri", codecOptional(codecForString()))
+ .property("contract_url", codecOptional(codecForString()))
+ .build("CheckPaymentResponse");
+
+export const codecForWithdrawOperationStatusResponse = (): Codec<
+ WithdrawOperationStatusResponse
+> =>
+ buildCodecForObject<WithdrawOperationStatusResponse>()
+ .property("selection_done", codecForBoolean())
+ .property("transfer_done", codecForBoolean())
+ .property("aborted", codecForBoolean())
+ .property("amount", codecForString())
+ .property("sender_wire", codecOptional(codecForString()))
+ .property("suggested_exchange", codecOptional(codecForString()))
+ .property("confirm_transfer_url", codecOptional(codecForString()))
+ .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 codecForRecoupConfirmation = (): Codec<RecoupConfirmation> =>
+ buildCodecForObject<RecoupConfirmation>()
+ .property("reserve_pub", codecOptional(codecForString()))
+ .property("old_coin_pub", codecOptional(codecForString()))
+ .build("RecoupConfirmation");
+
+export const codecForWithdrawResponse = (): Codec<WithdrawResponse> =>
+ buildCodecForObject<WithdrawResponse>()
+ .property("ev_sig", codecForString())
+ .build("WithdrawResponse");
+
+export const codecForMerchantPayResponse = (): Codec<MerchantPayResponse> =>
+ buildCodecForObject<MerchantPayResponse>()
+ .property("sig", codecForString())
+ .build("MerchantPayResponse");
+
+export const codecForExchangeMeltResponse = (): Codec<ExchangeMeltResponse> =>
+ buildCodecForObject<ExchangeMeltResponse>()
+ .property("exchange_pub", codecForString())
+ .property("exchange_sig", codecForString())
+ .property("noreveal_index", codecForNumber())
+ .property("refresh_base_url", codecOptional(codecForString()))
+ .build("ExchangeMeltResponse");
+
+export const codecForExchangeRevealItem = (): Codec<ExchangeRevealItem> =>
+ buildCodecForObject<ExchangeRevealItem>()
+ .property("ev_sig", codecForString())
+ .build("ExchangeRevealItem");
+
+export const codecForExchangeRevealResponse = (): Codec<
+ ExchangeRevealResponse
+> =>
+ buildCodecForObject<ExchangeRevealResponse>()
+ .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("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>()
+ .property("taler_pay_uri", codecForString())
+ .property("already_paid_order_id", codecOptional(codecForString()))
+ .build("MerchantOrderStatusUnpaid");
+
+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: string;
+
+ // 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[];
+}
+
+export interface AbortingCoin {
+ // Public key of a coin for which the wallet is requesting an abort-related refund.
+ coin_pub: EddsaPublicKeyString;
+
+ // 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 const codecForAbortResponse = (): Codec<AbortResponse> =>
+ buildCodecForObject<AbortResponse>()
+ .property("refunds", codecForList(codecForMerchantAbortPayRefundStatus()))
+ .build("AbortResponse");
+
+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: number;
+
+ // Taler error code from the exchange reply, if available.
+ exchange_code?: number;
+
+ // If available, HTTP reply from the exchange.
+ exchange_reply?: unknown;
+}
+
+// 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: string;
+
+ // 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: 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");
diff --git a/packages/taler-util/src/talerconfig.test.ts b/packages/taler-util/src/talerconfig.test.ts
new file mode 100644
index 000000000..2f920fccf
--- /dev/null
+++ b/packages/taler-util/src/talerconfig.test.ts
@@ -0,0 +1,124 @@
+/*
+ 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/>
+ */
+
+/**
+ * Imports
+ */
+import test from "ava";
+import { pathsub, Configuration } from "./talerconfig";
+
+test("pathsub", (t) => {
+ t.assert("foo" === pathsub("foo", () => undefined));
+
+ t.assert("fo${bla}o" === pathsub("fo${bla}o", () => undefined));
+
+ const d: Record<string, string> = {
+ w: "world",
+ f: "foo",
+ "1foo": "x",
+ foo_bar: "quux",
+ };
+
+ t.is(
+ pathsub("hello ${w}!", (v) => d[v]),
+ "hello world!",
+ );
+
+ t.is(
+ pathsub("hello ${w} ${w}!", (v) => d[v]),
+ "hello world world!",
+ );
+
+ t.is(
+ pathsub("hello ${x:-blabla}!", (v) => d[v]),
+ "hello blabla!",
+ );
+
+ // No braces
+ t.is(
+ pathsub("hello $w!", (v) => d[v]),
+ "hello world!",
+ );
+ t.is(
+ pathsub("hello $foo!", (v) => d[v]),
+ "hello $foo!",
+ );
+ t.is(
+ pathsub("hello $1foo!", (v) => d[v]),
+ "hello $1foo!",
+ );
+ t.is(
+ pathsub("hello $$ world!", (v) => d[v]),
+ "hello $$ world!",
+ );
+ t.is(
+ pathsub("hello $$ world!", (v) => d[v]),
+ "hello $$ world!",
+ );
+
+ t.is(
+ pathsub("hello $foo_bar!", (v) => d[v]),
+ "hello quux!",
+ );
+
+ // Recursive lookup in default
+ t.is(
+ pathsub("hello ${x:-${w}}!", (v) => d[v]),
+ "hello world!",
+ );
+
+ // No variables in variable name part
+ t.is(
+ pathsub("hello ${${w}:-x}!", (v) => d[v]),
+ "hello ${${w}:-x}!",
+ );
+
+ // Missing closing brace
+ t.is(
+ pathsub("hello ${w!", (v) => d[v]),
+ "hello ${w!",
+ );
+});
+
+test("path expansion", (t) => {
+ const config = new Configuration();
+ config.setString("paths", "taler_home", "foo/bar");
+ config.setString(
+ "paths",
+ "taler_data_home",
+ "$TALER_HOME/.local/share/taler/",
+ );
+ config.setString(
+ "exchange",
+ "master_priv_file",
+ "${TALER_DATA_HOME}/exchange/offline-keys/master.priv",
+ );
+ t.is(
+ config.getPath("exchange", "MaStER_priv_file").required(),
+ "foo/bar/.local/share/taler//exchange/offline-keys/master.priv",
+ );
+});
+
+test("recursive path resolution", (t) => {
+ console.log("recursive test");
+ const config = new Configuration();
+ config.setString("paths", "a", "x${b}");
+ config.setString("paths", "b", "y${a}");
+ config.setString("foo", "x", "z${a}");
+ t.throws(() => {
+ config.getPath("foo", "a").required();
+ });
+});
diff --git a/packages/taler-util/src/talerconfig.ts b/packages/taler-util/src/talerconfig.ts
new file mode 100644
index 000000000..fa8c2d40f
--- /dev/null
+++ b/packages/taler-util/src/talerconfig.ts
@@ -0,0 +1,318 @@
+/*
+ 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/>
+ */
+
+/**
+ * Utilities to handle Taler-style configuration files.
+ *
+ * @author Florian Dold <dold@taler.net>
+ */
+
+/**
+ * Imports
+ */
+import { AmountJson } from "./amounts";
+import * as Amounts from "./amounts";
+import fs from "fs";
+
+export class ConfigError extends Error {
+ constructor(message: string) {
+ super();
+ Object.setPrototypeOf(this, ConfigError.prototype);
+ this.name = "ConfigError";
+ this.message = message;
+ }
+}
+
+type OptionMap = { [optionName: string]: string };
+type SectionMap = { [sectionName: string]: OptionMap };
+
+export class ConfigValue<T> {
+ constructor(
+ private sectionName: string,
+ private optionName: string,
+ private val: string | undefined,
+ private converter: (x: string) => T,
+ ) {}
+
+ required(): T {
+ if (!this.val) {
+ throw new ConfigError(
+ `required option [${this.sectionName}]/${this.optionName} not found`,
+ );
+ }
+ return this.converter(this.val);
+ }
+
+ orUndefined(): T | undefined {
+ if (this.val !== undefined) {
+ return this.converter(this.val);
+ } else {
+ return undefined;
+ }
+ }
+
+ orDefault(v: T): T | undefined {
+ if (this.val !== undefined) {
+ return this.converter(this.val);
+ } else {
+ return v;
+ }
+ }
+
+ isDefined(): boolean {
+ return this.val !== undefined;
+ }
+}
+
+/**
+ * Shell-style path substitution.
+ *
+ * Supported patterns:
+ * "$x" (look up "x")
+ * "${x}" (look up "x")
+ * "${x:-y}" (look up "x", fall back to expanded y)
+ */
+export function pathsub(
+ x: string,
+ lookup: (s: string, depth: number) => string | undefined,
+ depth = 0,
+): string {
+ if (depth >= 10) {
+ throw Error("recursion in path substitution");
+ }
+ let s = x;
+ let l = 0;
+ while (l < s.length) {
+ if (s[l] === "$") {
+ if (s[l + 1] === "{") {
+ let depth = 1;
+ const start = l;
+ let p = start + 2;
+ let insideNamePart = true;
+ let hasDefault = false;
+ for (; p < s.length; p++) {
+ if (s[p] == "}") {
+ insideNamePart = false;
+ depth--;
+ } else if (s[p] === "$" && s[p + 1] === "{") {
+ insideNamePart = false;
+ depth++;
+ }
+ if (insideNamePart && s[p] === ":" && s[p + 1] === "-") {
+ hasDefault = true;
+ }
+ if (depth == 0) {
+ break;
+ }
+ }
+ if (depth == 0) {
+ const inner = s.slice(start + 2, p);
+ let varname: string;
+ let defaultValue: string | undefined;
+ if (hasDefault) {
+ [varname, defaultValue] = inner.split(":-", 2);
+ } else {
+ varname = inner;
+ defaultValue = undefined;
+ }
+
+ const r = lookup(inner, depth + 1);
+ if (r !== undefined) {
+ s = s.substr(0, start) + r + s.substr(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);
+ l = start + resolvedDefault.length;
+ continue;
+ }
+ }
+ l = p;
+ continue;
+ } 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);
+ if (r !== undefined) {
+ s = s.substr(0, l) + r + s.substr(l + 1 + m[0].length);
+ l = l + r.length;
+ continue;
+ }
+ }
+ }
+ }
+ l++;
+ }
+ return s;
+}
+
+export class Configuration {
+ private sectionMap: SectionMap = {};
+
+ loadFromString(s: string): void {
+ const reComment = /^\s*#.*$/;
+ const reSection = /^\s*\[\s*([^\]]*)\s*\]\s*$/;
+ const reParam = /^\s*([^=]+?)\s*=\s*(.*?)\s*$/;
+ const reEmptyLine = /^\s*$/;
+
+ let currentSection: string | undefined = undefined;
+
+ const lines = s.split("\n");
+ for (const line of lines) {
+ if (reEmptyLine.test(line)) {
+ continue;
+ }
+ if (reComment.test(line)) {
+ continue;
+ }
+ const secMatch = line.match(reSection);
+ if (secMatch) {
+ currentSection = secMatch[1];
+ continue;
+ }
+ if (currentSection === undefined) {
+ throw Error("invalid configuration, expected section header");
+ }
+ currentSection = currentSection.toUpperCase();
+ const paramMatch = line.match(reParam);
+ if (paramMatch) {
+ const optName = paramMatch[1].toUpperCase();
+ let val = paramMatch[2];
+ if (val.startsWith('"') && val.endsWith('"')) {
+ val = val.slice(1, val.length - 1);
+ }
+ const sec = this.sectionMap[currentSection] ?? {};
+ this.sectionMap[currentSection] = Object.assign(sec, {
+ [optName]: val,
+ });
+ continue;
+ }
+ throw Error(
+ "invalid configuration, expected section header or option assignment",
+ );
+ }
+ }
+
+ setString(section: string, option: string, value: string): void {
+ const secNorm = section.toUpperCase();
+ const sec = this.sectionMap[secNorm] ?? (this.sectionMap[secNorm] = {});
+ sec[option.toUpperCase()] = value;
+ }
+
+ /**
+ * Get lower-cased section names.
+ */
+ getSectionNames(): string[] {
+ return Object.keys(this.sectionMap).map((x) => x.toLowerCase());
+ }
+
+ getString(section: string, option: string): ConfigValue<string> {
+ const secNorm = section.toUpperCase();
+ const optNorm = option.toUpperCase();
+ const val = (this.sectionMap[secNorm] ?? {})[optNorm];
+ return new ConfigValue(secNorm, optNorm, val, (x) => x);
+ }
+
+ getPath(section: string, option: string): ConfigValue<string> {
+ const secNorm = section.toUpperCase();
+ const optNorm = option.toUpperCase();
+ const val = (this.sectionMap[secNorm] ?? {})[optNorm];
+ return new ConfigValue(secNorm, optNorm, val, (x) =>
+ pathsub(x, (v, d) => this.lookupVariable(v, d + 1)),
+ );
+ }
+
+ getYesNo(section: string, option: string): ConfigValue<boolean> {
+ const secNorm = section.toUpperCase();
+ const optNorm = option.toUpperCase();
+ const val = (this.sectionMap[secNorm] ?? {})[optNorm];
+ const convert = (x: string): boolean => {
+ x = x.toLowerCase();
+ if (x === "yes") {
+ return true;
+ } else if (x === "no") {
+ return false;
+ }
+ throw Error(
+ `invalid config value for [${secNorm}]/${optNorm}, expected yes/no`,
+ );
+ };
+ return new ConfigValue(secNorm, optNorm, val, convert);
+ }
+
+ getNumber(section: string, option: string): ConfigValue<number> {
+ const secNorm = section.toUpperCase();
+ const optNorm = option.toUpperCase();
+ const val = (this.sectionMap[secNorm] ?? {})[optNorm];
+ const convert = (x: string): number => {
+ try {
+ return Number.parseInt(x, 10);
+ } catch (e) {
+ throw Error(
+ `invalid config value for [${secNorm}]/${optNorm}, expected number`,
+ );
+ }
+ };
+ return new ConfigValue(secNorm, optNorm, val, convert);
+ }
+
+ lookupVariable(x: string, depth: number = 0): string | undefined {
+ // We loop up options in PATHS in upper case, as option names
+ // are case insensitive
+ const val = (this.sectionMap["PATHS"] ?? {})[x.toUpperCase()];
+ 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;
+ }
+ return;
+ }
+
+ getAmount(section: string, option: string): ConfigValue<AmountJson> {
+ const val = (this.sectionMap[section] ?? {})[option];
+ return new ConfigValue(section, option, val, (x) =>
+ Amounts.parseOrThrow(x),
+ );
+ }
+
+ static load(filename: string): Configuration {
+ const s = fs.readFileSync(filename, "utf-8");
+ const cfg = new Configuration();
+ cfg.loadFromString(s);
+ return cfg;
+ }
+
+ write(filename: string): void {
+ let s = "";
+ for (const sectionName of Object.keys(this.sectionMap)) {
+ s += `[${sectionName}]\n`;
+ for (const optionName of Object.keys(
+ this.sectionMap[sectionName] ?? {},
+ )) {
+ const val = this.sectionMap[sectionName][optionName];
+ if (val !== undefined) {
+ s += `${optionName} = ${val}\n`;
+ }
+ }
+ s += "\n";
+ }
+ fs.writeFileSync(filename, s);
+ }
+}
diff --git a/packages/taler-util/src/taleruri.test.ts b/packages/taler-util/src/taleruri.test.ts
new file mode 100644
index 000000000..e80acc5c3
--- /dev/null
+++ b/packages/taler-util/src/taleruri.test.ts
@@ -0,0 +1,184 @@
+/*
+ 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 {
+ parsePayUri,
+ parseWithdrawUri,
+ parseRefundUri,
+ parseTipUri,
+} from "./taleruri";
+
+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);
+});
+
+test("taler pay url parsing: defaults", (t) => {
+ const url1 = "taler://pay/example.com/myorder/";
+ const r1 = parsePayUri(url1);
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.is(r1.merchantBaseUrl, "https://example.com/");
+ t.is(r1.sessionId, "");
+
+ const url2 = "taler://pay/example.com/myorder/mysession";
+ const r2 = parsePayUri(url2);
+ if (!r2) {
+ t.fail();
+ return;
+ }
+ t.is(r2.merchantBaseUrl, "https://example.com/");
+ t.is(r2.sessionId, "mysession");
+});
+
+test("taler pay url parsing: instance", (t) => {
+ const url1 = "taler://pay/example.com/instances/myinst/myorder/";
+ const r1 = parsePayUri(url1);
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.is(r1.merchantBaseUrl, "https://example.com/instances/myinst/");
+ t.is(r1.orderId, "myorder");
+});
+
+test("taler pay url parsing (claim token)", (t) => {
+ const url1 = "taler://pay/example.com/instances/myinst/myorder/?c=ASDF";
+ const r1 = parsePayUri(url1);
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.is(r1.merchantBaseUrl, "https://example.com/instances/myinst/");
+ t.is(r1.orderId, "myorder");
+ 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);
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.is(r1.merchantBaseUrl, "http://example.com/");
+ t.is(r1.orderId, "myorder");
+});
+
+test("taler pay uri parsing: missing session component", (t) => {
+ const url1 = "taler+http://pay/example.com/myorder";
+ const r1 = parsePayUri(url1);
+ if (r1) {
+ t.fail();
+ return;
+ }
+ 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 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 refund uri parsing", (t) => {
+ const url1 = "taler://refund/merchant.example.com/1234/";
+ const r1 = parseRefundUri(url1);
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.is(r1.merchantBaseUrl, "https://merchant.example.com/");
+ t.is(r1.orderId, "1234");
+});
+
+test("taler refund uri parsing with instance", (t) => {
+ const url1 = "taler://refund/merchant.example.com/instances/myinst/1234/";
+ const r1 = parseRefundUri(url1);
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.is(r1.orderId, "1234");
+ 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);
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.is(r1.merchantBaseUrl, "https://merchant.example.com/");
+});
+
+test("taler tip pickup uri with instance", (t) => {
+ const url1 = "taler://tip/merchant.example.com/instances/tipm/tipid";
+ const r1 = parseTipUri(url1);
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.is(r1.merchantBaseUrl, "https://merchant.example.com/instances/tipm/");
+ t.is(r1.merchantTipId, "tipid");
+});
+
+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);
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.is(r1.merchantBaseUrl, "https://merchant.example.com/my/pfx/tipm/");
+ t.is(r1.merchantTipId, "tipid");
+});
diff --git a/packages/taler-util/src/taleruri.ts b/packages/taler-util/src/taleruri.ts
new file mode 100644
index 000000000..d8366fd0f
--- /dev/null
+++ b/packages/taler-util/src/taleruri.ts
@@ -0,0 +1,220 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019-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 { canonicalizeBaseUrl } from "./helpers";
+import { URLSearchParams } from "./url";
+
+export interface PayUriResult {
+ merchantBaseUrl: string;
+ orderId: string;
+ sessionId: string;
+ claimToken: string | undefined;
+}
+
+export interface WithdrawUriResult {
+ bankIntegrationApiBaseUrl: string;
+ withdrawalOperationId: string;
+}
+
+export interface RefundUriResult {
+ merchantBaseUrl: string;
+ orderId: string;
+}
+
+export interface TipUriResult {
+ merchantTipId: string;
+ merchantBaseUrl: 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;
+ }
+ const parts = pi.rest.split("/");
+
+ if (parts.length < 2) {
+ return undefined;
+ }
+
+ const host = parts[0].toLowerCase();
+ const pathSegments = parts.slice(1, parts.length - 1);
+ const withdrawId = parts[parts.length - 1];
+ const p = [host, ...pathSegments].join("/");
+
+ return {
+ bankIntegrationApiBaseUrl: canonicalizeBaseUrl(`${pi.innerProto}://${p}/`),
+ withdrawalOperationId: withdrawId,
+ };
+}
+
+export enum TalerUriType {
+ TalerPay = "taler-pay",
+ TalerWithdraw = "taler-withdraw",
+ TalerTip = "taler-tip",
+ TalerRefund = "taler-refund",
+ TalerNotifyReserve = "taler-notify-reserve",
+ 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;
+}
+
+interface TalerUriProtoInfo {
+ innerProto: "http" | "https";
+ rest: string;
+}
+
+function parseProtoInfo(
+ s: string,
+ action: string,
+): TalerUriProtoInfo | undefined {
+ const pfxPlain = `taler://${action}/`;
+ const pfxHttp = `taler+http://${action}/`;
+ if (s.toLowerCase().startsWith(pfxPlain)) {
+ return {
+ innerProto: "https",
+ rest: s.substring(pfxPlain.length),
+ };
+ } else if (s.toLowerCase().startsWith(pfxHttp)) {
+ return {
+ innerProto: "http",
+ rest: s.substring(pfxHttp.length),
+ };
+ } else {
+ return undefined;
+ }
+}
+
+/**
+ * Parse a taler[+http]://pay URI.
+ * Return undefined if not passed a valid URI.
+ */
+export function parsePayUri(s: string): PayUriResult | undefined {
+ const pi = parseProtoInfo(s, "pay");
+ if (!pi) {
+ return undefined;
+ }
+ const c = pi?.rest.split("?");
+ const q = new URLSearchParams(c[1] ?? "");
+ const claimToken = q.get("c") ?? undefined;
+ const parts = c[0].split("/");
+ if (parts.length < 3) {
+ return undefined;
+ }
+ const host = parts[0].toLowerCase();
+ 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}/`);
+
+ return {
+ merchantBaseUrl,
+ orderId,
+ sessionId: sessionId,
+ claimToken,
+ };
+}
+
+/**
+ * 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");
+ 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 tipId = parts[parts.length - 1];
+ const pathSegments = parts.slice(1, parts.length - 1);
+ const p = [host, ...pathSegments].join("/");
+ const merchantBaseUrl = canonicalizeBaseUrl(`${pi.innerProto}://${p}/`);
+
+ return {
+ merchantBaseUrl,
+ merchantTipId: tipId,
+ };
+}
+
+/**
+ * Parse a taler[+http]://refund URI.
+ * Return undefined if not passed a valid URI.
+ */
+export function parseRefundUri(s: string): RefundUriResult | undefined {
+ const pi = parseProtoInfo(s, "refund");
+ if (!pi) {
+ return undefined;
+ }
+ const c = pi?.rest.split("?");
+ const parts = c[0].split("/");
+ if (parts.length < 3) {
+ return undefined;
+ }
+ const host = parts[0].toLowerCase();
+ 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}/`);
+
+ return {
+ merchantBaseUrl,
+ orderId,
+ };
+}
diff --git a/packages/taler-util/src/time.ts b/packages/taler-util/src/time.ts
new file mode 100644
index 000000000..1a23037a0
--- /dev/null
+++ b/packages/taler-util/src/time.ts
@@ -0,0 +1,264 @@
+/*
+ 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/>
+ */
+
+/**
+ * Helpers for relative and absolute time.
+ */
+
+/**
+ * Imports.
+ */
+import { Codec, renderContext, Context } from "./codec";
+
+export class Timestamp {
+ /**
+ * Timestamp in milliseconds.
+ */
+ readonly t_ms: number | "never";
+}
+
+export interface Duration {
+ /**
+ * Duration in milliseconds.
+ */
+ readonly d_ms: number | "forever";
+}
+
+let timeshift = 0;
+
+export function setDangerousTimetravel(dt: number): void {
+ timeshift = dt;
+}
+
+export function getTimestampNow(): Timestamp {
+ return {
+ t_ms: new Date().getTime() + timeshift,
+ };
+}
+
+export function isTimestampExpired(t: Timestamp) {
+ return timestampCmp(t, getTimestampNow()) <= 0;
+}
+
+export function getDurationRemaining(
+ deadline: Timestamp,
+ now = getTimestampNow(),
+): Duration {
+ if (deadline.t_ms === "never") {
+ return { d_ms: "forever" };
+ }
+ if (now.t_ms === "never") {
+ throw Error("invalid argument for 'now'");
+ }
+ if (deadline.t_ms < now.t_ms) {
+ return { d_ms: 0 };
+ }
+ return { d_ms: deadline.t_ms - now.t_ms };
+}
+
+export function timestampMin(t1: Timestamp, t2: Timestamp): Timestamp {
+ if (t1.t_ms === "never") {
+ return { t_ms: t2.t_ms };
+ }
+ if (t2.t_ms === "never") {
+ return { t_ms: t2.t_ms };
+ }
+ return { t_ms: Math.min(t1.t_ms, t2.t_ms) };
+}
+
+export function timestampMax(t1: Timestamp, t2: Timestamp): Timestamp {
+ if (t1.t_ms === "never") {
+ return { t_ms: "never" };
+ }
+ if (t2.t_ms === "never") {
+ return { t_ms: "never" };
+ }
+ return { t_ms: Math.max(t1.t_ms, t2.t_ms) };
+}
+
+const SECONDS = 1000;
+const MINUTES = SECONDS * 60;
+const HOURS = MINUTES * 60;
+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 };
+}
+
+/**
+ * Truncate a timestamp so that that it represents a multiple
+ * of seconds. The timestamp is always rounded down.
+ */
+export function timestampTruncateToSecond(t1: Timestamp): Timestamp {
+ if (t1.t_ms === "never") {
+ return { t_ms: "never" };
+ }
+ return {
+ t_ms: Math.floor(t1.t_ms / 1000) * 1000,
+ };
+}
+
+export function durationMin(d1: Duration, d2: Duration): Duration {
+ if (d1.d_ms === "forever") {
+ return { d_ms: d2.d_ms };
+ }
+ if (d2.d_ms === "forever") {
+ return { d_ms: d2.d_ms };
+ }
+ return { d_ms: Math.min(d1.d_ms, d2.d_ms) };
+}
+
+export function durationMax(d1: Duration, d2: Duration): Duration {
+ if (d1.d_ms === "forever") {
+ return { d_ms: "forever" };
+ }
+ if (d2.d_ms === "forever") {
+ return { d_ms: "forever" };
+ }
+ return { d_ms: Math.max(d1.d_ms, d2.d_ms) };
+}
+
+export function durationMul(d: Duration, n: number): Duration {
+ if (d.d_ms === "forever") {
+ return { d_ms: "forever" };
+ }
+ return { d_ms: Math.round(d.d_ms * n) };
+}
+
+export function durationAdd(d1: Duration, d2: Duration): Duration {
+ if (d1.d_ms === "forever" || d2.d_ms === "forever") {
+ return { d_ms: "forever" };
+ }
+ return { d_ms: d1.d_ms + d2.d_ms };
+}
+
+export function timestampCmp(t1: Timestamp, t2: Timestamp): number {
+ if (t1.t_ms === "never") {
+ if (t2.t_ms === "never") {
+ return 0;
+ }
+ return 1;
+ }
+ if (t2.t_ms === "never") {
+ return -1;
+ }
+ if (t1.t_ms == t2.t_ms) {
+ return 0;
+ }
+ if (t1.t_ms > t2.t_ms) {
+ return 1;
+ }
+ return -1;
+}
+
+export function timestampAddDuration(t1: Timestamp, d: Duration): Timestamp {
+ if (t1.t_ms === "never" || d.d_ms === "forever") {
+ return { t_ms: "never" };
+ }
+ return { t_ms: t1.t_ms + d.d_ms };
+}
+
+export function timestampSubtractDuraction(
+ t1: Timestamp,
+ d: Duration,
+): Timestamp {
+ if (t1.t_ms === "never") {
+ return { t_ms: "never" };
+ }
+ if (d.d_ms === "forever") {
+ return { t_ms: 0 };
+ }
+ return { t_ms: Math.max(0, t1.t_ms - d.d_ms) };
+}
+
+export function stringifyTimestamp(t: Timestamp): string {
+ if (t.t_ms === "never") {
+ return "never";
+ }
+ return new Date(t.t_ms).toISOString();
+}
+
+export function timestampDifference(t1: Timestamp, t2: Timestamp): Duration {
+ if (t1.t_ms === "never") {
+ return { d_ms: "forever" };
+ }
+ if (t2.t_ms === "never") {
+ return { d_ms: "forever" };
+ }
+ return { d_ms: Math.abs(t1.t_ms - t2.t_ms) };
+}
+
+export function timestampIsBetween(
+ t: Timestamp,
+ start: Timestamp,
+ end: Timestamp,
+): boolean {
+ if (timestampCmp(t, start) < 0) {
+ return false;
+ }
+ if (timestampCmp(t, end) > 0) {
+ return false;
+ }
+ return true;
+}
+
+export const codecForTimestamp: Codec<Timestamp> = {
+ decode(x: any, c?: Context): Timestamp {
+ const t_ms = x.t_ms;
+ if (typeof t_ms === "string") {
+ if (t_ms === "never") {
+ return { t_ms: "never" };
+ }
+ throw Error(`expected timestamp at ${renderContext(c)}`);
+ }
+ if (typeof t_ms === "number") {
+ return { t_ms };
+ }
+ throw Error(`expected timestamp at ${renderContext(c)}`);
+ },
+};
+
+export const codecForDuration: Codec<Duration> = {
+ decode(x: any, c?: Context): Duration {
+ const d_ms = x.d_ms;
+ if (typeof d_ms === "string") {
+ if (d_ms === "forever") {
+ return { d_ms: "forever" };
+ }
+ throw Error(`expected duration at ${renderContext(c)}`);
+ }
+ if (typeof d_ms === "number") {
+ return { d_ms };
+ }
+ throw Error(`expected duration at ${renderContext(c)}`);
+ },
+};
diff --git a/packages/taler-util/src/transactionsTypes.ts b/packages/taler-util/src/transactionsTypes.ts
new file mode 100644
index 000000000..b3cc274a0
--- /dev/null
+++ b/packages/taler-util/src/transactionsTypes.ts
@@ -0,0 +1,364 @@
+/*
+ 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 { Timestamp } 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 { TalerErrorDetails } 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: Timestamp;
+
+ // 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;
+
+ // 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?: TalerErrorDetails;
+}
+
+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[];
+}
+
+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;
+}
+
+// This should only be used for actual withdrawals
+// and not for tips that have their own transactions type.
+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;
+}
+
+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;
+}
+
+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.
+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.
+ */
+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/types-test.ts b/packages/taler-util/src/types-test.ts
new file mode 100644
index 000000000..19c9b5aa6
--- /dev/null
+++ b/packages/taler-util/src/types-test.ts
@@ -0,0 +1,93 @@
+/*
+ This file is part of TALER
+ (C) 2017 Inria and 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/>
+ */
+
+import test from "ava";
+import { codecForContractTerms } from "./talerTypes";
+
+test("contract terms validation", (t) => {
+ const c = {
+ nonce: "123123123",
+ h_wire: "123",
+ amount: "EUR:1.5",
+ auditors: [],
+ exchanges: [{ master_pub: "foo", url: "foo" }],
+ fulfillment_url: "foo",
+ max_fee: "EUR:1.5",
+ merchant_pub: "12345",
+ merchant: { name: "Foo" },
+ order_id: "test_order",
+ pay_deadline: { t_ms: 42 },
+ wire_transfer_deadline: { t_ms: 42 },
+ merchant_base_url: "https://example.com/pay",
+ products: [],
+ refund_deadline: { t_ms: 42 },
+ summary: "hello",
+ timestamp: { t_ms: 42 },
+ wire_method: "test",
+ };
+
+ codecForContractTerms().decode(c);
+
+ const c1 = JSON.parse(JSON.stringify(c));
+ c1.pay_deadline = "foo";
+
+ try {
+ codecForContractTerms().decode(c1);
+ } catch (e) {
+ t.pass();
+ return;
+ }
+
+ t.fail();
+});
+
+test("contract terms validation (locations)", (t) => {
+ const c = {
+ nonce: "123123123",
+ h_wire: "123",
+ amount: "EUR:1.5",
+ auditors: [],
+ exchanges: [{ master_pub: "foo", url: "foo" }],
+ fulfillment_url: "foo",
+ max_fee: "EUR:1.5",
+ merchant_pub: "12345",
+ merchant: {
+ name: "Foo",
+ address: {
+ country: "DE",
+ },
+ },
+ order_id: "test_order",
+ pay_deadline: { t_ms: 42 },
+ wire_transfer_deadline: { t_ms: 42 },
+ merchant_base_url: "https://example.com/pay",
+ products: [],
+ refund_deadline: { t_ms: 42 },
+ summary: "hello",
+ timestamp: { t_ms: 42 },
+ wire_method: "test",
+ delivery_location: {
+ country: "FR",
+ town: "Rennes",
+ },
+ };
+
+ const r = codecForContractTerms().decode(c);
+
+ t.assert(r.merchant.address?.country === "DE");
+ t.assert(r.delivery_location?.country === "FR");
+ t.assert(r.delivery_location?.town === "Rennes");
+});
diff --git a/packages/taler-util/src/url.ts b/packages/taler-util/src/url.ts
new file mode 100644
index 000000000..b50b4b466
--- /dev/null
+++ b/packages/taler-util/src/url.ts
@@ -0,0 +1,74 @@
+/*
+ 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/>
+ */
+
+interface URL {
+ hash: string;
+ host: string;
+ hostname: string;
+ href: string;
+ toString(): string;
+ readonly origin: string;
+ password: string;
+ pathname: string;
+ port: string;
+ protocol: string;
+ search: string;
+ readonly searchParams: URLSearchParams;
+ username: string;
+ toJSON(): string;
+}
+
+interface URLSearchParams {
+ append(name: string, value: string): void;
+ delete(name: string): void;
+ get(name: string): string | null;
+ getAll(name: string): string[];
+ has(name: string): boolean;
+ set(name: string, value: string): void;
+ sort(): void;
+ toString(): string;
+ forEach(
+ callbackfn: (value: string, key: string, parent: URLSearchParams) => void,
+ thisArg?: any,
+ ): void;
+}
+
+export interface URLSearchParamsCtor {
+ new (
+ init?: string[][] | Record<string, string> | string | URLSearchParams,
+ ): URLSearchParams;
+}
+
+export interface URLCtor {
+ new (url: string, base?: string | URL): URL;
+}
+
+// @ts-ignore
+const _URL = globalThis.URL;
+if (!_URL) {
+ throw Error("FATAL: URL not available");
+}
+
+export const URL: URLCtor = _URL;
+
+// @ts-ignore
+const _URLSearchParams = globalThis.URLSearchParams;
+
+if (!_URLSearchParams) {
+ throw Error("FATAL: URLSearchParams not available");
+}
+
+export const URLSearchParams: URLSearchParamsCtor = _URLSearchParams;
diff --git a/packages/taler-util/src/walletTypes.ts b/packages/taler-util/src/walletTypes.ts
new file mode 100644
index 000000000..d25108df7
--- /dev/null
+++ b/packages/taler-util/src/walletTypes.ts
@@ -0,0 +1,936 @@
+/*
+ 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 * as LibtoolVersion from "./libtool-version.js";
+import { Timestamp, codecForTimestamp } from "./time.js";
+import {
+ buildCodecForObject,
+ codecForString,
+ codecOptional,
+ Codec,
+ codecForList,
+ codecForBoolean,
+ codecForConstString,
+ codecForAny,
+ buildCodecForUnion,
+} from "./codec.js";
+import {
+ AmountString,
+ codecForContractTerms,
+ ContractTerms,
+} from "./talerTypes.js";
+import { OrderShortInfo, codecForOrderShortInfo } from "./transactionsTypes";
+import { BackupRecovery } from "./backupTypes.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: TalerErrorDetails;
+}
+
+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;
+}
+
+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 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: Timestamp;
+}
+
+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(
+ "status",
+ codecForConstString(PreparePayResultType.PaymentPossible),
+ )
+ .build("PreparePayResultPaymentPossible");
+
+export const codecForPreparePayResultInsufficientBalance = (): Codec<PreparePayResultInsufficientBalance> =>
+ buildCodecForObject<PreparePayResultInsufficientBalance>()
+ .property("amountRaw", codecForAmountString())
+ .property("contractTerms", codecForAny())
+ .property("proposalId", 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;
+ amountRaw: string;
+ amountEffective: string;
+}
+
+export interface PreparePayResultInsufficientBalance {
+ status: PreparePayResultType.InsufficientBalance;
+ proposalId: string;
+ contractTerms: ContractTerms;
+ amountRaw: 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 TalerErrorDetails {
+ code: number;
+ hint: string;
+ message: string;
+ details: unknown;
+}
+
+export interface PlanchetCreationResult {
+ coinPub: string;
+ coinPriv: string;
+ reservePub: string;
+ denomPubHash: string;
+ denomPub: string;
+ blindingKey: string;
+ withdrawSig: string;
+ coinEv: string;
+ coinValue: AmountJson;
+ coinEvHash: string;
+}
+
+export interface PlanchetCreationRequest {
+ secretSeed: string;
+ coinIndex: number;
+ value: AmountJson;
+ feeWithdraw: AmountJson;
+ denomPub: string;
+ reservePub: string;
+ reservePriv: string;
+}
+
+/**
+ * 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: Timestamp;
+ refundDeadline: Timestamp;
+ merchantPub: string;
+ feeDeposit: AmountJson;
+ wireInfoHash: string;
+ denomPubHash: string;
+ denomSig: string;
+}
+
+export interface ExchangesListRespose {
+ exchanges: ExchangeListItem[];
+}
+
+export interface ExchangeListItem {
+ exchangeBaseUrl: string;
+ currency: string;
+ paytoUris: string[];
+}
+
+export const codecForExchangeListItem = (): Codec<ExchangeListItem> =>
+ buildCodecForObject<ExchangeListItem>()
+ .property("currency", codecForString())
+ .property("exchangeBaseUrl", codecForString())
+ .property("paytoUris", codecForList(codecForString()))
+ .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.
+ */
+ tos: 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;
+}
+
+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;
+}
+
+export const codecForAddExchangeRequest = (): Codec<AddExchangeRequest> =>
+ buildCodecForObject<AddExchangeRequest>()
+ .property("exchangeBaseUrl", codecForString())
+ .build("AddExchangeRequest");
+
+export interface ForceExchangeUpdateRequest {
+ exchangeBaseUrl: string;
+}
+
+export const codecForForceExchangeUpdateRequest = (): Codec<AddExchangeRequest> =>
+ buildCodecForObject<AddExchangeRequest>()
+ .property("exchangeBaseUrl", codecForString())
+ .build("AddExchangeRequest");
+
+export interface GetExchangeTosRequest {
+ exchangeBaseUrl: string;
+}
+
+export const codecForGetExchangeTosRequest = (): Codec<GetExchangeTosRequest> =>
+ buildCodecForObject<GetExchangeTosRequest>()
+ .property("exchangeBaseUrl", codecForString())
+ .build("GetExchangeTosRequest");
+
+export interface AcceptManualWithdrawalRequest {
+ exchangeBaseUrl: string;
+ amount: string;
+}
+
+export const codecForAcceptManualWithdrawalRequet = (): Codec<AcceptManualWithdrawalRequest> =>
+ buildCodecForObject<AcceptManualWithdrawalRequest>()
+ .property("exchangeBaseUrl", codecForString())
+ .property("amount", codecForString())
+ .build("AcceptManualWithdrawalRequest");
+
+export interface GetWithdrawalDetailsForAmountRequest {
+ exchangeBaseUrl: string;
+ amount: string;
+}
+
+export interface AcceptBankIntegratedWithdrawalRequest {
+ talerWithdrawUri: string;
+ exchangeBaseUrl: string;
+}
+
+export const codecForAcceptBankIntegratedWithdrawalRequest = (): Codec<AcceptBankIntegratedWithdrawalRequest> =>
+ buildCodecForObject<AcceptBankIntegratedWithdrawalRequest>()
+ .property("exchangeBaseUrl", codecForString())
+ .property("talerWithdrawUri", codecForString())
+ .build("AcceptBankIntegratedWithdrawalRequest");
+
+export const codecForGetWithdrawalDetailsForAmountRequest = (): Codec<GetWithdrawalDetailsForAmountRequest> =>
+ buildCodecForObject<GetWithdrawalDetailsForAmountRequest>()
+ .property("exchangeBaseUrl", codecForString())
+ .property("amount", codecForString())
+ .build("GetWithdrawalDetailsForAmountRequest");
+
+export interface AcceptExchangeTosRequest {
+ exchangeBaseUrl: string;
+ etag: string;
+}
+
+export const codecForAcceptExchangeTosRequest = (): Codec<AcceptExchangeTosRequest> =>
+ buildCodecForObject<AcceptExchangeTosRequest>()
+ .property("exchangeBaseUrl", codecForString())
+ .property("etag", 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;
+}
+
+export const codecForGetWithdrawalDetailsForUri = (): Codec<GetWithdrawalDetailsForUriRequest> =>
+ buildCodecForObject<GetWithdrawalDetailsForUriRequest>()
+ .property("talerWithdrawUri", codecForString())
+ .build("GetWithdrawalDetailsForUriRequest");
+
+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: TalerErrorDetails;
+}
+
+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;
+}
+
+/**
+ * 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 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 CreateDepositGroupRequest {
+ depositPaytoUri: string;
+ amount: string;
+}
+
+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");