aboutsummaryrefslogtreecommitdiff
path: root/src/util
diff options
context:
space:
mode:
Diffstat (limited to 'src/util')
-rw-r--r--src/util/amounts.ts340
-rw-r--r--src/util/assertUnreachable.ts19
-rw-r--r--src/util/asyncMemo.ts52
-rw-r--r--src/util/checkable.ts417
-rw-r--r--src/util/helpers-test.ts38
-rw-r--r--src/util/helpers.ts204
-rw-r--r--src/util/http.ts109
-rw-r--r--src/util/libtoolVersion-test.ts30
-rw-r--r--src/util/libtoolVersion.ts86
-rw-r--r--src/util/logging.ts25
-rw-r--r--src/util/payto-test.ts31
-rw-r--r--src/util/payto.ts54
-rw-r--r--src/util/promiseUtils.ts39
-rw-r--r--src/util/query.ts446
-rw-r--r--src/util/taleruri-test.ts230
-rw-r--r--src/util/taleruri.ts200
-rw-r--r--src/util/timer.ts145
-rw-r--r--src/util/wire.ts53
18 files changed, 2518 insertions, 0 deletions
diff --git a/src/util/amounts.ts b/src/util/amounts.ts
new file mode 100644
index 000000000..b90d54a31
--- /dev/null
+++ b/src/util/amounts.ts
@@ -0,0 +1,340 @@
+/*
+ This file is part of TALER
+ (C) 2018 GNUnet e.V. and INRIA
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+
+/**
+ * Types and helper functions for dealing with Taler amounts.
+ */
+
+
+/**
+ * Imports.
+ */
+import { Checkable } from "./checkable";
+
+
+/**
+ * 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.
+ */
+@Checkable.Class()
+export class AmountJson {
+ /**
+ * Value, must be an integer.
+ */
+ @Checkable.Number()
+ readonly value: number;
+
+ /**
+ * Fraction, must be an integer. Represent 1/1e8 of a unit.
+ */
+ @Checkable.Number()
+ readonly fraction: number;
+
+ /**
+ * Currency of the amount.
+ */
+ @Checkable.String()
+ readonly currency: string;
+
+ /**
+ * Verify that a value matches the schema of this class and convert it into a
+ * member.
+ */
+ static checked: (obj: any) => 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[]) {
+ 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): number {
+ 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;
+}
+
+
+/**
+ * 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;
+ }
+ let 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) {
+ 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 toString(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.
+ */
+export function check(a: any): boolean {
+ if (typeof a !== "string") {
+ return false;
+ }
+ try {
+ const parsedAmount = parse(a);
+ return !!parsedAmount;
+ } catch {
+ return false;
+ }
+}
diff --git a/src/util/assertUnreachable.ts b/src/util/assertUnreachable.ts
new file mode 100644
index 000000000..90f2476b4
--- /dev/null
+++ b/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");
+} \ No newline at end of file
diff --git a/src/util/asyncMemo.ts b/src/util/asyncMemo.ts
new file mode 100644
index 000000000..8b7b1c9bb
--- /dev/null
+++ b/src/util/asyncMemo.ts
@@ -0,0 +1,52 @@
+/*
+ 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 interface MemoEntry<T> {
+ p: Promise<T>;
+ t: number;
+ n: number;
+}
+
+export class AsyncOpMemo<T> {
+ n = 0;
+ memo: { [k: string]: MemoEntry<T> } = {};
+ put(key: string, p: Promise<T>): Promise<T> {
+ const n = this.n++;
+ this.memo[key] = {
+ p,
+ n,
+ t: new Date().getTime(),
+ };
+ p.finally(() => {
+ const r = this.memo[key];
+ if (r && r.n === n) {
+ delete this.memo[key];
+ }
+ });
+ return p;
+ }
+ find(key: string): Promise<T> | undefined {
+ const res = this.memo[key];
+ const tNow = new Date().getTime();
+ if (res && res.t < tNow - 10 * 1000) {
+ delete this.memo[key];
+ return;
+ } else if (res) {
+ return res.p;
+ }
+ return;
+ }
+} \ No newline at end of file
diff --git a/src/util/checkable.ts b/src/util/checkable.ts
new file mode 100644
index 000000000..3c9fe5bc1
--- /dev/null
+++ b/src/util/checkable.ts
@@ -0,0 +1,417 @@
+/*
+ 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/>
+ */
+
+
+/**
+ * Decorators for validating JSON objects and converting them to a typed
+ * object.
+ *
+ * The decorators are put onto classes, and the validation is done
+ * via a static method that is filled in by the annotation.
+ *
+ * Example:
+ * ```
+ * @Checkable.Class
+ * class Person {
+ * @Checkable.String
+ * name: string;
+ * @Checkable.Number
+ * age: number;
+ *
+ * // Method will be implemented automatically
+ * static checked(obj: any): Person;
+ * }
+ * ```
+ */
+export namespace Checkable {
+
+ type Path = Array<number | string>;
+
+ interface SchemaErrorConstructor {
+ new (err: string): SchemaError;
+ }
+
+ interface SchemaError {
+ name: string;
+ message: string;
+ }
+
+ interface Prop {
+ propertyKey: any;
+ checker: any;
+ type?: any;
+ typeThunk?: () => any;
+ elementChecker?: any;
+ elementProp?: any;
+ keyProp?: any;
+ stringChecker?: (s: string) => boolean;
+ valueProp?: any;
+ optional?: boolean;
+ }
+
+ interface CheckableInfo {
+ extraAllowed: boolean;
+ props: Prop[];
+ }
+
+ // tslint:disable-next-line:no-shadowed-variable
+ export const SchemaError = (function SchemaError(this: any, message: string) {
+ const that: any = this as any;
+ that.name = "SchemaError";
+ that.message = message;
+ that.stack = (new Error() as any).stack;
+ }) as any as SchemaErrorConstructor;
+
+
+ SchemaError.prototype = new Error();
+
+ /**
+ * Classes that are checkable are annotated with this
+ * checkable info symbol, which contains the information necessary
+ * to check if they're valid.
+ */
+ const checkableInfoSym = Symbol("checkableInfo");
+
+ /**
+ * Get the current property list for a checkable type.
+ */
+ function getCheckableInfo(target: any): CheckableInfo {
+ let chk = target[checkableInfoSym] as CheckableInfo|undefined;
+ if (!chk) {
+ chk = { props: [], extraAllowed: false };
+ target[checkableInfoSym] = chk;
+ }
+ return chk;
+ }
+
+
+ function checkNumber(target: any, prop: Prop, path: Path): any {
+ if ((typeof target) !== "number") {
+ throw new SchemaError(`expected number for ${path}`);
+ }
+ return target;
+ }
+
+
+ function checkString(target: any, prop: Prop, path: Path): any {
+ if (typeof target !== "string") {
+ throw new SchemaError(`expected string for ${path}, got ${typeof target} instead`);
+ }
+ if (prop.stringChecker && !prop.stringChecker(target)) {
+ throw new SchemaError(`string property ${path} malformed`);
+ }
+ return target;
+ }
+
+ function checkBoolean(target: any, prop: Prop, path: Path): any {
+ if (typeof target !== "boolean") {
+ throw new SchemaError(`expected boolean for ${path}, got ${typeof target} instead`);
+ }
+ return target;
+ }
+
+
+ function checkAnyObject(target: any, prop: Prop, path: Path): any {
+ if (typeof target !== "object") {
+ throw new SchemaError(`expected (any) object for ${path}, got ${typeof target} instead`);
+ }
+ return target;
+ }
+
+
+ function checkAny(target: any, prop: Prop, path: Path): any {
+ return target;
+ }
+
+
+ function checkList(target: any, prop: Prop, path: Path): any {
+ if (!Array.isArray(target)) {
+ throw new SchemaError(`array expected for ${path}, got ${typeof target} instead`);
+ }
+ for (let i = 0; i < target.length; i++) {
+ const v = target[i];
+ prop.elementChecker(v, prop.elementProp, path.concat([i]));
+ }
+ return target;
+ }
+
+ function checkMap(target: any, prop: Prop, path: Path): any {
+ if (typeof target !== "object") {
+ throw new SchemaError(`expected object for ${path}, got ${typeof target} instead`);
+ }
+ for (const key in target) {
+ prop.keyProp.checker(key, prop.keyProp, path.concat([key]));
+ const value = target[key];
+ prop.valueProp.checker(value, prop.valueProp, path.concat([key]));
+ }
+ return target;
+ }
+
+
+ function checkOptional(target: any, prop: Prop, path: Path): any {
+ console.assert(prop.propertyKey);
+ prop.elementChecker(target,
+ prop.elementProp,
+ path.concat([prop.propertyKey]));
+ return target;
+ }
+
+
+ function checkValue(target: any, prop: Prop, path: Path): any {
+ let type;
+ if (prop.type) {
+ type = prop.type;
+ } else if (prop.typeThunk) {
+ type = prop.typeThunk();
+ if (!type) {
+ throw Error(`assertion failed: typeThunk returned null (prop is ${JSON.stringify(prop)})`);
+ }
+ } else {
+ throw Error(`assertion failed: type/typeThunk missing (prop is ${JSON.stringify(prop)})`);
+ }
+ const typeName = type.name || "??";
+ const v = target;
+ if (!v || typeof v !== "object") {
+ throw new SchemaError(
+ `expected object for ${path.join(".")}, got ${typeof v} instead`);
+ }
+ const chk = type.prototype[checkableInfoSym];
+ const props = chk.props;
+ const remainingPropNames = new Set(Object.getOwnPropertyNames(v));
+ const obj = new type();
+ for (const innerProp of props) {
+ if (!remainingPropNames.has(innerProp.propertyKey)) {
+ if (innerProp.optional) {
+ continue;
+ }
+ throw new SchemaError(`Property '${innerProp.propertyKey}' missing on '${path}' of '${typeName}'`);
+ }
+ if (!remainingPropNames.delete(innerProp.propertyKey)) {
+ throw new SchemaError("assertion failed");
+ }
+ const propVal = v[innerProp.propertyKey];
+ obj[innerProp.propertyKey] = innerProp.checker(propVal,
+ innerProp,
+ path.concat([innerProp.propertyKey]));
+ }
+
+ if (!chk.extraAllowed && remainingPropNames.size !== 0) {
+ const err = `superfluous properties ${JSON.stringify(Array.from(remainingPropNames.values()))} of ${typeName}`;
+ throw new SchemaError(err);
+ }
+ return obj;
+ }
+
+
+ /**
+ * Class with checkable annotations on fields.
+ * This annotation adds the implementation of the `checked`
+ * static method.
+ */
+ export function Class(opts: {extra?: boolean, validate?: boolean} = {}) {
+ return (target: any) => {
+ const chk = getCheckableInfo(target.prototype);
+ chk.extraAllowed = !!opts.extra;
+ target.checked = (v: any) => {
+ const cv = checkValue(v, {
+ checker: checkValue,
+ propertyKey: "(root)",
+ type: target,
+ }, ["(root)"]);
+ if (opts.validate) {
+ if (typeof target.validate !== "function") {
+ throw Error("invalid Checkable annotion: validate method required");
+ }
+ // May throw exception
+ target.validate(cv);
+ }
+ return cv;
+ };
+ return target;
+ };
+ }
+
+
+ /**
+ * Target property must be a Checkable object of the given type.
+ */
+ export function Value(typeThunk: () => any) {
+ function deco(target: object, propertyKey: string | symbol): void {
+ const chk = getCheckableInfo(target);
+ chk.props.push({
+ checker: checkValue,
+ propertyKey,
+ typeThunk,
+ });
+ }
+
+ return deco;
+ }
+
+
+ /**
+ * List of values that match the given annotation. For example, `@Checkable.List(Checkable.String)` is
+ * an annotation for a list of strings.
+ */
+ export function List(type: any) {
+ const stub = {};
+ type(stub, "(list-element)");
+ const elementProp = getCheckableInfo(stub).props[0];
+ const elementChecker = elementProp.checker;
+ if (!elementChecker) {
+ throw Error("assertion failed");
+ }
+ function deco(target: object, propertyKey: string | symbol): void {
+ const chk = getCheckableInfo(target);
+ chk.props.push({
+ checker: checkList,
+ elementChecker,
+ elementProp,
+ propertyKey,
+ });
+ }
+
+ return deco;
+ }
+
+
+ /**
+ * Map from the key type to value type. Takes two annotations,
+ * one for the key type and one for the value type.
+ */
+ export function Map(keyType: any, valueType: any) {
+ const keyStub = {};
+ keyType(keyStub, "(map-key)");
+ const keyProp = getCheckableInfo(keyStub).props[0];
+ if (!keyProp) {
+ throw Error("assertion failed");
+ }
+ const valueStub = {};
+ valueType(valueStub, "(map-value)");
+ const valueProp = getCheckableInfo(valueStub).props[0];
+ if (!valueProp) {
+ throw Error("assertion failed");
+ }
+ function deco(target: object, propertyKey: string | symbol): void {
+ const chk = getCheckableInfo(target);
+ chk.props.push({
+ checker: checkMap,
+ keyProp,
+ propertyKey,
+ valueProp,
+ });
+ }
+
+ return deco;
+ }
+
+
+ /**
+ * Makes another annotation optional, for example `@Checkable.Optional(Checkable.Number)`.
+ */
+ export function Optional(type: (target: object, propertyKey: string | symbol) => void | any) {
+ const stub = {};
+ type(stub, "(optional-element)");
+ const elementProp = getCheckableInfo(stub).props[0];
+ const elementChecker = elementProp.checker;
+ if (!elementChecker) {
+ throw Error("assertion failed");
+ }
+ function deco(target: object, propertyKey: string | symbol): void {
+ const chk = getCheckableInfo(target);
+ chk.props.push({
+ checker: checkOptional,
+ elementChecker,
+ elementProp,
+ optional: true,
+ propertyKey,
+ });
+ }
+
+ return deco;
+ }
+
+
+ /**
+ * Target property must be a number.
+ */
+ export function Number(): (target: object, propertyKey: string | symbol) => void {
+ const deco = (target: object, propertyKey: string | symbol) => {
+ const chk = getCheckableInfo(target);
+ chk.props.push({checker: checkNumber, propertyKey});
+ };
+ return deco;
+ }
+
+
+ /**
+ * Target property must be an arbitary object.
+ */
+ export function AnyObject(): (target: object, propertyKey: string | symbol) => void {
+ const deco = (target: object, propertyKey: string | symbol) => {
+ const chk = getCheckableInfo(target);
+ chk.props.push({
+ checker: checkAnyObject,
+ propertyKey,
+ });
+ };
+ return deco;
+ }
+
+
+ /**
+ * Target property can be anything.
+ *
+ * Not useful by itself, but in combination with higher-order annotations
+ * such as List or Map.
+ */
+ export function Any(): (target: object, propertyKey: string | symbol) => void {
+ const deco = (target: object, propertyKey: string | symbol) => {
+ const chk = getCheckableInfo(target);
+ chk.props.push({
+ checker: checkAny,
+ optional: true,
+ propertyKey,
+ });
+ };
+ return deco;
+ }
+
+
+ /**
+ * Target property must be a string.
+ */
+ export function String(
+ stringChecker?: (s: string) => boolean): (target: object, propertyKey: string | symbol,
+ ) => void {
+ const deco = (target: object, propertyKey: string | symbol) => {
+ const chk = getCheckableInfo(target);
+ chk.props.push({ checker: checkString, propertyKey, stringChecker });
+ };
+ return deco;
+ }
+
+ /**
+ * Target property must be a boolean value.
+ */
+ export function Boolean(): (target: object, propertyKey: string | symbol) => void {
+ const deco = (target: object, propertyKey: string | symbol) => {
+ const chk = getCheckableInfo(target);
+ chk.props.push({ checker: checkBoolean, propertyKey });
+ };
+ return deco;
+ }
+}
diff --git a/src/util/helpers-test.ts b/src/util/helpers-test.ts
new file mode 100644
index 000000000..74817120a
--- /dev/null
+++ b/src/util/helpers-test.ts
@@ -0,0 +1,38 @@
+/*
+ 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"));
+ t.pass();
+});
diff --git a/src/util/helpers.ts b/src/util/helpers.ts
new file mode 100644
index 000000000..eb8a1c7b2
--- /dev/null
+++ b/src/util/helpers.ts
@@ -0,0 +1,204 @@
+/*
+ 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 { Timestamp } from "../walletTypes";
+
+/**
+ * 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) {
+ 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]));
+}
+
+
+/**
+ * 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], []);
+}
+
+
+/**
+ * Extract a numeric timstamp (in seconds) from the Taler date format
+ * ("/Date([n])/"). Returns null if input is not in the right format.
+ */
+export function getTalerStampSec(stamp: string): number | null {
+ const m = stamp.match(/\/?Date\(([0-9]*)\)\/?/);
+ if (!m || !m[1]) {
+ return null;
+ }
+ return parseInt(m[1], 10);
+}
+
+/**
+ * Extract a timestamp from a Taler timestamp string.
+ */
+export function extractTalerStamp(stamp: string): Timestamp | undefined {
+ const m = stamp.match(/\/?Date\(([0-9]*)\)\/?/);
+ if (!m || !m[1]) {
+ return undefined;
+ }
+ return {
+ t_ms: parseInt(m[1], 10) * 1000,
+ };
+}
+
+/**
+ * Extract a timestamp from a Taler timestamp string.
+ */
+export function extractTalerStampOrThrow(stamp: string): Timestamp {
+ const r = extractTalerStamp(stamp);
+ if (!r) {
+ throw Error("invalid time stamp");
+ }
+ return r;
+}
+
+/**
+ * Check if a timestamp is in the right format.
+ */
+export function timestampCheck(stamp: string): boolean {
+ return getTalerStampSec(stamp) !== null;
+}
+
+
+/**
+ * Get a JavaScript Date object from a Taler date string.
+ * Returns null if input is not in the right format.
+ */
+export function getTalerStampDate(stamp: string): Date | null {
+ const sec = getTalerStampSec(stamp);
+ if (sec == null) {
+ return null;
+ }
+ return new Date(sec * 1000);
+}
+
+/**
+ * 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/src/util/http.ts b/src/util/http.ts
new file mode 100644
index 000000000..a2bfab279
--- /dev/null
+++ b/src/util/http.ts
@@ -0,0 +1,109 @@
+/*
+ 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.
+ */
+
+/**
+ * An HTTP response that is returned by all request methods of this library.
+ */
+export interface HttpResponse {
+ status: number;
+ responseJson: object & any;
+}
+
+/**
+ * The request library is bundled into an interface to make mocking easy.
+ */
+export interface HttpRequestLibrary {
+ get(url: string): Promise<HttpResponse>;
+
+ postJson(url: string, body: any): Promise<HttpResponse>;
+}
+
+/**
+ * An implementation of the [[HttpRequestLibrary]] using the
+ * browser's XMLHttpRequest.
+ */
+export class BrowserHttpLib implements HttpRequestLibrary {
+ private req(
+ method: string,
+ url: string,
+ options?: any,
+ ): Promise<HttpResponse> {
+ return new Promise<HttpResponse>((resolve, reject) => {
+ const myRequest = new XMLHttpRequest();
+ myRequest.open(method, url);
+ if (options && options.req) {
+ myRequest.send(options.req);
+ } else {
+ myRequest.send();
+ }
+
+ myRequest.onerror = e => {
+ console.error("http request error");
+ reject(Error("could not make XMLHttpRequest"));
+ };
+
+ myRequest.addEventListener("readystatechange", e => {
+ if (myRequest.readyState === XMLHttpRequest.DONE) {
+ if (myRequest.status === 0) {
+ reject(Error("HTTP Request failed (status code 0, maybe URI scheme is wrong?)"))
+ return;
+ }
+ if (myRequest.status != 200) {
+ reject(
+ Error(
+ `HTTP Response with unexpected status code ${myRequest.status}: ${myRequest.statusText}`,
+ ),
+ );
+ return;
+ }
+ let responseJson;
+ try {
+ responseJson = JSON.parse(myRequest.responseText);
+ } catch (e) {
+ reject(Error("Invalid JSON from HTTP response"));
+ return;
+ }
+ if (responseJson === null || typeof responseJson !== "object") {
+ reject(Error("Invalid JSON from HTTP response"));
+ return;
+ }
+ const resp = {
+ responseJson: responseJson,
+ status: myRequest.status,
+ };
+ resolve(resp);
+ }
+ });
+ });
+ }
+
+ get(url: string) {
+ return this.req("get", url);
+ }
+
+ postJson(url: string, body: any) {
+ return this.req("post", url, { req: JSON.stringify(body) });
+ }
+
+ postForm(url: string, form: any) {
+ return this.req("post", url, { req: form });
+ }
+}
diff --git a/src/util/libtoolVersion-test.ts b/src/util/libtoolVersion-test.ts
new file mode 100644
index 000000000..0a610e455
--- /dev/null
+++ b/src/util/libtoolVersion-test.ts
@@ -0,0 +1,30 @@
+/*
+ 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/src/util/libtoolVersion.ts b/src/util/libtoolVersion.ts
new file mode 100644
index 000000000..cc2435b94
--- /dev/null
+++ b/src/util/libtoolVersion.ts
@@ -0,0 +1,86 @@
+/*
+ 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/src/util/logging.ts b/src/util/logging.ts
new file mode 100644
index 000000000..309d1593b
--- /dev/null
+++ b/src/util/logging.ts
@@ -0,0 +1,25 @@
+/*
+ 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/>
+ */
+
+export class Logger {
+ constructor(private tag: string) {}
+ info(message: string, ...args: any[]) {
+ console.log(`${new Date().toISOString()} ${this.tag} INFO ` + message, ...args);
+ }
+ trace(message: any, ...args: any[]) {
+ console.log(`${new Date().toISOString()} ${this.tag} TRACE ` + message, ...args)
+ }
+} \ No newline at end of file
diff --git a/src/util/payto-test.ts b/src/util/payto-test.ts
new file mode 100644
index 000000000..82daff164
--- /dev/null
+++ b/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");
+}); \ No newline at end of file
diff --git a/src/util/payto.ts b/src/util/payto.ts
new file mode 100644
index 000000000..0926fdeed
--- /dev/null
+++ b/src/util/payto.ts
@@ -0,0 +1,54 @@
+/*
+ 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 PaytoUri {
+ targetType: string;
+ targetPath: string;
+ params: { [name: string]: string };
+}
+
+
+export function parsePaytoUri(s: string): PaytoUri | undefined {
+ const pfx = "payto://"
+ if (!s.startsWith(pfx)) {
+ return undefined;
+ }
+
+ const [acct, search] = s.slice(pfx.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,
+ }
+} \ No newline at end of file
diff --git a/src/util/promiseUtils.ts b/src/util/promiseUtils.ts
new file mode 100644
index 000000000..eb649471b
--- /dev/null
+++ b/src/util/promiseUtils.ts
@@ -0,0 +1,39 @@
+/*
+ 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 };
+} \ No newline at end of file
diff --git a/src/util/query.ts b/src/util/query.ts
new file mode 100644
index 000000000..5726bcaa6
--- /dev/null
+++ b/src/util/query.ts
@@ -0,0 +1,446 @@
+/*
+ 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";
+
+
+/**
+ * Result of an inner join.
+ */
+export interface JoinResult<L, R> {
+ left: L;
+ right: R;
+}
+
+/**
+ * Result of a left outer join.
+ */
+export interface JoinLeftResult<L, R> {
+ left: L;
+ right?: R;
+}
+
+/**
+ * Definition of an object store.
+ */
+export class Store<T> {
+ constructor(
+ public name: string,
+ public storeParams?: 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: 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: 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);
+ };
+ });
+}
+
+export async function oneShotGet<T>(
+ db: IDBDatabase,
+ store: Store<T>,
+ key: any,
+): Promise<T | undefined> {
+ const tx = db.transaction([store.name], "readonly");
+ const req = tx.objectStore(store.name).get(key);
+ const v = await requestToPromise(req)
+ await transactionToPromise(tx);
+ return v;
+}
+
+export async function oneShotGetIndexed<S extends IDBValidKey, T>(
+ db: IDBDatabase,
+ index: Index<S, T>,
+ key: any,
+): Promise<T | undefined> {
+ const tx = 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;
+}
+
+export async function oneShotPut<T>(
+ db: IDBDatabase,
+ store: Store<T>,
+ value: T,
+ key?: any,
+): Promise<any> {
+ const tx = db.transaction([store.name], "readwrite");
+ const req = tx.objectStore(store.name).put(value, key);
+ const v = await requestToPromise(req);
+ await transactionToPromise(tx);
+ return v;
+}
+
+function applyMutation<T>(
+ req: 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: IDBRequest = cursor.update(modVal);
+ req2.onerror = () => {
+ reject(req2.error);
+ };
+ req2.onsuccess = () => {
+ cursor.continue();
+ };
+ } else {
+ cursor.continue();
+ }
+ } else {
+ resolve();
+ }
+ };
+ req.onerror = () => {
+ reject(req.error);
+ };
+ });
+}
+
+export async function oneShotMutate<T>(
+ db: IDBDatabase,
+ store: Store<T>,
+ key: any,
+ f: (x: T) => T | undefined,
+): Promise<void> {
+ const tx = db.transaction([store.name], "readwrite");
+ const req = tx.objectStore(store.name).openCursor(key);
+ await applyMutation(req, f);
+ await transactionToPromise(tx);
+}
+
+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: boolean = false;
+ private awaitingResult: boolean = false;
+
+ constructor(private req: 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 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 = 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 function oneShotIter<T>(
+ db: IDBDatabase,
+ store: Store<T>,
+): ResultStream<T> {
+ const tx = db.transaction([store.name], "readonly");
+ const req = tx.objectStore(store.name).openCursor();
+ return new ResultStream<T>(req);
+}
+
+export function oneShotIterIndex<S extends IDBValidKey, T>(
+ db: IDBDatabase,
+ index: Index<S, T>,
+ query?: any,
+): ResultStream<T> {
+ const tx = db.transaction([index.storeName], "readonly");
+ const req = tx
+ .objectStore(index.storeName)
+ .index(index.indexName)
+ .openCursor(query);
+ return new ResultStream<T>(req);
+}
+
+class TransactionHandle {
+ constructor(private tx: 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);
+ }
+
+ iter<T>(store: Store<T>, key?: any): ResultStream<T> {
+ const req = this.tx.objectStore(store.name).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) {
+ const req = this.tx.objectStore(store.name).openCursor(key);
+ return applyMutation(req, f);
+ }
+}
+
+export function runWithWriteTransaction<T>(
+ db: IDBDatabase,
+ stores: Store<any>[],
+ f: (t: TransactionHandle) => Promise<T>,
+): 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, "readwrite");
+ let funResult: any = undefined;
+ let gotFunResult: boolean = 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");
+ };
+ 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 = f(th);
+ resP.then(result => {
+ gotFunResult = true;
+ funResult = result;
+ }).catch((e) => {
+ if (e == TransactionAbort) {
+ console.info("aborting transaction");
+ } else {
+ tx.abort();
+ console.error("Transaction failed:", e);
+ console.error(stack);
+ }
+ });
+ });
+}
+
+/**
+ * Definition of an index.
+ */
+export class Index<S extends 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;
+}
+
+/**
+ * Exception that should be thrown by client code to abort a transaction.
+ */
+export const TransactionAbort = Symbol("transaction_abort");
diff --git a/src/util/taleruri-test.ts b/src/util/taleruri-test.ts
new file mode 100644
index 000000000..02eecf209
--- /dev/null
+++ b/src/util/taleruri-test.ts
@@ -0,0 +1,230 @@
+/*
+ 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: http(s)", t => {
+ const url1 = "https://example.com/bar?spam=eggs";
+ const r1 = parsePayUri(url1);
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.is(r1.downloadUrl, url1);
+ t.is(r1.sessionId, undefined);
+ const url2 = "http://example.com/bar?spam=eggs";
+ const r2 = parsePayUri(url2);
+ if (!r2) {
+ t.fail();
+ return;
+ }
+});
+
+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(url1);
+ 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.downloadUrl, "https://example.com/public/proposal?order_id=myorder");
+ t.is(r1.sessionId, undefined);
+
+ const url2 = "taler://pay/example.com/-/-/myorder/mysession";
+ const r2 = parsePayUri(url2);
+ if (!r2) {
+ t.fail();
+ return;
+ }
+ t.is(r2.downloadUrl, "https://example.com/public/proposal?order_id=myorder");
+ t.is(r2.sessionId, "mysession");
+});
+
+test("taler pay url parsing: trailing parts", t => {
+ const url1 = "taler://pay/example.com/-/-/myorder/mysession/spam/eggs";
+ const r1 = parsePayUri(url1);
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.is(r1.downloadUrl, "https://example.com/public/proposal?order_id=myorder");
+ t.is(r1.sessionId, "mysession");
+});
+
+test("taler pay url parsing: instance", t => {
+ const url1 = "taler://pay/example.com/-/myinst/myorder";
+ const r1 = parsePayUri(url1);
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.is(
+ r1.downloadUrl,
+ "https://example.com/public/instances/myinst/proposal?order_id=myorder",
+ );
+});
+
+test("taler pay url parsing: path prefix and instance", t => {
+ const url1 = "taler://pay/example.com/mypfx/myinst/myorder";
+ const r1 = parsePayUri(url1);
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.is(
+ r1.downloadUrl,
+ "https://example.com/mypfx/instances/myinst/proposal?order_id=myorder",
+ );
+});
+
+test("taler pay url parsing: complex path prefix", t => {
+ const url1 = "taler://pay/example.com/mypfx%2Fpublic/-/myorder";
+ const r1 = parsePayUri(url1);
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.is(
+ r1.downloadUrl,
+ "https://example.com/mypfx/public/proposal?order_id=myorder",
+ );
+});
+
+test("taler pay url parsing: complex path prefix and instance", t => {
+ const url1 = "taler://pay/example.com/mypfx%2Fpublic/foo/myorder";
+ const r1 = parsePayUri(url1);
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.is(
+ r1.downloadUrl,
+ "https://example.com/mypfx/public/instances/foo/proposal?order_id=myorder",
+ );
+});
+
+test("taler pay url parsing: non-https #1", t => {
+ const url1 = "taler://pay/example.com/-/-/myorder?insecure=1";
+ const r1 = parsePayUri(url1);
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.is(r1.downloadUrl, "http://example.com/public/proposal?order_id=myorder");
+});
+
+test("taler pay url parsing: non-https #2", t => {
+ const url1 = "taler://pay/example.com/-/-/myorder?insecure=2";
+ const r1 = parsePayUri(url1);
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.is(r1.downloadUrl, "https://example.com/public/proposal?order_id=myorder");
+});
+
+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.statusUrl, "https://bank.example.com/api/withdraw-operation/12345");
+});
+
+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.refundUrl,
+ "https://merchant.example.com/public/refund?order_id=1234",
+ );
+});
+
+test("taler refund uri parsing with instance", t => {
+ const url1 = "taler://refund/merchant.example.com/-/myinst/1234";
+ const r1 = parseRefundUri(url1);
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.is(
+ r1.refundUrl,
+ "https://merchant.example.com/public/instances/myinst/refund?order_id=1234",
+ );
+});
+
+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/public/tip-pickup?tip_id=tipid",
+ );
+});
+
+test("taler tip pickup uri with instance", t => {
+ const url1 = "taler://tip/merchant.example.com/-/tipm/tipid";
+ const r1 = parseTipUri(url1);
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.is(
+ r1.merchantBaseUrl,
+ "https://merchant.example.com/public/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%2fpfx/tipm/tipid";
+ const r1 = parseTipUri(url1);
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.is(
+ r1.merchantBaseUrl,
+ "https://merchant.example.com/my/pfx/instances/tipm/",
+ );
+ t.is(r1.merchantTipId, "tipid");
+});
diff --git a/src/util/taleruri.ts b/src/util/taleruri.ts
new file mode 100644
index 000000000..aa6705c07
--- /dev/null
+++ b/src/util/taleruri.ts
@@ -0,0 +1,200 @@
+/*
+ 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 interface PayUriResult {
+ downloadUrl: string;
+ sessionId?: string;
+}
+
+export interface WithdrawUriResult {
+ statusUrl: string;
+}
+
+export interface RefundUriResult {
+ refundUrl: string;
+}
+
+export interface TipUriResult {
+ merchantTipId: string;
+ merchantOrigin: string;
+ merchantBaseUrl: string;
+}
+
+export function parseWithdrawUri(s: string): WithdrawUriResult | undefined {
+ const pfx = "taler://withdraw/";
+ if (!s.startsWith(pfx)) {
+ return undefined;
+ }
+
+ const rest = s.substring(pfx.length);
+
+ let [host, path, withdrawId] = rest.split("/");
+
+ if (path === "-") {
+ path = "api/withdraw-operation";
+ }
+
+ return {
+ statusUrl: `https://${host}/${path}/${withdrawId}`,
+ };
+}
+
+export function parsePayUri(s: string): PayUriResult | undefined {
+ if (s.startsWith("https://") || s.startsWith("http://")) {
+ return {
+ downloadUrl: s,
+ sessionId: undefined,
+ };
+ }
+ const pfx = "taler://pay/";
+ if (!s.startsWith(pfx)) {
+ return undefined;
+ }
+
+ const [path, search] = s.slice(pfx.length).split("?");
+
+ let [host, maybePath, maybeInstance, orderId, maybeSessionid] = path.split(
+ "/",
+ );
+
+ if (!host) {
+ return undefined;
+ }
+
+ if (!maybePath) {
+ return undefined;
+ }
+
+ if (!orderId) {
+ return undefined;
+ }
+
+ if (maybePath === "-") {
+ maybePath = "public/";
+ } else {
+ maybePath = decodeURIComponent(maybePath) + "/";
+ }
+ let maybeInstancePath = "";
+ if (maybeInstance !== "-") {
+ maybeInstancePath = `instances/${maybeInstance}/`;
+ }
+
+ let protocol = "https";
+ const searchParams = new URLSearchParams(search);
+ if (searchParams.get("insecure") === "1") {
+ protocol = "http";
+ }
+
+ const downloadUrl =
+ `${protocol}://${host}/` +
+ decodeURIComponent(maybePath) +
+ maybeInstancePath +
+ `proposal?order_id=${orderId}`;
+
+ return {
+ downloadUrl,
+ sessionId: maybeSessionid,
+ };
+}
+
+export function parseTipUri(s: string): TipUriResult | undefined {
+ const pfx = "taler://tip/";
+ if (!s.startsWith(pfx)) {
+ return undefined;
+ }
+
+ const path = s.slice(pfx.length);
+
+ let [host, maybePath, maybeInstance, tipId] = path.split("/");
+
+ if (!host) {
+ return undefined;
+ }
+
+ if (!maybePath) {
+ return undefined;
+ }
+
+ if (!tipId) {
+ return undefined;
+ }
+
+ if (maybePath === "-") {
+ maybePath = "public/";
+ } else {
+ maybePath = decodeURIComponent(maybePath) + "/";
+ }
+ let maybeInstancePath = "";
+ if (maybeInstance !== "-") {
+ maybeInstancePath = `instances/${maybeInstance}/`;
+ }
+
+ const merchantBaseUrl = `https://${host}/${maybePath}${maybeInstancePath}`;
+
+ return {
+ merchantTipId: tipId,
+ merchantOrigin: new URL(merchantBaseUrl).origin,
+ merchantBaseUrl,
+ };
+}
+
+export function parseRefundUri(s: string): RefundUriResult | undefined {
+ const pfx = "taler://refund/";
+
+ if (!s.startsWith(pfx)) {
+ return undefined;
+ }
+
+ const path = s.slice(pfx.length);
+
+ let [host, maybePath, maybeInstance, orderId] = path.split("/");
+
+ if (!host) {
+ return undefined;
+ }
+
+ if (!maybePath) {
+ return undefined;
+ }
+
+ if (!orderId) {
+ return undefined;
+ }
+
+ if (maybePath === "-") {
+ maybePath = "public/";
+ } else {
+ maybePath = decodeURIComponent(maybePath) + "/";
+ }
+ let maybeInstancePath = "";
+ if (maybeInstance !== "-") {
+ maybeInstancePath = `instances/${maybeInstance}/`;
+ }
+
+ const refundUrl =
+ "https://" +
+ host +
+ "/" +
+ maybePath +
+ maybeInstancePath +
+ "refund" +
+ "?order_id=" +
+ orderId;
+
+ return {
+ refundUrl,
+ };
+}
diff --git a/src/util/timer.ts b/src/util/timer.ts
new file mode 100644
index 000000000..d3bb5d485
--- /dev/null
+++ b/src/util/timer.ts
@@ -0,0 +1,145 @@
+/*
+ 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/>
+ */
+
+/**
+ * Cross-platform timers.
+ *
+ * NodeJS and the browser use slightly different timer API,
+ * this abstracts over these differences.
+ */
+
+/**
+ * Cancelable timer.
+ */
+export interface TimerHandle {
+ clear(): void;
+}
+
+class IntervalHandle {
+ constructor(public h: any) {
+ }
+
+ clear() {
+ clearInterval(this.h);
+ }
+}
+
+class TimeoutHandle {
+ constructor(public h: any) {
+ }
+
+ clear() {
+ clearTimeout(this.h);
+ }
+}
+
+/**
+ * Get a performance counter in milliseconds.
+ */
+export const performanceNow: () => number = (() => {
+ if (typeof process !== "undefined" && process.hrtime) {
+ return () => {
+ const t = process.hrtime();
+ return t[0] * 1e9 + t[1];
+ };
+ } else if (typeof "performance" !== "undefined") {
+ return () => performance.now();
+ } else {
+ 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: boolean = false;
+
+ private timerMap: { [index: number]: TimerHandle } = {};
+
+ private idGen = 1;
+
+ stopCurrentAndFutureTimers() {
+ this.stopped = true;
+ for (const x in this.timerMap) {
+ if (!this.timerMap.hasOwnProperty(x)) {
+ continue;
+ }
+ this.timerMap[x].clear();
+ delete this.timerMap[x];
+ }
+ }
+
+ after(delayMs: number, callback: () => void): TimerHandle {
+ if (this.stopped) {
+ console.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) {
+ console.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/src/util/wire.ts b/src/util/wire.ts
new file mode 100644
index 000000000..63b73d864
--- /dev/null
+++ b/src/util/wire.ts
@@ -0,0 +1,53 @@
+/*
+ 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`;
+ }
+}
+