summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core/src/util
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-wallet-core/src/util')
-rw-r--r--packages/taler-wallet-core/src/util/RequestThrottler.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/util/RequestThrottler.ts129
-rw-r--r--packages/taler-wallet-core/src/util/amounts-test.ts140
-rw-r--r--packages/taler-wallet-core/src/util/amounts.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/util/amounts.ts384
-rw-r--r--packages/taler-wallet-core/src/util/assertUnreachable.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/util/assertUnreachable.ts19
-rw-r--r--packages/taler-wallet-core/src/util/asyncMemo.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/util/asyncMemo.ts87
-rw-r--r--packages/taler-wallet-core/src/util/codec-test.ts78
-rw-r--r--packages/taler-wallet-core/src/util/codec.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/util/codec.ts406
-rw-r--r--packages/taler-wallet-core/src/util/helpers-test.ts46
-rw-r--r--packages/taler-wallet-core/src/util/helpers.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/util/helpers.ts148
-rw-r--r--packages/taler-wallet-core/src/util/http.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/util/http.ts237
-rw-r--r--packages/taler-wallet-core/src/util/libtoolVersion-test.ts48
-rw-r--r--packages/taler-wallet-core/src/util/libtoolVersion.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/util/libtoolVersion.ts88
-rw-r--r--packages/taler-wallet-core/src/util/logging.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/util/logging.ts89
-rw-r--r--packages/taler-wallet-core/src/util/payto-test.ts31
-rw-r--r--packages/taler-wallet-core/src/util/payto.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/util/payto.ts71
-rw-r--r--packages/taler-wallet-core/src/util/promiseUtils.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/util/promiseUtils.ts60
-rw-r--r--packages/taler-wallet-core/src/util/query.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/util/query.ts576
-rw-r--r--packages/taler-wallet-core/src/util/reserveHistoryUtil-test.ts285
-rw-r--r--packages/taler-wallet-core/src/util/reserveHistoryUtil.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/util/reserveHistoryUtil.ts360
-rw-r--r--packages/taler-wallet-core/src/util/talerconfig.ts120
-rw-r--r--packages/taler-wallet-core/src/util/taleruri-test.ts184
-rw-r--r--packages/taler-wallet-core/src/util/taleruri.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/util/taleruri.ts215
-rw-r--r--packages/taler-wallet-core/src/util/testvectors.ts36
-rw-r--r--packages/taler-wallet-core/src/util/time.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/util/time.ts198
-rw-r--r--packages/taler-wallet-core/src/util/timer.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/util/timer.ts165
-rw-r--r--packages/taler-wallet-core/src/util/url.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/util/url.ts74
-rw-r--r--packages/taler-wallet-core/src/util/wire.ts51
44 files changed, 4342 insertions, 0 deletions
diff --git a/packages/taler-wallet-core/src/util/RequestThrottler.d.ts.map b/packages/taler-wallet-core/src/util/RequestThrottler.d.ts.map
new file mode 100644
index 000000000..3a2fa1081
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/RequestThrottler.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"RequestThrottler.d.ts","sourceRoot":"","sources":["RequestThrottler.ts"],"names":[],"mappings":"AAiGA;;;;GAIG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,aAAa,CAAyC;IAE9D;;;;OAIG;IACH,OAAO,CAAC,QAAQ;IAShB;;;;OAIG;IACH,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO;CAI3C"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/util/RequestThrottler.ts b/packages/taler-wallet-core/src/util/RequestThrottler.ts
new file mode 100644
index 000000000..6f51a72bc
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/RequestThrottler.ts
@@ -0,0 +1,129 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 GNUnet e.V.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Implementation of token bucket throttling.
+ */
+
+/**
+ * Imports.
+ */
+import { getTimestampNow, timestampDifference } from "../util/time";
+import { URL } from "./url";
+
+/**
+ * Maximum request per second, per origin.
+ */
+const MAX_PER_SECOND = 50;
+
+/**
+ * Maximum request per minute, per origin.
+ */
+const MAX_PER_MINUTE = 100;
+
+/**
+ * Maximum request per hour, per origin.
+ */
+const MAX_PER_HOUR = 1000;
+
+/**
+ * Throttling state for one origin.
+ */
+class OriginState {
+ private tokensSecond: number = MAX_PER_SECOND;
+ private tokensMinute: number = MAX_PER_MINUTE;
+ private tokensHour: number = MAX_PER_HOUR;
+ private lastUpdate = getTimestampNow();
+
+ private refill(): void {
+ const now = getTimestampNow();
+ const d = timestampDifference(now, this.lastUpdate);
+ if (d.d_ms === "forever") {
+ throw Error("assertion failed");
+ }
+ const d_s = d.d_ms / 1000;
+ this.tokensSecond = Math.min(
+ MAX_PER_SECOND,
+ this.tokensSecond + d_s / 1000,
+ );
+ this.tokensMinute = Math.min(
+ MAX_PER_MINUTE,
+ this.tokensMinute + (d_s / 1000) * 60,
+ );
+ this.tokensHour = Math.min(
+ MAX_PER_HOUR,
+ this.tokensHour + (d_s / 1000) * 60 * 60,
+ );
+ this.lastUpdate = now;
+ }
+
+ /**
+ * Return true if the request for this origin should be throttled.
+ * Otherwise, take a token out of the respective buckets.
+ */
+ applyThrottle(): boolean {
+ this.refill();
+ if (this.tokensSecond < 1) {
+ console.log("request throttled (per second limit exceeded)");
+ return true;
+ }
+ if (this.tokensMinute < 1) {
+ console.log("request throttled (per minute limit exceeded)");
+ return true;
+ }
+ if (this.tokensHour < 1) {
+ console.log("request throttled (per hour limit exceeded)");
+ return true;
+ }
+ this.tokensSecond--;
+ this.tokensMinute--;
+ this.tokensHour--;
+ return false;
+ }
+}
+
+/**
+ * Request throttler, used as a "last layer of defense" when some
+ * other part of the re-try logic is broken and we're sending too
+ * many requests to the same exchange/bank/merchant.
+ */
+export class RequestThrottler {
+ private perOriginInfo: { [origin: string]: OriginState } = {};
+
+ /**
+ * Get the throttling state for an origin, or
+ * initialize if no state is associated with the
+ * origin yet.
+ */
+ private getState(origin: string): OriginState {
+ const s = this.perOriginInfo[origin];
+ if (s) {
+ return s;
+ }
+ const ns = (this.perOriginInfo[origin] = new OriginState());
+ return ns;
+ }
+
+ /**
+ * Apply throttling to a request.
+ *
+ * @returns whether the request should be throttled.
+ */
+ applyThrottle(requestUrl: string): boolean {
+ const origin = new URL(requestUrl).origin;
+ return this.getState(origin).applyThrottle();
+ }
+}
diff --git a/packages/taler-wallet-core/src/util/amounts-test.ts b/packages/taler-wallet-core/src/util/amounts-test.ts
new file mode 100644
index 000000000..afd8caa51
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/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 "../util/amounts";
+
+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-wallet-core/src/util/amounts.d.ts.map b/packages/taler-wallet-core/src/util/amounts.d.ts.map
new file mode 100644
index 000000000..c70d06fb7
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/amounts.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"amounts.d.ts","sourceRoot":"","sources":["amounts.ts"],"names":[],"mappings":"AAgBA;;GAEG;AAEH;;GAEG;AACH,OAAO,EAIL,KAAK,EACN,MAAM,SAAS,CAAC;AAEjB;;GAEG;AACH,eAAO,MAAM,cAAc,YAAM,CAAC;AAElC;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,IAAI,CAAC;AAElC;;GAEG;AACH,eAAO,MAAM,cAAc,QAAU,CAAC;AAEtC;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB;;OAEG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAEvB;;OAEG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAE1B;;OAEG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED,eAAO,MAAM,kBAAkB,QAAO,KAAK,CAAC,UAAU,CAK9B,CAAC;AAEzB;;GAEG;AACH,MAAM,WAAW,MAAM;IACrB;;OAEG;IACH,MAAM,EAAE,UAAU,CAAC;IACnB;;OAEG;IACH,SAAS,EAAE,OAAO,CAAC;CACpB;AAED;;GAEG;AACH,wBAAgB,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,CAMpD;AAED,wBAAgB,GAAG,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG,MAAM,CAKjD;AAED;;;;;;GAMG;AACH,wBAAgB,GAAG,CAAC,KAAK,EAAE,UAAU,EAAE,GAAG,IAAI,EAAE,UAAU,EAAE,GAAG,MAAM,CA8BpE;AAED;;;;;;GAMG;AACH,wBAAgB,GAAG,CAAC,CAAC,EAAE,UAAU,EAAE,GAAG,IAAI,EAAE,UAAU,EAAE,GAAG,MAAM,CAyBhE;AAED;;;GAGG;AACH,wBAAgB,GAAG,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,UAAU,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAsB5D;AAED;;GAEG;AACH,wBAAgB,IAAI,CAAC,CAAC,EAAE,UAAU,GAAG,UAAU,CAM9C;AAED;;GAEG;AACH,wBAAgB,MAAM,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,MAAM,GAAG,UAAU,CAa3D;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,UAAU,GAAG,OAAO,CAEhD;AAED,wBAAgB,MAAM,CAAC,CAAC,EAAE,UAAU,GAAG,OAAO,CAE7C;AAED;;GAEG;AACH,wBAAgB,KAAK,CAAC,CAAC,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS,CAkBvD;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,GAAG,UAAU,CAMlD;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,UAAU,CAMxE;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,UAAU,GAAG,MAAM,CAkB/C;AAED;;GAEG;AACH,iBAAS,KAAK,CAAC,CAAC,EAAE,GAAG,GAAG,OAAO,CAU9B;AAED,iBAAS,IAAI,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CA8B9C;AAGD,eAAO,MAAM,OAAO;;;;;;;;;;;;;;;;CAgBnB,CAAC"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/util/amounts.ts b/packages/taler-wallet-core/src/util/amounts.ts
new file mode 100644
index 000000000..00f4b17d7
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/amounts.ts
@@ -0,0 +1,384 @@
+/*
+ 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 {
+ makeCodecForObject,
+ codecForString,
+ codecForNumber,
+ Codec,
+} from "./codec";
+
+/**
+ * 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> =>
+ makeCodecForObject<AmountJson>()
+ .property("currency", codecForString)
+ .property("value", codecForNumber)
+ .property("fraction", codecForNumber)
+ .build("AmountJson");
+
+/**
+ * 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 function sum(amounts: AmountJson[]): Result {
+ if (amounts.length <= 0) {
+ throw Error("can't sum zero amounts");
+ }
+ return add(amounts[0], ...amounts.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: AmountJson, b: AmountJson): -1 | 0 | 1 {
+ 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: AmountJson): boolean {
+ 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: AmountJson): string {
+ 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);
+}
+
+// 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,
+ check: check,
+ getZero: getZero,
+ isZero: isZero,
+ maxAmountValue: maxAmountValue,
+ fromFloat: fromFloat,
+ copy: copy,
+ fractionalBase: fractionalBase,
+};
diff --git a/packages/taler-wallet-core/src/util/assertUnreachable.d.ts.map b/packages/taler-wallet-core/src/util/assertUnreachable.d.ts.map
new file mode 100644
index 000000000..64a1ed8e8
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/assertUnreachable.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"assertUnreachable.d.ts","sourceRoot":"","sources":["assertUnreachable.ts"],"names":[],"mappings":"AAgBA,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,KAAK,GAAG,KAAK,CAEjD"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/util/assertUnreachable.ts b/packages/taler-wallet-core/src/util/assertUnreachable.ts
new file mode 100644
index 000000000..ffdf88f04
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/assertUnreachable.ts
@@ -0,0 +1,19 @@
+/*
+ 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/>
+ */
+
+export function assertUnreachable(x: never): never {
+ throw new Error("Didn't expect to get here");
+}
diff --git a/packages/taler-wallet-core/src/util/asyncMemo.d.ts.map b/packages/taler-wallet-core/src/util/asyncMemo.d.ts.map
new file mode 100644
index 000000000..0b764b61b
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/asyncMemo.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"asyncMemo.d.ts","sourceRoot":"","sources":["asyncMemo.ts"],"names":[],"mappings":"AAsBA,qBAAa,cAAc,CAAC,CAAC;IAC3B,OAAO,CAAC,CAAC,CAAK;IACd,OAAO,CAAC,OAAO,CAAqC;IAEpD,OAAO,CAAC,OAAO;IAOf,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAiBnD,KAAK,IAAI,IAAI;CAGd;AAED,qBAAa,iBAAiB,CAAC,CAAC;IAC9B,OAAO,CAAC,CAAC,CAAK;IACd,OAAO,CAAC,SAAS,CAA2B;IAE5C,OAAO,CAAC,OAAO;IAMf,IAAI,CAAC,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAkBtC,KAAK,IAAI,IAAI;CAGd"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/util/asyncMemo.ts b/packages/taler-wallet-core/src/util/asyncMemo.ts
new file mode 100644
index 000000000..6e88081b6
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/asyncMemo.ts
@@ -0,0 +1,87 @@
+/*
+ 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/>
+ */
+
+interface MemoEntry<T> {
+ p: Promise<T>;
+ t: number;
+ n: number;
+}
+
+export class AsyncOpMemoMap<T> {
+ private n = 0;
+ private memoMap: { [k: string]: MemoEntry<T> } = {};
+
+ private cleanUp(key: string, n: number): void {
+ const r = this.memoMap[key];
+ if (r && r.n === n) {
+ delete this.memoMap[key];
+ }
+ }
+
+ memo(key: string, pg: () => Promise<T>): Promise<T> {
+ const res = this.memoMap[key];
+ if (res) {
+ return res.p;
+ }
+ const n = this.n++;
+ // Wrap the operation in case it immediately throws
+ const p = Promise.resolve().then(() => pg());
+ this.memoMap[key] = {
+ p,
+ n,
+ t: new Date().getTime(),
+ };
+ return p.finally(() => {
+ this.cleanUp(key, n);
+ });
+ }
+ clear(): void {
+ this.memoMap = {};
+ }
+}
+
+export class AsyncOpMemoSingle<T> {
+ private n = 0;
+ private memoEntry: MemoEntry<T> | undefined;
+
+ private cleanUp(n: number): void {
+ if (this.memoEntry && this.memoEntry.n === n) {
+ this.memoEntry = undefined;
+ }
+ }
+
+ memo(pg: () => Promise<T>): Promise<T> {
+ const res = this.memoEntry;
+ if (res) {
+ return res.p;
+ }
+ const n = this.n++;
+ // Wrap the operation in case it immediately throws
+ const p = Promise.resolve().then(() => pg());
+ p.finally(() => {
+ this.cleanUp(n);
+ });
+ this.memoEntry = {
+ p,
+ n,
+ t: new Date().getTime(),
+ };
+ return p;
+ }
+ clear(): void {
+ this.memoEntry = undefined;
+ }
+}
diff --git a/packages/taler-wallet-core/src/util/codec-test.ts b/packages/taler-wallet-core/src/util/codec-test.ts
new file mode 100644
index 000000000..b429c318c
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/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,
+ makeCodecForObject,
+ makeCodecForConstString,
+ codecForString,
+ makeCodecForUnion,
+} 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 = makeCodecForObject<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> = makeCodecForObject<AltOne>()
+ .property("type", makeCodecForConstString("one"))
+ .property("foo", codecForString)
+ .build("AltOne");
+ const altTwoCodec: Codec<AltTwo> = makeCodecForObject<AltTwo>()
+ .property("type", makeCodecForConstString("two"))
+ .property("bar", codecForString)
+ .build("AltTwo");
+ const myUnionCodec: Codec<MyUnion> = makeCodecForUnion<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-wallet-core/src/util/codec.d.ts.map b/packages/taler-wallet-core/src/util/codec.d.ts.map
new file mode 100644
index 000000000..4304f5cef
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/codec.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"codec.d.ts","sourceRoot":"","sources":["codec.ts"],"names":[],"mappings":"AAgBA;;GAEG;AAIH;;GAEG;AACH,qBAAa,aAAc,SAAQ,KAAK;gBAC1B,OAAO,EAAE,MAAM;CAK5B;AAED;;GAEG;AACH,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED,wBAAgB,aAAa,CAAC,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,CAOjD;AASD;;GAEG;AACH,MAAM,WAAW,KAAK,CAAC,CAAC;IACtB;;OAEG;IACH,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,CAAC,CAAC;CAC7C;AAED,aAAK,eAAe,CAAC,CAAC,SAAS,MAAM,GAAG,EAAE,CAAC,IAAI;KAAG,CAAC,IAAI,CAAC,GAAG,CAAC;CAAE,CAAC;AAY/D,cAAM,kBAAkB,CAAC,UAAU,EAAE,iBAAiB;IACpD,OAAO,CAAC,QAAQ,CAAc;IAE9B;;OAEG;IACH,QAAQ,CAAC,CAAC,SAAS,MAAM,UAAU,GAAG,MAAM,EAAE,CAAC,SAAS,UAAU,CAAC,CAAC,CAAC,EACnE,CAAC,EAAE,CAAC,EACJ,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,GACd,kBAAkB,CAAC,UAAU,EAAE,iBAAiB,GAAG,eAAe,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAQ5E;;;;;OAKG;IACH,KAAK,CAAC,iBAAiB,EAAE,MAAM,GAAG,KAAK,CAAC,iBAAiB,CAAC;CA6B3D;AAED,cAAM,iBAAiB,CACrB,UAAU,EACV,gBAAgB,SAAS,MAAM,UAAU,EACzC,cAAc,EACd,iBAAiB;IAKf,OAAO,CAAC,aAAa;IACrB,OAAO,CAAC,SAAS,CAAC;IAJpB,OAAO,CAAC,YAAY,CAA+B;gBAGzC,aAAa,EAAE,gBAAgB,EAC/B,SAAS,CAAC,mCAAuB;IAG3C;;OAEG;IACH,WAAW,CAAC,CAAC,EACX,QAAQ,EAAE,UAAU,CAAC,gBAAgB,CAAC,EACtC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,GACd,iBAAiB,CAClB,UAAU,EACV,gBAAgB,EAChB,cAAc,EACd,iBAAiB,GAAG,CAAC,CACtB;IAQD;;;;;OAKG;IACH,KAAK,CAAC,CAAC,SAAS,iBAAiB,GAAG,cAAc,GAAG,KAAK,EACxD,iBAAiB,EAAE,MAAM,GACxB,KAAK,CAAC,CAAC,CAAC;CAqCZ;AAED,qBAAa,oBAAoB,CAAC,CAAC;IACjC,cAAc,CAAC,CAAC,SAAS,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,EACtC,aAAa,EAAE,CAAC,EAChB,SAAS,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,GACnB,iBAAiB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,KAAK,CAAC;CAGrC;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,KAAK,kBAAkB,CAAC,CAAC,EAAE,EAAE,CAAC,CAEjE;AAED,wBAAgB,iBAAiB,CAAC,CAAC,KAAK,oBAAoB,CAAC,CAAC,CAAC,CAE9D;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAC/B,UAAU,EAAE,KAAK,CAAC,CAAC,CAAC,GACnB,KAAK,CAAC;IAAE,CAAC,CAAC,EAAE,MAAM,GAAG,CAAC,CAAA;CAAE,CAAC,CAgB3B;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,UAAU,EAAE,KAAK,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,EAAE,CAAC,CAgBpE;AAED;;GAEG;AACH,eAAO,MAAM,cAAc,EAAE,KAAK,CAAC,MAAM,CASxC,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,OAAO,CAS1C,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,cAAc,EAAE,KAAK,CAAC,MAAM,CASxC,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,WAAW,EAAE,KAAK,CAAC,GAAG,CAIlC,CAAC;AAEF;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAaxE;AAED;;GAEG;AACH,wBAAgB,qBAAqB,IAAI,KAAK,CAAC,IAAI,CAAC,CAanD;AAED;;GAEG;AACH,wBAAgB,sBAAsB,IAAI,KAAK,CAAC,KAAK,CAAC,CAarD;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAaxE;AAED,wBAAgB,iBAAiB,CAAC,CAAC,EACjC,UAAU,EAAE,KAAK,CAAC,CAAC,CAAC,GACnB,KAAK,CAAC,CAAC,GAAG,SAAS,CAAC,CAStB"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/util/codec.ts b/packages/taler-wallet-core/src/util/codec.ts
new file mode 100644
index 000000000..2ce3c2cba
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/codec.ts
@@ -0,0 +1,406 @@
+/*
+ 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 makeCodecForObject<T>(): ObjectCodecBuilder<T, {}> {
+ return new ObjectCodecBuilder<T, {}>();
+}
+
+export function makeCodecForUnion<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 makeCodecForMap<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 makeCodecForList<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 const codecForNumber: Codec<number> = {
+ 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 const codecForBoolean: Codec<boolean> = {
+ 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 const codecForString: Codec<string> = {
+ 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 const codecForAny: Codec<any> = {
+ decode(x: any, c?: Context): any {
+ return x;
+ },
+};
+
+/**
+ * Return a codec for a value that must be a string.
+ */
+export function makeCodecForConstString<V extends string>(s: V): Codec<V> {
+ return {
+ decode(x: any, c?: Context): V {
+ if (x === s) {
+ return x;
+ }
+ throw new DecodingError(
+ `expected string constant "${s}" at ${renderContext(
+ c,
+ )} but got ${typeof x}`,
+ );
+ },
+ };
+}
+
+/**
+ * Return a codec for a boolean true constant.
+ */
+export function makeCodecForConstTrue(): 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 makeCodecForConstFalse(): 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 makeCodecForConstNumber<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 makeCodecOptional<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-wallet-core/src/util/helpers-test.ts b/packages/taler-wallet-core/src/util/helpers-test.ts
new file mode 100644
index 000000000..dbecf14b8
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/helpers-test.ts
@@ -0,0 +1,46 @@
+/*
+ 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 * as helpers from "./helpers";
+
+test("URL canonicalization", (t) => {
+ // converts to relative, adds https
+ t.is(
+ "https://alice.example.com/exchange/",
+ helpers.canonicalizeBaseUrl("alice.example.com/exchange"),
+ );
+
+ // keeps http, adds trailing slash
+ t.is(
+ "http://alice.example.com/exchange/",
+ helpers.canonicalizeBaseUrl("http://alice.example.com/exchange"),
+ );
+
+ // keeps http, adds trailing slash
+ t.is(
+ "http://alice.example.com/exchange/",
+ helpers.canonicalizeBaseUrl("http://alice.example.com/exchange#foobar"),
+ );
+
+ // Remove search component
+ t.is(
+ "http://alice.example.com/exchange/",
+ helpers.canonicalizeBaseUrl("http://alice.example.com/exchange?foo=bar"),
+ );
+
+ t.pass();
+});
diff --git a/packages/taler-wallet-core/src/util/helpers.d.ts.map b/packages/taler-wallet-core/src/util/helpers.d.ts.map
new file mode 100644
index 000000000..789c5c81c
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/helpers.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["helpers.ts"],"names":[],"mappings":"AAgBA;;GAEG;AAEH;;GAEG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAIvC;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAGzD;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAWvD;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,GAAG,GAAG,MAAM,CAwB9C;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,GAAG,OAAO,CAclD;AAED,wBAAgB,QAAQ,CAAC,CAAC,EAAE,GAAG,GAAG,GAAG,CAGpC;AAED;;;GAGG;AACH,wBAAgB,OAAO,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,EAAE,CAE5D;AAED;;GAEG;AACH,wBAAgB,IAAI,CAAC,GAAG,EAAE,GAAG,GAAG,MAAM,CAarC;AAED;;GAEG;AACH,wBAAgB,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAQrD"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/util/helpers.ts b/packages/taler-wallet-core/src/util/helpers.ts
new file mode 100644
index 000000000..ae4b0359e
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/helpers.ts
@@ -0,0 +1,148 @@
+/*
+ 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.
+ 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;
+}
diff --git a/packages/taler-wallet-core/src/util/http.d.ts.map b/packages/taler-wallet-core/src/util/http.d.ts.map
new file mode 100644
index 000000000..edbe41970
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/http.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["http.ts"],"names":[],"mappings":"AAgBA;;;GAGG;AAEH;;GAEG;AACH,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAOhC;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,IAAI,OAAO,CAAC,GAAG,CAAC,CAAC;IACrB,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;CACzB;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,CAAC,EAAE;QAAE,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;CACtC;AAED,oBAAY,kBAAkB;IAC5B,EAAE,MAAM;IACR,IAAI,MAAM;CACX;AAED;;GAEG;AACH,qBAAa,OAAO;IAClB,OAAO,CAAC,SAAS,CAA6B;IAE9C,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAQhC,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;CASvC;AAED;;;;;GAKG;AACH,MAAM,WAAW,kBAAkB;IACjC;;OAEG;IACH,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IAElE;;OAEG;IACH,QAAQ,CACN,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,GAAG,EACT,GAAG,CAAC,EAAE,kBAAkB,GACvB,OAAO,CAAC,YAAY,CAAC,CAAC;CAC1B;AAED,aAAK,kBAAkB,GAAG;IACxB,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,OAAO,CAAC;AAEZ,aAAK,eAAe,CAAC,CAAC,IAClB;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,QAAQ,EAAE,CAAC,CAAA;CAAE,GAC/B;IAAE,OAAO,EAAE,IAAI,CAAC;IAAC,kBAAkB,EAAE,kBAAkB,CAAA;CAAE,CAAC;AAE9D,wBAAsB,kCAAkC,CAAC,CAAC,EACxD,YAAY,EAAE,YAAY,EAC1B,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,GACd,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,CAuC7B;AAED,wBAAgB,2BAA2B,CACzC,YAAY,EAAE,YAAY,EAC1B,kBAAkB,EAAE,kBAAkB,GACrC,KAAK,CAYP;AAED,wBAAsB,8BAA8B,CAAC,CAAC,EACpD,YAAY,EAAE,YAAY,EAC1B,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,GACd,OAAO,CAAC,CAAC,CAAC,CAMZ;AAGD,wBAAsB,kCAAkC,CAAC,CAAC,EACxD,YAAY,EAAE,YAAY,GACzB,OAAO,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC,CAyBlC;AAED,wBAAsB,2BAA2B,CAC/C,YAAY,EAAE,YAAY,GACzB,OAAO,CAAC,IAAI,CAAC,CAiBf;AAED,wBAAsB,8BAA8B,CAAC,CAAC,EACpD,YAAY,EAAE,YAAY,GACzB,OAAO,CAAC,MAAM,CAAC,CAMjB"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/util/http.ts b/packages/taler-wallet-core/src/util/http.ts
new file mode 100644
index 000000000..ad9f0293c
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/http.ts
@@ -0,0 +1,237 @@
+/*
+ This file is part of TALER
+ (C) 2016 GNUnet e.V.
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Helpers for doing XMLHttpRequest-s that are based on ES6 promises.
+ * Allows for easy mocking for test cases.
+ */
+
+/**
+ * Imports
+ */
+import { Codec } from "./codec";
+import { OperationFailedError, makeErrorDetails } from "../operations/errors";
+import { TalerErrorCode } from "../TalerErrorCode";
+import { Logger } from "./logging";
+
+const logger = new Logger("http.ts");
+
+/**
+ * An HTTP response that is returned by all request methods of this library.
+ */
+export interface HttpResponse {
+ requestUrl: string;
+ status: number;
+ headers: Headers;
+ json(): Promise<any>;
+ text(): Promise<string>;
+}
+
+export interface HttpRequestOptions {
+ headers?: { [name: string]: string };
+}
+
+export enum HttpResponseStatus {
+ Ok = 200,
+ Gone = 210,
+}
+
+/**
+ * Headers, roughly modeled after the fetch API's headers object.
+ */
+export class Headers {
+ private headerMap = new Map<string, string>();
+
+ get(name: string): string | null {
+ const r = this.headerMap.get(name.toLowerCase());
+ if (r) {
+ return r;
+ }
+ return null;
+ }
+
+ set(name: string, value: string): void {
+ const normalizedName = name.toLowerCase();
+ const existing = this.headerMap.get(normalizedName);
+ if (existing !== undefined) {
+ this.headerMap.set(normalizedName, existing + "," + value);
+ } else {
+ this.headerMap.set(normalizedName, value);
+ }
+ }
+}
+
+/**
+ * Interface for the HTTP request library used by the wallet.
+ *
+ * The request library is bundled into an interface to make mocking and
+ * request tunneling easy.
+ */
+export interface HttpRequestLibrary {
+ /**
+ * Make an HTTP GET request.
+ */
+ get(url: string, opt?: HttpRequestOptions): Promise<HttpResponse>;
+
+ /**
+ * Make an HTTP POST request with a JSON body.
+ */
+ postJson(
+ url: string,
+ body: any,
+ opt?: HttpRequestOptions,
+ ): Promise<HttpResponse>;
+}
+
+type TalerErrorResponse = {
+ code: number;
+} & unknown;
+
+type ResponseOrError<T> =
+ | { isError: false; response: T }
+ | { isError: true; talerErrorResponse: TalerErrorResponse };
+
+export async function readSuccessResponseJsonOrErrorCode<T>(
+ httpResponse: HttpResponse,
+ codec: Codec<T>,
+): Promise<ResponseOrError<T>> {
+ if (!(httpResponse.status >= 200 && httpResponse.status < 300)) {
+ const errJson = await httpResponse.json();
+ const talerErrorCode = errJson.code;
+ if (typeof talerErrorCode !== "number") {
+ throw new OperationFailedError(
+ makeErrorDetails(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ "Error response did not contain error code",
+ {
+ requestUrl: httpResponse.requestUrl,
+ },
+ ),
+ );
+ }
+ return {
+ isError: true,
+ talerErrorResponse: errJson,
+ };
+ }
+ const respJson = await httpResponse.json();
+ let parsedResponse: T;
+ try {
+ parsedResponse = codec.decode(respJson);
+ } catch (e) {
+ throw OperationFailedError.fromCode(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ "Response invalid",
+ {
+ requestUrl: httpResponse.requestUrl,
+ httpStatusCode: httpResponse.status,
+ validationError: e.toString(),
+ },
+ );
+ }
+ return {
+ isError: false,
+ response: parsedResponse,
+ };
+}
+
+export function throwUnexpectedRequestError(
+ httpResponse: HttpResponse,
+ talerErrorResponse: TalerErrorResponse,
+): never {
+ throw new OperationFailedError(
+ makeErrorDetails(
+ TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
+ "Unexpected error code in response",
+ {
+ requestUrl: httpResponse.requestUrl,
+ httpStatusCode: httpResponse.status,
+ errorResponse: talerErrorResponse,
+ },
+ ),
+ );
+}
+
+export async function readSuccessResponseJsonOrThrow<T>(
+ httpResponse: HttpResponse,
+ codec: Codec<T>,
+): Promise<T> {
+ const r = await readSuccessResponseJsonOrErrorCode(httpResponse, codec);
+ if (!r.isError) {
+ return r.response;
+ }
+ throwUnexpectedRequestError(httpResponse, r.talerErrorResponse);
+}
+
+export async function readSuccessResponseTextOrErrorCode<T>(
+ httpResponse: HttpResponse,
+): Promise<ResponseOrError<string>> {
+ if (!(httpResponse.status >= 200 && httpResponse.status < 300)) {
+ const errJson = await httpResponse.json();
+ const talerErrorCode = errJson.code;
+ if (typeof talerErrorCode !== "number") {
+ throw new OperationFailedError(
+ makeErrorDetails(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ "Error response did not contain error code",
+ {
+ requestUrl: httpResponse.requestUrl,
+ },
+ ),
+ );
+ }
+ return {
+ isError: true,
+ talerErrorResponse: errJson,
+ };
+ }
+ const respJson = await httpResponse.text();
+ return {
+ isError: false,
+ response: respJson,
+ };
+}
+
+export async function checkSuccessResponseOrThrow(
+ httpResponse: HttpResponse,
+): Promise<void> {
+ if (!(httpResponse.status >= 200 && httpResponse.status < 300)) {
+ const errJson = await httpResponse.json();
+ const talerErrorCode = errJson.code;
+ if (typeof talerErrorCode !== "number") {
+ throw new OperationFailedError(
+ makeErrorDetails(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ "Error response did not contain error code",
+ {
+ requestUrl: httpResponse.requestUrl,
+ },
+ ),
+ );
+ }
+ throwUnexpectedRequestError(httpResponse, errJson);
+ }
+}
+
+export async function readSuccessResponseTextOrThrow<T>(
+ httpResponse: HttpResponse,
+): Promise<string> {
+ const r = await readSuccessResponseTextOrErrorCode(httpResponse);
+ if (!r.isError) {
+ return r.response;
+ }
+ throwUnexpectedRequestError(httpResponse, r.talerErrorResponse);
+}
diff --git a/packages/taler-wallet-core/src/util/libtoolVersion-test.ts b/packages/taler-wallet-core/src/util/libtoolVersion-test.ts
new file mode 100644
index 000000000..e58e94759
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/libtoolVersion-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 "./libtoolVersion";
+
+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-wallet-core/src/util/libtoolVersion.d.ts.map b/packages/taler-wallet-core/src/util/libtoolVersion.d.ts.map
new file mode 100644
index 000000000..d0e111aa1
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/libtoolVersion.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"libtoolVersion.d.ts","sourceRoot":"","sources":["libtoolVersion.ts"],"names":[],"mappings":"AAgBA;;;GAGG;AAEH;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC;;OAEG;IACH,UAAU,EAAE,OAAO,CAAC;IACpB;;;OAGG;IACH,UAAU,EAAE,MAAM,CAAC;CACpB;AAQD;;GAEG;AACH,wBAAgB,OAAO,CACrB,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,MAAM,GACZ,kBAAkB,GAAG,SAAS,CAehC"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/util/libtoolVersion.ts b/packages/taler-wallet-core/src/util/libtoolVersion.ts
new file mode 100644
index 000000000..5e9d0b74e
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/libtoolVersion.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-wallet-core/src/util/logging.d.ts.map b/packages/taler-wallet-core/src/util/logging.d.ts.map
new file mode 100644
index 000000000..3e289d866
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/logging.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"logging.d.ts","sourceRoot":"","sources":["logging.ts"],"names":[],"mappings":"AAuCA;;;GAGG;AACH,qBAAa,MAAM;IACL,OAAO,CAAC,GAAG;gBAAH,GAAG,EAAE,MAAM;IAE/B,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI;IAW3C,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI;IAW3C,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI;IAW5C,KAAK,CAAC,OAAO,EAAE,GAAG,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI;CAU1C"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/util/logging.ts b/packages/taler-wallet-core/src/util/logging.ts
new file mode 100644
index 000000000..e4f3be2ff
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/logging.ts
@@ -0,0 +1,89 @@
+/*
+ This file is part of TALER
+ (C) 2019 GNUnet e.V.
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Check if we are running under nodejs.
+ */
+
+const isNode =
+ typeof process !== "undefined" && process.release.name === "node";
+
+function writeNodeLog(
+ message: string,
+ tag: string,
+ level: string,
+ args: any[],
+): void {
+ process.stderr.write(`${new Date().toISOString()} ${tag} ${level} `);
+ process.stderr.write(message);
+ if (args.length != 0) {
+ process.stderr.write(" ");
+ process.stderr.write(JSON.stringify(args, undefined, 2));
+ }
+ process.stderr.write("\n");
+}
+
+/**
+ * Logger that writes to stderr when running under node,
+ * and uses the corresponding console.* method to log in the browser.
+ */
+export class Logger {
+ constructor(private tag: string) {}
+
+ info(message: string, ...args: any[]): void {
+ if (isNode) {
+ writeNodeLog(message, this.tag, "INFO", args);
+ } else {
+ console.info(
+ `${new Date().toISOString()} ${this.tag} INFO ` + message,
+ ...args,
+ );
+ }
+ }
+
+ warn(message: string, ...args: any[]): void {
+ if (isNode) {
+ writeNodeLog(message, this.tag, "WARN", args);
+ } else {
+ console.warn(
+ `${new Date().toISOString()} ${this.tag} INFO ` + message,
+ ...args,
+ );
+ }
+ }
+
+ error(message: string, ...args: any[]): void {
+ if (isNode) {
+ writeNodeLog(message, this.tag, "ERROR", args);
+ } else {
+ console.info(
+ `${new Date().toISOString()} ${this.tag} ERROR ` + message,
+ ...args,
+ );
+ }
+ }
+
+ trace(message: any, ...args: any[]): void {
+ if (isNode) {
+ writeNodeLog(message, this.tag, "TRACE", args);
+ } else {
+ console.info(
+ `${new Date().toISOString()} ${this.tag} TRACE ` + message,
+ ...args,
+ );
+ }
+ }
+}
diff --git a/packages/taler-wallet-core/src/util/payto-test.ts b/packages/taler-wallet-core/src/util/payto-test.ts
new file mode 100644
index 000000000..01280b650
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/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-wallet-core/src/util/payto.d.ts.map b/packages/taler-wallet-core/src/util/payto.d.ts.map
new file mode 100644
index 000000000..a23c5f5d4
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/payto.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"payto.d.ts","sourceRoot":"","sources":["payto.ts"],"names":[],"mappings":"AAkBA,UAAU,QAAQ;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE;QAAE,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;CACpC;AAID;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,CAAC,EAAE,MAAM,EACT,MAAM,EAAE;IAAE,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;CAAE,GACjC,MAAM,CAOR;AAED,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS,CA6B7D"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/util/payto.ts b/packages/taler-wallet-core/src/util/payto.ts
new file mode 100644
index 000000000..a1c47eb2f
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/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";
+
+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-wallet-core/src/util/promiseUtils.d.ts.map b/packages/taler-wallet-core/src/util/promiseUtils.d.ts.map
new file mode 100644
index 000000000..1ca9a4c99
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/promiseUtils.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"promiseUtils.d.ts","sourceRoot":"","sources":["promiseUtils.ts"],"names":[],"mappings":"AAgBA,MAAM,WAAW,aAAa,CAAC,CAAC;IAC9B,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IACpB,OAAO,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,IAAI,CAAC;IAC1B,MAAM,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,CAAC;CAC5B;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,CAAC,KAAK,aAAa,CAAC,CAAC,CAAC,CAYjD;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,YAAY,CAAgB;IACpC,OAAO,CAAC,mBAAmB,CAAsB;;IAOjD,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAIrB,OAAO,IAAI,IAAI;CAMhB"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/util/promiseUtils.ts b/packages/taler-wallet-core/src/util/promiseUtils.ts
new file mode 100644
index 000000000..d409686d9
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/promiseUtils.ts
@@ -0,0 +1,60 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 GNUnet e.V.
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+export interface OpenedPromise<T> {
+ promise: Promise<T>;
+ resolve: (val: T) => void;
+ reject: (err: any) => void;
+}
+
+/**
+ * Get an unresolved promise together with its extracted resolve / reject
+ * function.
+ */
+export function openPromise<T>(): OpenedPromise<T> {
+ let resolve: ((x?: any) => void) | null = null;
+ let reject: ((reason?: any) => void) | null = null;
+ const promise = new Promise<T>((res, rej) => {
+ resolve = res;
+ reject = rej;
+ });
+ if (!(resolve && reject)) {
+ // Never happens, unless JS implementation is broken
+ throw Error();
+ }
+ return { resolve, reject, promise };
+}
+
+export class AsyncCondition {
+ private _waitPromise: Promise<void>;
+ private _resolveWaitPromise: (val: void) => void;
+ constructor() {
+ const op = openPromise<void>();
+ this._waitPromise = op.promise;
+ this._resolveWaitPromise = op.resolve;
+ }
+
+ wait(): Promise<void> {
+ return this._waitPromise;
+ }
+
+ trigger(): void {
+ this._resolveWaitPromise();
+ const op = openPromise<void>();
+ this._waitPromise = op.promise;
+ this._resolveWaitPromise = op.resolve;
+ }
+}
diff --git a/packages/taler-wallet-core/src/util/query.d.ts.map b/packages/taler-wallet-core/src/util/query.d.ts.map
new file mode 100644
index 000000000..4b3fc92ea
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/query.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"query.d.ts","sourceRoot":"","sources":["query.ts"],"names":[],"mappings":"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"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/util/query.ts b/packages/taler-wallet-core/src/util/query.ts
new file mode 100644
index 000000000..53359752e
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/query.ts
@@ -0,0 +1,576 @@
+/*
+ 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/>
+ */
+
+/**
+ * Database query abstractions.
+ * @module Query
+ * @author Florian Dold
+ */
+
+/**
+ * Imports.
+ */
+import { openPromise } from "./promiseUtils";
+import type { idbtypes } from "idb-bridge";
+
+/**
+ * Exception that should be thrown by client code to abort a transaction.
+ */
+export const TransactionAbort = Symbol("transaction_abort");
+
+/**
+ * Definition of an object store.
+ */
+export class Store<T> {
+ constructor(
+ public name: string,
+ public storeParams?: idbtypes.IDBObjectStoreParameters,
+ public validator?: (v: T) => T,
+ ) {}
+}
+
+/**
+ * Options for an index.
+ */
+export interface IndexOptions {
+ /**
+ * If true and the path resolves to an array, create an index entry for
+ * each member of the array (instead of one index entry containing the full array).
+ *
+ * Defaults to false.
+ */
+ multiEntry?: boolean;
+}
+
+function requestToPromise(req: idbtypes.IDBRequest): Promise<any> {
+ const stack = Error("Failed request was started here.");
+ return new Promise((resolve, reject) => {
+ req.onsuccess = () => {
+ resolve(req.result);
+ };
+ req.onerror = () => {
+ console.log("error in DB request", req.error);
+ reject(req.error);
+ console.log("Request failed:", stack);
+ };
+ });
+}
+
+function transactionToPromise(tx: idbtypes.IDBTransaction): Promise<void> {
+ const stack = Error("Failed transaction was started here.");
+ return new Promise((resolve, reject) => {
+ tx.onabort = () => {
+ reject(TransactionAbort);
+ };
+ tx.oncomplete = () => {
+ resolve();
+ };
+ tx.onerror = () => {
+ console.error("Transaction failed:", stack);
+ reject(tx.error);
+ };
+ });
+}
+
+function applyMutation<T>(
+ req: idbtypes.IDBRequest,
+ f: (x: T) => T | undefined,
+): Promise<void> {
+ return new Promise((resolve, reject) => {
+ req.onsuccess = () => {
+ const cursor = req.result;
+ if (cursor) {
+ const val = cursor.value;
+ const modVal = f(val);
+ if (modVal !== undefined && modVal !== null) {
+ const req2: idbtypes.IDBRequest = cursor.update(modVal);
+ req2.onerror = () => {
+ reject(req2.error);
+ };
+ req2.onsuccess = () => {
+ cursor.continue();
+ };
+ } else {
+ cursor.continue();
+ }
+ } else {
+ resolve();
+ }
+ };
+ req.onerror = () => {
+ reject(req.error);
+ };
+ });
+}
+
+type CursorResult<T> = CursorEmptyResult<T> | CursorValueResult<T>;
+
+interface CursorEmptyResult<T> {
+ hasValue: false;
+}
+
+interface CursorValueResult<T> {
+ hasValue: true;
+ value: T;
+}
+
+class ResultStream<T> {
+ private currentPromise: Promise<void>;
+ private gotCursorEnd = false;
+ private awaitingResult = false;
+
+ constructor(private req: idbtypes.IDBRequest) {
+ this.awaitingResult = true;
+ let p = openPromise<void>();
+ this.currentPromise = p.promise;
+ req.onsuccess = () => {
+ if (!this.awaitingResult) {
+ throw Error("BUG: invariant violated");
+ }
+ const cursor = req.result;
+ if (cursor) {
+ this.awaitingResult = false;
+ p.resolve();
+ p = openPromise<void>();
+ this.currentPromise = p.promise;
+ } else {
+ this.gotCursorEnd = true;
+ p.resolve();
+ }
+ };
+ req.onerror = () => {
+ p.reject(req.error);
+ };
+ }
+
+ async toArray(): Promise<T[]> {
+ const arr: T[] = [];
+ while (true) {
+ const x = await this.next();
+ if (x.hasValue) {
+ arr.push(x.value);
+ } else {
+ break;
+ }
+ }
+ return arr;
+ }
+
+ async map<R>(f: (x: T) => R): Promise<R[]> {
+ const arr: R[] = [];
+ while (true) {
+ const x = await this.next();
+ if (x.hasValue) {
+ arr.push(f(x.value));
+ } else {
+ break;
+ }
+ }
+ return arr;
+ }
+
+ async forEachAsync(f: (x: T) => Promise<void>): Promise<void> {
+ while (true) {
+ const x = await this.next();
+ if (x.hasValue) {
+ await f(x.value);
+ } else {
+ break;
+ }
+ }
+ }
+
+ async forEach(f: (x: T) => void): Promise<void> {
+ while (true) {
+ const x = await this.next();
+ if (x.hasValue) {
+ f(x.value);
+ } else {
+ break;
+ }
+ }
+ }
+
+ async filter(f: (x: T) => boolean): Promise<T[]> {
+ const arr: T[] = [];
+ while (true) {
+ const x = await this.next();
+ if (x.hasValue) {
+ if (f(x.value)) {
+ arr.push(x.value);
+ }
+ } else {
+ break;
+ }
+ }
+ return arr;
+ }
+
+ async next(): Promise<CursorResult<T>> {
+ if (this.gotCursorEnd) {
+ return { hasValue: false };
+ }
+ if (!this.awaitingResult) {
+ const cursor: idbtypes.IDBCursor | undefined = this.req.result;
+ if (!cursor) {
+ throw Error("assertion failed");
+ }
+ this.awaitingResult = true;
+ cursor.continue();
+ }
+ await this.currentPromise;
+ if (this.gotCursorEnd) {
+ return { hasValue: false };
+ }
+ const cursor = this.req.result;
+ if (!cursor) {
+ throw Error("assertion failed");
+ }
+ return { hasValue: true, value: cursor.value };
+ }
+}
+
+export class TransactionHandle {
+ constructor(private tx: idbtypes.IDBTransaction) {}
+
+ put<T>(store: Store<T>, value: T, key?: any): Promise<any> {
+ const req = this.tx.objectStore(store.name).put(value, key);
+ return requestToPromise(req);
+ }
+
+ add<T>(store: Store<T>, value: T, key?: any): Promise<any> {
+ const req = this.tx.objectStore(store.name).add(value, key);
+ return requestToPromise(req);
+ }
+
+ get<T>(store: Store<T>, key: any): Promise<T | undefined> {
+ const req = this.tx.objectStore(store.name).get(key);
+ return requestToPromise(req);
+ }
+
+ getIndexed<S extends idbtypes.IDBValidKey, T>(
+ index: Index<S, T>,
+ key: any,
+ ): Promise<T | undefined> {
+ const req = this.tx
+ .objectStore(index.storeName)
+ .index(index.indexName)
+ .get(key);
+ return requestToPromise(req);
+ }
+
+ iter<T>(store: Store<T>, key?: any): ResultStream<T> {
+ const req = this.tx.objectStore(store.name).openCursor(key);
+ return new ResultStream<T>(req);
+ }
+
+ iterIndexed<S extends idbtypes.IDBValidKey, T>(
+ index: Index<S, T>,
+ key?: any,
+ ): ResultStream<T> {
+ const req = this.tx
+ .objectStore(index.storeName)
+ .index(index.indexName)
+ .openCursor(key);
+ return new ResultStream<T>(req);
+ }
+
+ delete<T>(store: Store<T>, key: any): Promise<void> {
+ const req = this.tx.objectStore(store.name).delete(key);
+ return requestToPromise(req);
+ }
+
+ mutate<T>(
+ store: Store<T>,
+ key: any,
+ f: (x: T) => T | undefined,
+ ): Promise<void> {
+ const req = this.tx.objectStore(store.name).openCursor(key);
+ return applyMutation(req, f);
+ }
+}
+
+function runWithTransaction<T>(
+ db: idbtypes.IDBDatabase,
+ stores: Store<any>[],
+ f: (t: TransactionHandle) => Promise<T>,
+ mode: "readonly" | "readwrite",
+): Promise<T> {
+ const stack = Error("Failed transaction was started here.");
+ return new Promise((resolve, reject) => {
+ const storeName = stores.map((x) => x.name);
+ const tx = db.transaction(storeName, mode);
+ let funResult: any = undefined;
+ let gotFunResult = false;
+ tx.oncomplete = () => {
+ // This is a fatal error: The transaction completed *before*
+ // the transaction function returned. Likely, the transaction
+ // function waited on a promise that is *not* resolved in the
+ // microtask queue, thus triggering the auto-commit behavior.
+ // Unfortunately, the auto-commit behavior of IDB can't be switched
+ // of. There are some proposals to add this functionality in the future.
+ if (!gotFunResult) {
+ const msg =
+ "BUG: transaction closed before transaction function returned";
+ console.error(msg);
+ reject(Error(msg));
+ }
+ resolve(funResult);
+ };
+ tx.onerror = () => {
+ console.error("error in transaction");
+ console.error(stack);
+ };
+ tx.onabort = () => {
+ if (tx.error) {
+ console.error("Transaction aborted with error:", tx.error);
+ } else {
+ console.log("Trasaction aborted (no error)");
+ }
+ reject(TransactionAbort);
+ };
+ const th = new TransactionHandle(tx);
+ const resP = Promise.resolve().then(() => f(th));
+ resP
+ .then((result) => {
+ gotFunResult = true;
+ funResult = result;
+ })
+ .catch((e) => {
+ if (e == TransactionAbort) {
+ console.info("aborting transaction");
+ } else {
+ console.error("Transaction failed:", e);
+ console.error(stack);
+ tx.abort();
+ }
+ })
+ .catch((e) => {
+ console.error("fatal: aborting transaction failed", e);
+ });
+ });
+}
+
+/**
+ * Definition of an index.
+ */
+export class Index<S extends idbtypes.IDBValidKey, T> {
+ /**
+ * Name of the store that this index is associated with.
+ */
+ storeName: string;
+
+ /**
+ * Options to use for the index.
+ */
+ options: IndexOptions;
+
+ constructor(
+ s: Store<T>,
+ public indexName: string,
+ public keyPath: string | string[],
+ options?: IndexOptions,
+ ) {
+ const defaultOptions = {
+ multiEntry: false,
+ };
+ this.options = { ...defaultOptions, ...(options || {}) };
+ this.storeName = s.name;
+ }
+
+ /**
+ * We want to have the key type parameter in use somewhere,
+ * because otherwise the compiler complains. In iterIndex the
+ * key type is pretty useful.
+ */
+ protected _dummyKey: S | undefined;
+}
+
+/**
+ * Return a promise that resolves
+ * to the taler wallet db.
+ */
+export function openDatabase(
+ idbFactory: idbtypes.IDBFactory,
+ databaseName: string,
+ databaseVersion: number,
+ onVersionChange: () => void,
+ onUpgradeNeeded: (
+ db: idbtypes.IDBDatabase,
+ oldVersion: number,
+ newVersion: number,
+ ) => void,
+): Promise<idbtypes.IDBDatabase> {
+ return new Promise<idbtypes.IDBDatabase>((resolve, reject) => {
+ const req = idbFactory.open(databaseName, databaseVersion);
+ req.onerror = (e) => {
+ console.log("taler database error", e);
+ reject(new Error("database error"));
+ };
+ req.onsuccess = (e) => {
+ req.result.onversionchange = (evt: idbtypes.IDBVersionChangeEvent) => {
+ console.log(
+ `handling live db version change from ${evt.oldVersion} to ${evt.newVersion}`,
+ );
+ req.result.close();
+ onVersionChange();
+ };
+ resolve(req.result);
+ };
+ req.onupgradeneeded = (e) => {
+ const db = req.result;
+ const newVersion = e.newVersion;
+ if (!newVersion) {
+ throw Error("upgrade needed, but new version unknown");
+ }
+ onUpgradeNeeded(db, e.oldVersion, newVersion);
+ };
+ });
+}
+
+export class Database {
+ constructor(private db: idbtypes.IDBDatabase) {}
+
+ static deleteDatabase(idbFactory: idbtypes.IDBFactory, dbName: string): void {
+ idbFactory.deleteDatabase(dbName);
+ }
+
+ async exportDatabase(): Promise<any> {
+ const db = this.db;
+ const dump = {
+ name: db.name,
+ stores: {} as { [s: string]: any },
+ version: db.version,
+ };
+
+ return new Promise((resolve, reject) => {
+ const tx = db.transaction(Array.from(db.objectStoreNames));
+ tx.addEventListener("complete", () => {
+ resolve(dump);
+ });
+ // tslint:disable-next-line:prefer-for-of
+ for (let i = 0; i < db.objectStoreNames.length; i++) {
+ const name = db.objectStoreNames[i];
+ const storeDump = {} as { [s: string]: any };
+ dump.stores[name] = storeDump;
+ tx.objectStore(name)
+ .openCursor()
+ .addEventListener("success", (e: idbtypes.Event) => {
+ const cursor = (e.target as any).result;
+ if (cursor) {
+ storeDump[cursor.key] = cursor.value;
+ cursor.continue();
+ }
+ });
+ }
+ });
+ }
+
+ importDatabase(dump: any): Promise<void> {
+ const db = this.db;
+ console.log("importing db", dump);
+ return new Promise<void>((resolve, reject) => {
+ const tx = db.transaction(Array.from(db.objectStoreNames), "readwrite");
+ if (dump.stores) {
+ for (const storeName in dump.stores) {
+ const objects = [];
+ const dumpStore = dump.stores[storeName];
+ for (const key in dumpStore) {
+ objects.push(dumpStore[key]);
+ }
+ console.log(`importing ${objects.length} records into ${storeName}`);
+ const store = tx.objectStore(storeName);
+ for (const obj of objects) {
+ store.put(obj);
+ }
+ }
+ }
+ tx.addEventListener("complete", () => {
+ resolve();
+ });
+ });
+ }
+
+ async get<T>(store: Store<T>, key: any): Promise<T | undefined> {
+ const tx = this.db.transaction([store.name], "readonly");
+ const req = tx.objectStore(store.name).get(key);
+ const v = await requestToPromise(req);
+ await transactionToPromise(tx);
+ return v;
+ }
+
+ async getIndexed<S extends idbtypes.IDBValidKey, T>(
+ index: Index<S, T>,
+ key: any,
+ ): Promise<T | undefined> {
+ const tx = this.db.transaction([index.storeName], "readonly");
+ const req = tx.objectStore(index.storeName).index(index.indexName).get(key);
+ const v = await requestToPromise(req);
+ await transactionToPromise(tx);
+ return v;
+ }
+
+ async put<T>(store: Store<T>, value: T, key?: any): Promise<any> {
+ const tx = this.db.transaction([store.name], "readwrite");
+ const req = tx.objectStore(store.name).put(value, key);
+ const v = await requestToPromise(req);
+ await transactionToPromise(tx);
+ return v;
+ }
+
+ async mutate<T>(
+ store: Store<T>,
+ key: any,
+ f: (x: T) => T | undefined,
+ ): Promise<void> {
+ const tx = this.db.transaction([store.name], "readwrite");
+ const req = tx.objectStore(store.name).openCursor(key);
+ await applyMutation(req, f);
+ await transactionToPromise(tx);
+ }
+
+ iter<T>(store: Store<T>): ResultStream<T> {
+ const tx = this.db.transaction([store.name], "readonly");
+ const req = tx.objectStore(store.name).openCursor();
+ return new ResultStream<T>(req);
+ }
+
+ iterIndex<S extends idbtypes.IDBValidKey, T>(
+ index: Index<S, T>,
+ query?: any,
+ ): ResultStream<T> {
+ const tx = this.db.transaction([index.storeName], "readonly");
+ const req = tx
+ .objectStore(index.storeName)
+ .index(index.indexName)
+ .openCursor(query);
+ return new ResultStream<T>(req);
+ }
+
+ async runWithReadTransaction<T>(
+ stores: Store<any>[],
+ f: (t: TransactionHandle) => Promise<T>,
+ ): Promise<T> {
+ return runWithTransaction<T>(this.db, stores, f, "readonly");
+ }
+
+ async runWithWriteTransaction<T>(
+ stores: Store<any>[],
+ f: (t: TransactionHandle) => Promise<T>,
+ ): Promise<T> {
+ return runWithTransaction<T>(this.db, stores, f, "readwrite");
+ }
+}
diff --git a/packages/taler-wallet-core/src/util/reserveHistoryUtil-test.ts b/packages/taler-wallet-core/src/util/reserveHistoryUtil-test.ts
new file mode 100644
index 000000000..79022de77
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/reserveHistoryUtil-test.ts
@@ -0,0 +1,285 @@
+/*
+ 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 {
+ reconcileReserveHistory,
+ summarizeReserveHistory,
+} from "./reserveHistoryUtil";
+import {
+ WalletReserveHistoryItem,
+ WalletReserveHistoryItemType,
+} from "../types/dbTypes";
+import {
+ ReserveTransaction,
+ ReserveTransactionType,
+} from "../types/ReserveTransaction";
+import { Amounts } from "./amounts";
+
+test("basics", (t) => {
+ const r = reconcileReserveHistory([], []);
+ t.deepEqual(r.updatedLocalHistory, []);
+});
+
+test("unmatched credit", (t) => {
+ const localHistory: WalletReserveHistoryItem[] = [];
+ const remoteHistory: ReserveTransaction[] = [
+ {
+ type: ReserveTransactionType.Credit,
+ amount: "TESTKUDOS:100",
+ sender_account_url: "payto://void/",
+ timestamp: { t_ms: 42 },
+ wire_reference: "ABC01",
+ },
+ ];
+ const r = reconcileReserveHistory(localHistory, remoteHistory);
+ const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS");
+ t.deepEqual(r.updatedLocalHistory.length, 1);
+ t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:100");
+ t.deepEqual(Amounts.stringify(s.awaitedReserveAmount), "TESTKUDOS:0");
+ t.deepEqual(Amounts.stringify(s.unclaimedReserveAmount), "TESTKUDOS:100");
+});
+
+test("unmatched credit #2", (t) => {
+ const localHistory: WalletReserveHistoryItem[] = [];
+ const remoteHistory: ReserveTransaction[] = [
+ {
+ type: ReserveTransactionType.Credit,
+ amount: "TESTKUDOS:100",
+ sender_account_url: "payto://void/",
+ timestamp: { t_ms: 42 },
+ wire_reference: "ABC01",
+ },
+ {
+ type: ReserveTransactionType.Credit,
+ amount: "TESTKUDOS:50",
+ sender_account_url: "payto://void/",
+ timestamp: { t_ms: 42 },
+ wire_reference: "ABC02",
+ },
+ ];
+ const r = reconcileReserveHistory(localHistory, remoteHistory);
+ const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS");
+ t.deepEqual(r.updatedLocalHistory.length, 2);
+ t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:150");
+ t.deepEqual(Amounts.stringify(s.awaitedReserveAmount), "TESTKUDOS:0");
+ t.deepEqual(Amounts.stringify(s.unclaimedReserveAmount), "TESTKUDOS:150");
+});
+
+test("matched credit", (t) => {
+ const localHistory: WalletReserveHistoryItem[] = [
+ {
+ type: WalletReserveHistoryItemType.Credit,
+ expectedAmount: Amounts.parseOrThrow("TESTKUDOS:100"),
+ matchedExchangeTransaction: {
+ type: ReserveTransactionType.Credit,
+ amount: "TESTKUDOS:100",
+ sender_account_url: "payto://void/",
+ timestamp: { t_ms: 42 },
+ wire_reference: "ABC01",
+ },
+ },
+ ];
+ const remoteHistory: ReserveTransaction[] = [
+ {
+ type: ReserveTransactionType.Credit,
+ amount: "TESTKUDOS:100",
+ sender_account_url: "payto://void/",
+ timestamp: { t_ms: 42 },
+ wire_reference: "ABC01",
+ },
+ {
+ type: ReserveTransactionType.Credit,
+ amount: "TESTKUDOS:50",
+ sender_account_url: "payto://void/",
+ timestamp: { t_ms: 42 },
+ wire_reference: "ABC02",
+ },
+ ];
+ const r = reconcileReserveHistory(localHistory, remoteHistory);
+ const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS");
+ t.deepEqual(r.updatedLocalHistory.length, 2);
+ t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:150");
+ t.deepEqual(Amounts.stringify(s.awaitedReserveAmount), "TESTKUDOS:0");
+ t.deepEqual(Amounts.stringify(s.unclaimedReserveAmount), "TESTKUDOS:150");
+});
+
+test("fulfilling credit", (t) => {
+ const localHistory: WalletReserveHistoryItem[] = [
+ {
+ type: WalletReserveHistoryItemType.Credit,
+ expectedAmount: Amounts.parseOrThrow("TESTKUDOS:100"),
+ },
+ ];
+ const remoteHistory: ReserveTransaction[] = [
+ {
+ type: ReserveTransactionType.Credit,
+ amount: "TESTKUDOS:100",
+ sender_account_url: "payto://void/",
+ timestamp: { t_ms: 42 },
+ wire_reference: "ABC01",
+ },
+ {
+ type: ReserveTransactionType.Credit,
+ amount: "TESTKUDOS:50",
+ sender_account_url: "payto://void/",
+ timestamp: { t_ms: 42 },
+ wire_reference: "ABC02",
+ },
+ ];
+ const r = reconcileReserveHistory(localHistory, remoteHistory);
+ const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS");
+ t.deepEqual(r.updatedLocalHistory.length, 2);
+ t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:150");
+});
+
+test("unfulfilled credit", (t) => {
+ const localHistory: WalletReserveHistoryItem[] = [
+ {
+ type: WalletReserveHistoryItemType.Credit,
+ expectedAmount: Amounts.parseOrThrow("TESTKUDOS:100"),
+ },
+ ];
+ const remoteHistory: ReserveTransaction[] = [
+ {
+ type: ReserveTransactionType.Credit,
+ amount: "TESTKUDOS:100",
+ sender_account_url: "payto://void/",
+ timestamp: { t_ms: 42 },
+ wire_reference: "ABC01",
+ },
+ {
+ type: ReserveTransactionType.Credit,
+ amount: "TESTKUDOS:50",
+ sender_account_url: "payto://void/",
+ timestamp: { t_ms: 42 },
+ wire_reference: "ABC02",
+ },
+ ];
+ const r = reconcileReserveHistory(localHistory, remoteHistory);
+ const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS");
+ t.deepEqual(r.updatedLocalHistory.length, 2);
+ t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:150");
+});
+
+test("awaited credit", (t) => {
+ const localHistory: WalletReserveHistoryItem[] = [
+ {
+ type: WalletReserveHistoryItemType.Credit,
+ expectedAmount: Amounts.parseOrThrow("TESTKUDOS:50"),
+ },
+ {
+ type: WalletReserveHistoryItemType.Credit,
+ expectedAmount: Amounts.parseOrThrow("TESTKUDOS:100"),
+ },
+ ];
+ const remoteHistory: ReserveTransaction[] = [
+ {
+ type: ReserveTransactionType.Credit,
+ amount: "TESTKUDOS:100",
+ sender_account_url: "payto://void/",
+ timestamp: { t_ms: 42 },
+ wire_reference: "ABC01",
+ },
+ ];
+ const r = reconcileReserveHistory(localHistory, remoteHistory);
+ const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS");
+ t.deepEqual(r.updatedLocalHistory.length, 2);
+ t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:100");
+ t.deepEqual(Amounts.stringify(s.awaitedReserveAmount), "TESTKUDOS:50");
+ t.deepEqual(Amounts.stringify(s.unclaimedReserveAmount), "TESTKUDOS:100");
+});
+
+test("withdrawal new match", (t) => {
+ const localHistory: WalletReserveHistoryItem[] = [
+ {
+ type: WalletReserveHistoryItemType.Credit,
+ expectedAmount: Amounts.parseOrThrow("TESTKUDOS:100"),
+ matchedExchangeTransaction: {
+ type: ReserveTransactionType.Credit,
+ amount: "TESTKUDOS:100",
+ sender_account_url: "payto://void/",
+ timestamp: { t_ms: 42 },
+ wire_reference: "ABC01",
+ },
+ },
+ {
+ type: WalletReserveHistoryItemType.Withdraw,
+ expectedAmount: Amounts.parseOrThrow("TESTKUDOS:5"),
+ },
+ ];
+ const remoteHistory: ReserveTransaction[] = [
+ {
+ type: ReserveTransactionType.Credit,
+ amount: "TESTKUDOS:100",
+ sender_account_url: "payto://void/",
+ timestamp: { t_ms: 42 },
+ wire_reference: "ABC01",
+ },
+ {
+ type: ReserveTransactionType.Withdraw,
+ amount: "TESTKUDOS:5",
+ h_coin_envelope: "foobar",
+ h_denom_pub: "foobar",
+ reserve_sig: "foobar",
+ withdraw_fee: "TESTKUDOS:0.1",
+ },
+ ];
+ const r = reconcileReserveHistory(localHistory, remoteHistory);
+ const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS");
+ t.deepEqual(r.updatedLocalHistory.length, 2);
+ t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:95");
+ t.deepEqual(Amounts.stringify(s.awaitedReserveAmount), "TESTKUDOS:0");
+ t.deepEqual(Amounts.stringify(s.unclaimedReserveAmount), "TESTKUDOS:95");
+});
+
+test("claimed but now arrived", (t) => {
+ const localHistory: WalletReserveHistoryItem[] = [
+ {
+ type: WalletReserveHistoryItemType.Credit,
+ expectedAmount: Amounts.parseOrThrow("TESTKUDOS:100"),
+ matchedExchangeTransaction: {
+ type: ReserveTransactionType.Credit,
+ amount: "TESTKUDOS:100",
+ sender_account_url: "payto://void/",
+ timestamp: { t_ms: 42 },
+ wire_reference: "ABC01",
+ },
+ },
+ {
+ type: WalletReserveHistoryItemType.Withdraw,
+ expectedAmount: Amounts.parseOrThrow("TESTKUDOS:5"),
+ },
+ ];
+ const remoteHistory: ReserveTransaction[] = [
+ {
+ type: ReserveTransactionType.Credit,
+ amount: "TESTKUDOS:100",
+ sender_account_url: "payto://void/",
+ timestamp: { t_ms: 42 },
+ wire_reference: "ABC01",
+ },
+ ];
+ const r = reconcileReserveHistory(localHistory, remoteHistory);
+ const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS");
+ t.deepEqual(r.updatedLocalHistory.length, 2);
+ t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:100");
+ t.deepEqual(Amounts.stringify(s.awaitedReserveAmount), "TESTKUDOS:0");
+ t.deepEqual(Amounts.stringify(s.unclaimedReserveAmount), "TESTKUDOS:95");
+});
diff --git a/packages/taler-wallet-core/src/util/reserveHistoryUtil.d.ts.map b/packages/taler-wallet-core/src/util/reserveHistoryUtil.d.ts.map
new file mode 100644
index 000000000..aec8f0715
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/reserveHistoryUtil.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"reserveHistoryUtil.d.ts","sourceRoot":"","sources":["reserveHistoryUtil.ts"],"names":[],"mappings":"AAgBA;;GAEG;AACH,OAAO,EACL,wBAAwB,EAEzB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,kBAAkB,EAEnB,MAAM,6BAA6B,CAAC;AACrC,OAAO,KAAK,OAAO,MAAM,iBAAiB,CAAC;AAK3C;;;;GAIG;AAEH,MAAM,WAAW,2BAA2B;IAC1C;;OAEG;IACH,mBAAmB,EAAE,wBAAwB,EAAE,CAAC;IAEhD;;;OAGG;IACH,aAAa,EAAE,wBAAwB,EAAE,CAAC;IAE1C;;;OAGG;IACH,eAAe,EAAE,wBAAwB,EAAE,CAAC;CAC7C;AAED;;;GAGG;AACH,MAAM,WAAW,qBAAqB;IACpC;;;OAGG;IACH,sBAAsB,EAAE,OAAO,CAAC,UAAU,CAAC;IAE3C;;OAEG;IACH,sBAAsB,EAAE,OAAO,CAAC,UAAU,CAAC;IAE3C;;OAEG;IACH,oBAAoB,EAAE,OAAO,CAAC,UAAU,CAAC;IAEzC;;;OAGG;IACH,eAAe,EAAE,OAAO,CAAC,UAAU,CAAC;CACrC;AA6BD;;GAEG;AACH,wBAAgB,yBAAyB,CACvC,EAAE,EAAE,wBAAwB,EAC5B,EAAE,EAAE,kBAAkB,GACrB,OAAO,CAwBT;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CACrC,YAAY,EAAE,wBAAwB,EAAE,EACxC,QAAQ,EAAE,MAAM,GACf,qBAAqB,CAmFvB;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,YAAY,EAAE,wBAAwB,EAAE,EACxC,aAAa,EAAE,kBAAkB,EAAE,GAClC,2BAA2B,CAqH7B"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/util/reserveHistoryUtil.ts b/packages/taler-wallet-core/src/util/reserveHistoryUtil.ts
new file mode 100644
index 000000000..855b71a3d
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/reserveHistoryUtil.ts
@@ -0,0 +1,360 @@
+/*
+ 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 {
+ WalletReserveHistoryItem,
+ WalletReserveHistoryItemType,
+} from "../types/dbTypes";
+import {
+ ReserveTransaction,
+ ReserveTransactionType,
+} from "../types/ReserveTransaction";
+import * as Amounts from "../util/amounts";
+import { timestampCmp } from "./time";
+import { deepCopy } from "./helpers";
+import { AmountJson } from "../util/amounts";
+
+/**
+ * Helpers for dealing with reserve histories.
+ *
+ * @author Florian Dold <dold@taler.net>
+ */
+
+export interface ReserveReconciliationResult {
+ /**
+ * The wallet's local history reconciled with the exchange's reserve history.
+ */
+ updatedLocalHistory: WalletReserveHistoryItem[];
+
+ /**
+ * History items that were newly created, subset of the
+ * updatedLocalHistory items.
+ */
+ newAddedItems: WalletReserveHistoryItem[];
+
+ /**
+ * History items that were newly matched, subset of the
+ * updatedLocalHistory items.
+ */
+ newMatchedItems: WalletReserveHistoryItem[];
+}
+
+/**
+ * Various totals computed from the wallet's view
+ * on the reserve history.
+ */
+export interface ReserveHistorySummary {
+ /**
+ * Balance computed by the wallet, should match the balance
+ * computed by the reserve.
+ */
+ computedReserveBalance: Amounts.AmountJson;
+
+ /**
+ * Reserve balance that is still available for withdrawal.
+ */
+ unclaimedReserveAmount: Amounts.AmountJson;
+
+ /**
+ * Amount that we're still expecting to come into the reserve.
+ */
+ awaitedReserveAmount: Amounts.AmountJson;
+
+ /**
+ * Amount withdrawn from the reserve so far. Only counts
+ * finished withdrawals, not withdrawals in progress.
+ */
+ withdrawnAmount: Amounts.AmountJson;
+}
+
+/**
+ * Check if two reserve history items (exchange's version) match.
+ */
+function isRemoteHistoryMatch(
+ t1: ReserveTransaction,
+ t2: ReserveTransaction,
+): boolean {
+ switch (t1.type) {
+ case ReserveTransactionType.Closing: {
+ return t1.type === t2.type && t1.wtid == t2.wtid;
+ }
+ case ReserveTransactionType.Credit: {
+ return t1.type === t2.type && t1.wire_reference === t2.wire_reference;
+ }
+ case ReserveTransactionType.Recoup: {
+ return (
+ t1.type === t2.type &&
+ t1.coin_pub === t2.coin_pub &&
+ timestampCmp(t1.timestamp, t2.timestamp) === 0
+ );
+ }
+ case ReserveTransactionType.Withdraw: {
+ return t1.type === t2.type && t1.h_coin_envelope === t2.h_coin_envelope;
+ }
+ }
+}
+
+/**
+ * Check a local reserve history item and a remote history item are a match.
+ */
+export function isLocalRemoteHistoryMatch(
+ t1: WalletReserveHistoryItem,
+ t2: ReserveTransaction,
+): boolean {
+ switch (t1.type) {
+ case WalletReserveHistoryItemType.Credit: {
+ return (
+ t2.type === ReserveTransactionType.Credit &&
+ !!t1.expectedAmount &&
+ Amounts.cmp(t1.expectedAmount, Amounts.parseOrThrow(t2.amount)) === 0
+ );
+ }
+ case WalletReserveHistoryItemType.Withdraw:
+ return (
+ t2.type === ReserveTransactionType.Withdraw &&
+ !!t1.expectedAmount &&
+ Amounts.cmp(t1.expectedAmount, Amounts.parseOrThrow(t2.amount)) === 0
+ );
+ case WalletReserveHistoryItemType.Recoup: {
+ return (
+ t2.type === ReserveTransactionType.Recoup &&
+ !!t1.expectedAmount &&
+ Amounts.cmp(t1.expectedAmount, Amounts.parseOrThrow(t2.amount)) === 0
+ );
+ }
+ }
+ return false;
+}
+
+/**
+ * Compute totals for the wallet's view of the reserve history.
+ */
+export function summarizeReserveHistory(
+ localHistory: WalletReserveHistoryItem[],
+ currency: string,
+): ReserveHistorySummary {
+ const posAmounts: AmountJson[] = [];
+ const negAmounts: AmountJson[] = [];
+ const expectedPosAmounts: AmountJson[] = [];
+ const expectedNegAmounts: AmountJson[] = [];
+ const withdrawnAmounts: AmountJson[] = [];
+
+ for (const item of localHistory) {
+ switch (item.type) {
+ case WalletReserveHistoryItemType.Credit:
+ if (item.matchedExchangeTransaction) {
+ posAmounts.push(
+ Amounts.parseOrThrow(item.matchedExchangeTransaction.amount),
+ );
+ } else if (item.expectedAmount) {
+ expectedPosAmounts.push(item.expectedAmount);
+ }
+ break;
+ case WalletReserveHistoryItemType.Recoup:
+ if (item.matchedExchangeTransaction) {
+ if (item.matchedExchangeTransaction) {
+ posAmounts.push(
+ Amounts.parseOrThrow(item.matchedExchangeTransaction.amount),
+ );
+ } else if (item.expectedAmount) {
+ expectedPosAmounts.push(item.expectedAmount);
+ } else {
+ throw Error("invariant failed");
+ }
+ }
+ break;
+ case WalletReserveHistoryItemType.Closing:
+ if (item.matchedExchangeTransaction) {
+ negAmounts.push(
+ Amounts.parseOrThrow(item.matchedExchangeTransaction.amount),
+ );
+ } else {
+ throw Error("invariant failed");
+ }
+ break;
+ case WalletReserveHistoryItemType.Withdraw:
+ if (item.matchedExchangeTransaction) {
+ negAmounts.push(
+ Amounts.parseOrThrow(item.matchedExchangeTransaction.amount),
+ );
+ withdrawnAmounts.push(
+ Amounts.parseOrThrow(item.matchedExchangeTransaction.amount),
+ );
+ } else if (item.expectedAmount) {
+ expectedNegAmounts.push(item.expectedAmount);
+ } else {
+ throw Error("invariant failed");
+ }
+ break;
+ }
+ }
+
+ const z = Amounts.getZero(currency);
+
+ const computedBalance = Amounts.sub(
+ Amounts.add(z, ...posAmounts).amount,
+ ...negAmounts,
+ ).amount;
+
+ const unclaimedReserveAmount = Amounts.sub(
+ Amounts.add(z, ...posAmounts).amount,
+ ...negAmounts,
+ ...expectedNegAmounts,
+ ).amount;
+
+ const awaitedReserveAmount = Amounts.sub(
+ Amounts.add(z, ...expectedPosAmounts).amount,
+ ...expectedNegAmounts,
+ ).amount;
+
+ const withdrawnAmount = Amounts.add(z, ...withdrawnAmounts).amount;
+
+ return {
+ computedReserveBalance: computedBalance,
+ unclaimedReserveAmount: unclaimedReserveAmount,
+ awaitedReserveAmount: awaitedReserveAmount,
+ withdrawnAmount,
+ };
+}
+
+/**
+ * Reconcile the wallet's local model of the reserve history
+ * with the reserve history of the exchange.
+ */
+export function reconcileReserveHistory(
+ localHistory: WalletReserveHistoryItem[],
+ remoteHistory: ReserveTransaction[],
+): ReserveReconciliationResult {
+ const updatedLocalHistory: WalletReserveHistoryItem[] = deepCopy(
+ localHistory,
+ );
+ const newMatchedItems: WalletReserveHistoryItem[] = [];
+ const newAddedItems: WalletReserveHistoryItem[] = [];
+
+ const remoteMatched = remoteHistory.map(() => false);
+ const localMatched = localHistory.map(() => false);
+
+ // Take care of deposits
+
+ // First, see which pairs are already a definite match.
+ for (let remoteIndex = 0; remoteIndex < remoteHistory.length; remoteIndex++) {
+ const rhi = remoteHistory[remoteIndex];
+ for (let localIndex = 0; localIndex < localHistory.length; localIndex++) {
+ if (localMatched[localIndex]) {
+ continue;
+ }
+ const lhi = localHistory[localIndex];
+ if (!lhi.matchedExchangeTransaction) {
+ continue;
+ }
+ if (isRemoteHistoryMatch(rhi, lhi.matchedExchangeTransaction)) {
+ localMatched[localIndex] = true;
+ remoteMatched[remoteIndex] = true;
+ break;
+ }
+ }
+ }
+
+ // Check that all previously matched items are still matched
+ for (let localIndex = 0; localIndex < localHistory.length; localIndex++) {
+ if (localMatched[localIndex]) {
+ continue;
+ }
+ const lhi = localHistory[localIndex];
+ if (lhi.matchedExchangeTransaction) {
+ // Don't use for further matching
+ localMatched[localIndex] = true;
+ // FIXME: emit some error here!
+ throw Error("previously matched reserve history item now unmatched");
+ }
+ }
+
+ // Next, find out if there are any exact new matches between local and remote
+ // history items
+ for (let localIndex = 0; localIndex < localHistory.length; localIndex++) {
+ if (localMatched[localIndex]) {
+ continue;
+ }
+ const lhi = localHistory[localIndex];
+ for (
+ let remoteIndex = 0;
+ remoteIndex < remoteHistory.length;
+ remoteIndex++
+ ) {
+ const rhi = remoteHistory[remoteIndex];
+ if (remoteMatched[remoteIndex]) {
+ continue;
+ }
+ if (isLocalRemoteHistoryMatch(lhi, rhi)) {
+ localMatched[localIndex] = true;
+ remoteMatched[remoteIndex] = true;
+ updatedLocalHistory[localIndex].matchedExchangeTransaction = rhi as any;
+ newMatchedItems.push(lhi);
+ break;
+ }
+ }
+ }
+
+ // Finally we add new history items
+ for (let remoteIndex = 0; remoteIndex < remoteHistory.length; remoteIndex++) {
+ if (remoteMatched[remoteIndex]) {
+ continue;
+ }
+ const rhi = remoteHistory[remoteIndex];
+ let newItem: WalletReserveHistoryItem;
+ switch (rhi.type) {
+ case ReserveTransactionType.Closing: {
+ newItem = {
+ type: WalletReserveHistoryItemType.Closing,
+ matchedExchangeTransaction: rhi,
+ };
+ break;
+ }
+ case ReserveTransactionType.Credit: {
+ newItem = {
+ type: WalletReserveHistoryItemType.Credit,
+ matchedExchangeTransaction: rhi,
+ };
+ break;
+ }
+ case ReserveTransactionType.Recoup: {
+ newItem = {
+ type: WalletReserveHistoryItemType.Recoup,
+ matchedExchangeTransaction: rhi,
+ };
+ break;
+ }
+ case ReserveTransactionType.Withdraw: {
+ newItem = {
+ type: WalletReserveHistoryItemType.Withdraw,
+ matchedExchangeTransaction: rhi,
+ };
+ break;
+ }
+ }
+ updatedLocalHistory.push(newItem);
+ newAddedItems.push(newItem);
+ }
+
+ return {
+ updatedLocalHistory,
+ newAddedItems,
+ newMatchedItems,
+ };
+}
diff --git a/packages/taler-wallet-core/src/util/talerconfig.ts b/packages/taler-wallet-core/src/util/talerconfig.ts
new file mode 100644
index 000000000..ec08c352f
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/talerconfig.ts
@@ -0,0 +1,120 @@
+/*
+ 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";
+
+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);
+ }
+}
+
+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) {
+ console.log("parsing line", JSON.stringify(line));
+ if (reEmptyLine.test(line)) {
+ continue;
+ }
+ if (reComment.test(line)) {
+ continue;
+ }
+ const secMatch = line.match(reSection);
+ if (secMatch) {
+ currentSection = secMatch[1];
+ console.log("setting section to", currentSection);
+ continue;
+ }
+ if (currentSection === undefined) {
+ throw Error("invalid configuration, expected section header");
+ }
+ const paramMatch = line.match(reParam);
+ if (paramMatch) {
+ const optName = paramMatch[1];
+ 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",
+ );
+ }
+
+ console.log("parsed config", JSON.stringify(this.sectionMap, undefined, 2));
+ }
+
+ getString(section: string, option: string): ConfigValue<string> {
+ const val = (this.sectionMap[section] ?? {})[option];
+ return new ConfigValue(section, option, val, (x) => x);
+ }
+
+ getAmount(section: string, option: string): ConfigValue<AmountJson> {
+ const val = (this.sectionMap[section] ?? {})[option];
+ return new ConfigValue(section, option, val, (x) =>
+ Amounts.parseOrThrow(x),
+ );
+ }
+}
diff --git a/packages/taler-wallet-core/src/util/taleruri-test.ts b/packages/taler-wallet-core/src/util/taleruri-test.ts
new file mode 100644
index 000000000..b6c326119
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/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-wallet-core/src/util/taleruri.d.ts.map b/packages/taler-wallet-core/src/util/taleruri.d.ts.map
new file mode 100644
index 000000000..36c16c889
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/taleruri.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"taleruri.d.ts","sourceRoot":"","sources":["taleruri.ts"],"names":[],"mappings":"AAkBA,MAAM,WAAW,YAAY;IAC3B,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;CAChC;AAED,MAAM,WAAW,iBAAiB;IAChC,yBAAyB,EAAE,MAAM,CAAC;IAClC,qBAAqB,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,eAAe;IAC9B,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,MAAM,GAAG,iBAAiB,GAAG,SAAS,CAoBzE;AAED,0BAAkB,YAAY;IAC5B,QAAQ,cAAc;IACtB,aAAa,mBAAmB;IAChC,QAAQ,cAAc;IACtB,WAAW,iBAAiB;IAC5B,kBAAkB,yBAAyB;IAC3C,OAAO,YAAY;CACpB;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,MAAM,GAAG,YAAY,CA2BxD;AA0BD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAyB/D;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAoB/D;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAoBrE"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/util/taleruri.ts b/packages/taler-wallet-core/src/util/taleruri.ts
new file mode 100644
index 000000000..43a869afe
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/taleruri.ts
@@ -0,0 +1,215 @@
+/*
+ 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 { 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: `${pi.innerProto}://${p}/`,
+ withdrawalOperationId: withdrawId,
+ };
+}
+
+export const 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://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 = `${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 = `${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 < 2) {
+ return undefined;
+ }
+ const host = parts[0].toLowerCase();
+ const orderId = parts[parts.length - 1];
+ const pathSegments = parts.slice(1, parts.length - 1);
+ const p = [host, ...pathSegments].join("/");
+ const merchantBaseUrl = `${pi.innerProto}://${p}/`;
+
+ return {
+ merchantBaseUrl,
+ orderId,
+ };
+}
diff --git a/packages/taler-wallet-core/src/util/testvectors.ts b/packages/taler-wallet-core/src/util/testvectors.ts
new file mode 100644
index 000000000..57ac6e992
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/testvectors.ts
@@ -0,0 +1,36 @@
+/*
+ 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 {
+ setupRefreshPlanchet,
+ encodeCrock,
+ getRandomBytes,
+} from "../crypto/talerCrypto";
+
+export function printTestVectors() {
+ const secretSeed = getRandomBytes(64);
+ const coinIndex = Math.ceil(Math.random() * 100);
+ const p = setupRefreshPlanchet(secretSeed, coinIndex);
+ console.log("setupRefreshPlanchet");
+ console.log(` (in) secret seed: ${encodeCrock(secretSeed)}`);
+ console.log(` (in) coin index: ${coinIndex}`);
+ console.log(` (out) blinding secret: ${encodeCrock(p.bks)}`);
+ console.log(` (out) coin priv: ${encodeCrock(p.coinPriv)}`);
+ console.log(` (out) coin pub: ${encodeCrock(p.coinPub)}`);
+}
diff --git a/packages/taler-wallet-core/src/util/time.d.ts.map b/packages/taler-wallet-core/src/util/time.d.ts.map
new file mode 100644
index 000000000..c38a23356
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/time.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"time.d.ts","sourceRoot":"","sources":["time.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAA0B,MAAM,SAAS,CAAC;AAkBxD;;GAEG;AAEH,qBAAa,SAAS;IACpB;;OAEG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;CACjC;AAED,MAAM,WAAW,QAAQ;IACvB;;OAEG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;CACnC;AAID,wBAAgB,sBAAsB,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAEvD;AAED,wBAAgB,eAAe,IAAI,SAAS,CAI3C;AAED,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,SAAS,EACnB,GAAG,YAAoB,GACtB,QAAQ,CAWV;AAED,wBAAgB,YAAY,CAAC,EAAE,EAAE,SAAS,EAAE,EAAE,EAAE,SAAS,GAAG,SAAS,CAQpE;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,EAAE,EAAE,SAAS,GAAG,SAAS,CAOlE;AAED,wBAAgB,WAAW,CAAC,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,QAAQ,GAAG,QAAQ,CAQhE;AAED,wBAAgB,YAAY,CAAC,EAAE,EAAE,SAAS,EAAE,EAAE,EAAE,SAAS,GAAG,MAAM,CAiBjE;AAED,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,SAAS,EAAE,CAAC,EAAE,QAAQ,GAAG,SAAS,CAK1E;AAED,wBAAgB,0BAA0B,CACxC,EAAE,EAAE,SAAS,EACb,CAAC,EAAE,QAAQ,GACV,SAAS,CAQX;AAED,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,SAAS,GAAG,MAAM,CAKvD;AAED,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,SAAS,EAAE,EAAE,EAAE,SAAS,GAAG,QAAQ,CAQ1E;AAED,wBAAgB,kBAAkB,CAChC,CAAC,EAAE,SAAS,EACZ,KAAK,EAAE,SAAS,EAChB,GAAG,EAAE,SAAS,GACb,OAAO,CAQT;AAED,eAAO,MAAM,iBAAiB,EAAE,KAAK,CAAC,SAAS,CAc9C,CAAC;AAEF,eAAO,MAAM,gBAAgB,EAAE,KAAK,CAAC,QAAQ,CAc5C,CAAC"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/util/time.ts b/packages/taler-wallet-core/src/util/time.ts
new file mode 100644
index 000000000..5c2f49d12
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/time.ts
@@ -0,0 +1,198 @@
+import { Codec, renderContext, Context } from "./codec";
+
+/*
+ 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.
+ */
+
+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 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) };
+}
+
+/**
+ * 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 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-wallet-core/src/util/timer.d.ts.map b/packages/taler-wallet-core/src/util/timer.d.ts.map
new file mode 100644
index 000000000..c2b5e536e
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/timer.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"timer.d.ts","sourceRoot":"","sources":["timer.ts"],"names":[],"mappings":"AAgBA;;;;;GAKG;AAEH;;GAEG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAKlC;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,KAAK,IAAI,IAAI,CAAC;CACf;AAkBD;;GAEG;AACH,eAAO,MAAM,cAAc,EAAE,MAAM,MAa/B,CAAC;AAEL;;GAEG;AACH,wBAAgB,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,WAAW,CAExE;AAED;;GAEG;AACH,wBAAgB,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,WAAW,CAExE;AASD;;GAEG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,OAAO,CAAS;IAExB,OAAO,CAAC,QAAQ,CAAwC;IAExD,OAAO,CAAC,KAAK,CAAK;IAElB,0BAA0B,IAAI,IAAI;IAWlC,YAAY,CAAC,OAAO,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAU9C,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,WAAW;IAmBzD,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,WAAW;CAkB1D"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/util/timer.ts b/packages/taler-wallet-core/src/util/timer.ts
new file mode 100644
index 000000000..8eab1399c
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/timer.ts
@@ -0,0 +1,165 @@
+/*
+ This file is part of GNU Taler
+ (C) 2017-2019 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Cross-platform timers.
+ *
+ * NodeJS and the browser use slightly different timer API,
+ * this abstracts over these differences.
+ */
+
+/**
+ * Imports.
+ */
+import { Duration } from "./time";
+import { Logger } from "./logging";
+
+const logger = new Logger("timer.ts");
+
+/**
+ * Cancelable timer.
+ */
+export interface TimerHandle {
+ clear(): void;
+}
+
+class IntervalHandle {
+ constructor(public h: any) {}
+
+ clear(): void {
+ clearInterval(this.h);
+ }
+}
+
+class TimeoutHandle {
+ constructor(public h: any) {}
+
+ clear(): void {
+ clearTimeout(this.h);
+ }
+}
+
+/**
+ * Get a performance counter in milliseconds.
+ */
+export const performanceNow: () => number = (() => {
+ // @ts-ignore
+ if (typeof process !== "undefined" && process.hrtime) {
+ return () => {
+ const t = process.hrtime();
+ return t[0] * 1e9 + t[1];
+ };
+ }
+
+ // @ts-ignore
+ if (typeof performance !== "undefined") {
+ // @ts-ignore
+ return () => performance.now();
+ }
+
+ return () => 0;
+})();
+
+/**
+ * Call a function every time the delay given in milliseconds passes.
+ */
+export function every(delayMs: number, callback: () => void): TimerHandle {
+ return new IntervalHandle(setInterval(callback, delayMs));
+}
+
+/**
+ * Call a function after the delay given in milliseconds passes.
+ */
+export function after(delayMs: number, callback: () => void): TimerHandle {
+ return new TimeoutHandle(setTimeout(callback, delayMs));
+}
+
+const nullTimerHandle = {
+ clear() {
+ // do nothing
+ return;
+ },
+};
+
+/**
+ * Group of timers that can be destroyed at once.
+ */
+export class TimerGroup {
+ private stopped = false;
+
+ private timerMap: { [index: number]: TimerHandle } = {};
+
+ private idGen = 1;
+
+ stopCurrentAndFutureTimers(): void {
+ this.stopped = true;
+ for (const x in this.timerMap) {
+ if (!this.timerMap.hasOwnProperty(x)) {
+ continue;
+ }
+ this.timerMap[x].clear();
+ delete this.timerMap[x];
+ }
+ }
+
+ resolveAfter(delayMs: Duration): Promise<void> {
+ return new Promise<void>((resolve, reject) => {
+ if (delayMs.d_ms !== "forever") {
+ this.after(delayMs.d_ms, () => {
+ resolve();
+ });
+ }
+ });
+ }
+
+ after(delayMs: number, callback: () => void): TimerHandle {
+ if (this.stopped) {
+ logger.warn("dropping timer since timer group is stopped");
+ return nullTimerHandle;
+ }
+ const h = after(delayMs, callback);
+ const myId = this.idGen++;
+ this.timerMap[myId] = h;
+
+ const tm = this.timerMap;
+
+ return {
+ clear() {
+ h.clear();
+ delete tm[myId];
+ },
+ };
+ }
+
+ every(delayMs: number, callback: () => void): TimerHandle {
+ if (this.stopped) {
+ logger.warn("dropping timer since timer group is stopped");
+ return nullTimerHandle;
+ }
+ const h = every(delayMs, callback);
+ const myId = this.idGen++;
+ this.timerMap[myId] = h;
+
+ const tm = this.timerMap;
+
+ return {
+ clear() {
+ h.clear();
+ delete tm[myId];
+ },
+ };
+ }
+}
diff --git a/packages/taler-wallet-core/src/util/url.d.ts.map b/packages/taler-wallet-core/src/util/url.d.ts.map
new file mode 100644
index 000000000..f238a9b5a
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/url.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"url.d.ts","sourceRoot":"","sources":["url.ts"],"names":[],"mappings":"AAgBA,UAAU,GAAG;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,IAAI,MAAM,CAAC;IACnB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,YAAY,EAAE,eAAe,CAAC;IACvC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,IAAI,MAAM,CAAC;CAClB;AAED,UAAU,eAAe;IACvB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1C,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IACjC,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC/B,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAC3B,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACvC,IAAI,IAAI,IAAI,CAAC;IACb,QAAQ,IAAI,MAAM,CAAC;IACnB,OAAO,CACL,UAAU,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,KAAK,IAAI,EACzE,OAAO,CAAC,EAAE,GAAG,GACZ,IAAI,CAAC;CACT;AAED,MAAM,WAAW,mBAAmB;IAClC,KAAI,IAAI,CAAC,EAAE,MAAM,EAAE,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,GAAG,eAAe,GAAG,eAAe,CAAC;CAC7F;AAED,MAAM,WAAW,OAAO;IACtB,KAAI,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,GAAG,GAAG,GAAG,CAAC;CAC5C;AAQD,eAAO,MAAM,GAAG,EAAE,OAAc,CAAC;AASjC,eAAO,MAAM,eAAe,EAAE,mBAAsC,CAAC"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/util/url.ts b/packages/taler-wallet-core/src/util/url.ts
new file mode 100644
index 000000000..b50b4b466
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/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-wallet-core/src/util/wire.ts b/packages/taler-wallet-core/src/util/wire.ts
new file mode 100644
index 000000000..95e324f3c
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/wire.ts
@@ -0,0 +1,51 @@
+/*
+ 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/>
+ */
+
+/**
+ * Display and manipulate wire information.
+ *
+ * Right now, all types are hard-coded. In the future, there might be plugins / configurable
+ * methods or support for the "payto://" URI scheme.
+ */
+
+/**
+ * Imports.
+ */
+import * as i18n from "../i18n";
+
+/**
+ * Short summary of the wire information.
+ *
+ * Might abbreviate and return the same summary for different
+ * wire details.
+ */
+export function summarizeWire(w: any): string {
+ if (!w.type) {
+ return i18n.str`Invalid Wire`;
+ }
+ switch (w.type.toLowerCase()) {
+ case "test":
+ if (!w.account_number && w.account_number !== 0) {
+ return i18n.str`Invalid Test Wire Detail`;
+ }
+ if (!w.bank_uri) {
+ return i18n.str`Invalid Test Wire Detail`;
+ }
+ return i18n.str`Test Wire Acct #${w.account_number} on ${w.bank_uri}`;
+ default:
+ return i18n.str`Unknown Wire Detail`;
+ }
+}