diff options
author | Florian Dold <florian.dold@gmail.com> | 2020-08-03 13:00:48 +0530 |
---|---|---|
committer | Florian Dold <florian.dold@gmail.com> | 2020-08-03 13:01:05 +0530 |
commit | ffd2a62c3f7df94365980302fef3bc3376b48182 (patch) | |
tree | 270af6f16b4cc7f5da2afdba55c8bc9dbea5eca5 /src/util | |
parent | aa481e42675fb7c4dcbbeec0ba1c61e1953b9596 (diff) | |
download | wallet-core-ffd2a62c3f7df94365980302fef3bc3376b48182.tar.xz |
modularize repo, use pnpm, improve typechecking
Diffstat (limited to 'src/util')
-rw-r--r-- | src/util/RequestThrottler.ts | 128 | ||||
-rw-r--r-- | src/util/amounts-test.ts | 140 | ||||
-rw-r--r-- | src/util/amounts.ts | 383 | ||||
-rw-r--r-- | src/util/assertUnreachable.ts | 19 | ||||
-rw-r--r-- | src/util/asyncMemo.ts | 87 | ||||
-rw-r--r-- | src/util/codec-test.ts | 78 | ||||
-rw-r--r-- | src/util/codec.ts | 410 | ||||
-rw-r--r-- | src/util/helpers-test.ts | 46 | ||||
-rw-r--r-- | src/util/helpers.ts | 147 | ||||
-rw-r--r-- | src/util/http.ts | 362 | ||||
-rw-r--r-- | src/util/libtoolVersion-test.ts | 48 | ||||
-rw-r--r-- | src/util/libtoolVersion.ts | 88 | ||||
-rw-r--r-- | src/util/logging.ts | 87 | ||||
-rw-r--r-- | src/util/payto-test.ts | 31 | ||||
-rw-r--r-- | src/util/payto.ts | 69 | ||||
-rw-r--r-- | src/util/promiseUtils.ts | 60 | ||||
-rw-r--r-- | src/util/query.ts | 575 | ||||
-rw-r--r-- | src/util/reserveHistoryUtil-test.ts | 285 | ||||
-rw-r--r-- | src/util/reserveHistoryUtil.ts | 360 | ||||
-rw-r--r-- | src/util/talerconfig.ts | 120 | ||||
-rw-r--r-- | src/util/taleruri-test.ts | 193 | ||||
-rw-r--r-- | src/util/taleruri.ts | 211 | ||||
-rw-r--r-- | src/util/time.ts | 198 | ||||
-rw-r--r-- | src/util/timer.ts | 160 | ||||
-rw-r--r-- | src/util/wire.ts | 51 |
25 files changed, 0 insertions, 4336 deletions
diff --git a/src/util/RequestThrottler.ts b/src/util/RequestThrottler.ts deleted file mode 100644 index d979fbfcf..000000000 --- a/src/util/RequestThrottler.ts +++ /dev/null @@ -1,128 +0,0 @@ -/* - 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"; - -/** - * 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/src/util/amounts-test.ts b/src/util/amounts-test.ts deleted file mode 100644 index afd8caa51..000000000 --- a/src/util/amounts-test.ts +++ /dev/null @@ -1,140 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -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/src/util/amounts.ts b/src/util/amounts.ts deleted file mode 100644 index 1e7f192f4..000000000 --- a/src/util/amounts.ts +++ /dev/null @@ -1,383 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2019 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * 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, -}; diff --git a/src/util/assertUnreachable.ts b/src/util/assertUnreachable.ts deleted file mode 100644 index ffdf88f04..000000000 --- a/src/util/assertUnreachable.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - 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/src/util/asyncMemo.ts b/src/util/asyncMemo.ts deleted file mode 100644 index 6e88081b6..000000000 --- a/src/util/asyncMemo.ts +++ /dev/null @@ -1,87 +0,0 @@ -/* - 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/src/util/codec-test.ts b/src/util/codec-test.ts deleted file mode 100644 index b429c318c..000000000 --- a/src/util/codec-test.ts +++ /dev/null @@ -1,78 +0,0 @@ -/* - 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/src/util/codec.ts b/src/util/codec.ts deleted file mode 100644 index 383a2d99e..000000000 --- a/src/util/codec.ts +++ /dev/null @@ -1,410 +0,0 @@ -/* - 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/src/util/helpers-test.ts b/src/util/helpers-test.ts deleted file mode 100644 index dbecf14b8..000000000 --- a/src/util/helpers-test.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - 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/src/util/helpers.ts b/src/util/helpers.ts deleted file mode 100644 index 7cd9e4234..000000000 --- a/src/util/helpers.ts +++ /dev/null @@ -1,147 +0,0 @@ -/* - 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"; - -/** - * 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/src/util/http.ts b/src/util/http.ts deleted file mode 100644 index 38892491b..000000000 --- a/src/util/http.ts +++ /dev/null @@ -1,362 +0,0 @@ -/* - 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>; -} - -/** - * An implementation of the [[HttpRequestLibrary]] using the - * browser's XMLHttpRequest. - */ -export class BrowserHttpLib implements HttpRequestLibrary { - private req( - method: string, - url: string, - requestBody?: any, - options?: HttpRequestOptions, - ): Promise<HttpResponse> { - return new Promise<HttpResponse>((resolve, reject) => { - const myRequest = new XMLHttpRequest(); - myRequest.open(method, url); - if (options?.headers) { - for (const headerName in options.headers) { - myRequest.setRequestHeader(headerName, options.headers[headerName]); - } - } - myRequest.setRequestHeader; - if (requestBody) { - myRequest.send(requestBody); - } else { - myRequest.send(); - } - - myRequest.onerror = (e) => { - logger.error("http request error"); - reject( - OperationFailedError.fromCode( - TalerErrorCode.WALLET_NETWORK_ERROR, - "Could not make request", - { - requestUrl: url, - }, - ), - ); - }; - - myRequest.addEventListener("readystatechange", (e) => { - if (myRequest.readyState === XMLHttpRequest.DONE) { - if (myRequest.status === 0) { - const exc = OperationFailedError.fromCode( - TalerErrorCode.WALLET_NETWORK_ERROR, - "HTTP request failed (status 0, maybe URI scheme was wrong?)", - { - requestUrl: url, - }, - ); - reject(exc); - return; - } - const makeJson = async (): Promise<any> => { - let responseJson; - try { - responseJson = JSON.parse(myRequest.responseText); - } catch (e) { - throw OperationFailedError.fromCode( - TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE, - "Invalid JSON from HTTP response", - { - requestUrl: url, - httpStatusCode: myRequest.status, - }, - ); - } - if (responseJson === null || typeof responseJson !== "object") { - throw OperationFailedError.fromCode( - TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE, - "Invalid JSON from HTTP response", - { - requestUrl: url, - httpStatusCode: myRequest.status, - }, - ); - } - return responseJson; - }; - - const headers = myRequest.getAllResponseHeaders(); - const arr = headers.trim().split(/[\r\n]+/); - - // Create a map of header names to values - const headerMap = new Headers(); - arr.forEach(function (line) { - const parts = line.split(": "); - const headerName = parts.shift(); - if (!headerName) { - logger.warn("skipping invalid header"); - return; - } - const value = parts.join(": "); - headerMap.set(headerName, value); - }); - const resp: HttpResponse = { - requestUrl: url, - status: myRequest.status, - headers: headerMap, - json: makeJson, - text: async () => myRequest.responseText, - }; - resolve(resp); - } - }); - }); - } - - get(url: string, opt?: HttpRequestOptions): Promise<HttpResponse> { - return this.req("get", url, undefined, opt); - } - - postJson( - url: string, - body: unknown, - opt?: HttpRequestOptions, - ): Promise<HttpResponse> { - return this.req("post", url, JSON.stringify(body), opt); - } - - stop(): void { - // Nothing to do - } -} - -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/src/util/libtoolVersion-test.ts b/src/util/libtoolVersion-test.ts deleted file mode 100644 index e58e94759..000000000 --- a/src/util/libtoolVersion-test.ts +++ /dev/null @@ -1,48 +0,0 @@ -/* - 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 deleted file mode 100644 index 5e9d0b74e..000000000 --- a/src/util/libtoolVersion.ts +++ /dev/null @@ -1,88 +0,0 @@ -/* - 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 deleted file mode 100644 index 83e8d2192..000000000 --- a/src/util/logging.ts +++ /dev/null @@ -1,87 +0,0 @@ -/* - 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/> - */ - -/** - * Imports. - */ -import { isNode } from "../webex/compat"; - -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/src/util/payto-test.ts b/src/util/payto-test.ts deleted file mode 100644 index 01280b650..000000000 --- a/src/util/payto-test.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - 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/src/util/payto.ts b/src/util/payto.ts deleted file mode 100644 index 835214b86..000000000 --- a/src/util/payto.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* - 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 }; -} - -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/src/util/promiseUtils.ts b/src/util/promiseUtils.ts deleted file mode 100644 index d409686d9..000000000 --- a/src/util/promiseUtils.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* - 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/src/util/query.ts b/src/util/query.ts deleted file mode 100644 index be319049b..000000000 --- a/src/util/query.ts +++ /dev/null @@ -1,575 +0,0 @@ -/* - 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"; - -/** - * 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?: 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); - }; - }); -} - -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); - }; - }); -} - -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: 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: 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: 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 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 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: 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 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: IDBFactory, - databaseName: string, - databaseVersion: number, - onVersionChange: () => void, - onUpgradeNeeded: ( - db: IDBDatabase, - oldVersion: number, - newVersion: number, - ) => void, -): Promise<IDBDatabase> { - return new Promise<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: 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: IDBDatabase) {} - - static deleteDatabase(idbFactory: 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: 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 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 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/src/util/reserveHistoryUtil-test.ts b/src/util/reserveHistoryUtil-test.ts deleted file mode 100644 index 79022de77..000000000 --- a/src/util/reserveHistoryUtil-test.ts +++ /dev/null @@ -1,285 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * 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/src/util/reserveHistoryUtil.ts b/src/util/reserveHistoryUtil.ts deleted file mode 100644 index 855b71a3d..000000000 --- a/src/util/reserveHistoryUtil.ts +++ /dev/null @@ -1,360 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * 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/src/util/talerconfig.ts b/src/util/talerconfig.ts deleted file mode 100644 index ec08c352f..000000000 --- a/src/util/talerconfig.ts +++ /dev/null @@ -1,120 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2020 Taler Systems S.A. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * 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/src/util/taleruri-test.ts b/src/util/taleruri-test.ts deleted file mode 100644 index 44edbe1c1..000000000 --- a/src/util/taleruri-test.ts +++ /dev/null @@ -1,193 +0,0 @@ -/* - 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/src/util/taleruri.ts b/src/util/taleruri.ts deleted file mode 100644 index c26c4a5db..000000000 --- a/src/util/taleruri.ts +++ /dev/null @@ -1,211 +0,0 @@ -/* - 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/> - */ - -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/src/util/time.ts b/src/util/time.ts deleted file mode 100644 index 5c2f49d12..000000000 --- a/src/util/time.ts +++ /dev/null @@ -1,198 +0,0 @@ -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/src/util/timer.ts b/src/util/timer.ts deleted file mode 100644 index 5f37a6f4d..000000000 --- a/src/util/timer.ts +++ /dev/null @@ -1,160 +0,0 @@ -/* - 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 = (() => { - 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 = 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/src/util/wire.ts b/src/util/wire.ts deleted file mode 100644 index 21ad600fc..000000000 --- a/src/util/wire.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - 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 "../webex/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`; - } -} |