summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core/src/util
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-wallet-core/src/util')
-rw-r--r--packages/taler-wallet-core/src/util/RequestThrottler.ts156
-rw-r--r--packages/taler-wallet-core/src/util/assertUnreachable.ts19
-rw-r--r--packages/taler-wallet-core/src/util/asyncMemo.ts87
-rw-r--r--packages/taler-wallet-core/src/util/coinSelection.test.ts254
-rw-r--r--packages/taler-wallet-core/src/util/coinSelection.ts332
-rw-r--r--packages/taler-wallet-core/src/util/contractTerms.test.ts122
-rw-r--r--packages/taler-wallet-core/src/util/contractTerms.ts231
-rw-r--r--packages/taler-wallet-core/src/util/debugFlags.ts32
-rw-r--r--packages/taler-wallet-core/src/util/http.ts342
-rw-r--r--packages/taler-wallet-core/src/util/invariants.ts39
-rw-r--r--packages/taler-wallet-core/src/util/promiseUtils.ts60
-rw-r--r--packages/taler-wallet-core/src/util/query.ts615
-rw-r--r--packages/taler-wallet-core/src/util/retries.ts85
-rw-r--r--packages/taler-wallet-core/src/util/timer.ts199
14 files changed, 0 insertions, 2573 deletions
diff --git a/packages/taler-wallet-core/src/util/RequestThrottler.ts b/packages/taler-wallet-core/src/util/RequestThrottler.ts
deleted file mode 100644
index d79afe47a..000000000
--- a/packages/taler-wallet-core/src/util/RequestThrottler.ts
+++ /dev/null
@@ -1,156 +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,
- timestampCmp,
- Logger,
- URL,
-} from "@gnu-taler/taler-util";
-
-const logger = new Logger("RequestThrottler.ts");
-
-/**
- * Maximum request per second, per origin.
- */
-const MAX_PER_SECOND = 100;
-
-/**
- * Maximum request per minute, per origin.
- */
-const MAX_PER_MINUTE = 500;
-
-/**
- * Maximum request per hour, per origin.
- */
-const MAX_PER_HOUR = 2000;
-
-/**
- * Throttling state for one origin.
- */
-class OriginState {
- tokensSecond: number = MAX_PER_SECOND;
- tokensMinute: number = MAX_PER_MINUTE;
- tokensHour: number = MAX_PER_HOUR;
- private lastUpdate = getTimestampNow();
-
- private refill(): void {
- const now = getTimestampNow();
- if (timestampCmp(now, this.lastUpdate) < 0) {
- // Did the system time change?
- this.lastUpdate = now;
- return;
- }
- const d = timestampDifference(now, this.lastUpdate);
- if (d.d_ms === "forever") {
- throw Error("assertion failed");
- }
- this.tokensSecond = Math.min(
- MAX_PER_SECOND,
- this.tokensSecond + d.d_ms / 1000,
- );
- this.tokensMinute = Math.min(
- MAX_PER_MINUTE,
- this.tokensMinute + d.d_ms / 1000 / 60,
- );
- this.tokensHour = Math.min(
- MAX_PER_HOUR,
- this.tokensHour + d.d_ms / 1000 / 60 / 60,
- );
- this.lastUpdate = now;
- }
-
- /**
- * Return true if the request for this origin should be throttled.
- * Otherwise, take a token out of the respective buckets.
- */
- applyThrottle(): boolean {
- this.refill();
- if (this.tokensSecond < 1) {
- logger.warn("request throttled (per second limit exceeded)");
- return true;
- }
- if (this.tokensMinute < 1) {
- logger.warn("request throttled (per minute limit exceeded)");
- return true;
- }
- if (this.tokensHour < 1) {
- logger.warn("request throttled (per hour limit exceeded)");
- return true;
- }
- this.tokensSecond--;
- this.tokensMinute--;
- this.tokensHour--;
- return false;
- }
-}
-
-/**
- * Request throttler, used as a "last layer of defense" when some
- * other part of the re-try logic is broken and we're sending too
- * many requests to the same exchange/bank/merchant.
- */
-export class 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();
- }
-
- /**
- * Get the throttle statistics for a particular URL.
- */
- getThrottleStats(requestUrl: string): Record<string, unknown> {
- const origin = new URL(requestUrl).origin;
- const state = this.getState(origin);
- return {
- tokensHour: state.tokensHour,
- tokensMinute: state.tokensMinute,
- tokensSecond: state.tokensSecond,
- maxTokensHour: MAX_PER_HOUR,
- maxTokensMinute: MAX_PER_MINUTE,
- maxTokensSecond: MAX_PER_SECOND,
- };
- }
-}
diff --git a/packages/taler-wallet-core/src/util/assertUnreachable.ts b/packages/taler-wallet-core/src/util/assertUnreachable.ts
deleted file mode 100644
index ffdf88f04..000000000
--- a/packages/taler-wallet-core/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/packages/taler-wallet-core/src/util/asyncMemo.ts b/packages/taler-wallet-core/src/util/asyncMemo.ts
deleted file mode 100644
index 6e88081b6..000000000
--- a/packages/taler-wallet-core/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/packages/taler-wallet-core/src/util/coinSelection.test.ts b/packages/taler-wallet-core/src/util/coinSelection.test.ts
deleted file mode 100644
index ed48b8dd1..000000000
--- a/packages/taler-wallet-core/src/util/coinSelection.test.ts
+++ /dev/null
@@ -1,254 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- * Imports.
- */
-import test from "ava";
-import { AmountJson, Amounts } from "@gnu-taler/taler-util";
-import { AvailableCoinInfo, selectPayCoins } from "./coinSelection.js";
-
-function a(x: string): AmountJson {
- const amt = Amounts.parse(x);
- if (!amt) {
- throw Error("invalid amount");
- }
- return amt;
-}
-
-function fakeAci(current: string, feeDeposit: string): AvailableCoinInfo {
- return {
- availableAmount: a(current),
- coinPub: "foobar",
- denomPub: "foobar",
- feeDeposit: a(feeDeposit),
- exchangeBaseUrl: "https://example.com/",
- };
-}
-
-test("coin selection 1", (t) => {
- const acis: AvailableCoinInfo[] = [
- fakeAci("EUR:1.0", "EUR:0.1"),
- fakeAci("EUR:1.0", "EUR:0.0"),
- ];
-
- const res = selectPayCoins({
- candidates: {
- candidateCoins: acis,
- wireFeesPerExchange: {},
- },
- contractTermsAmount: a("EUR:2.0"),
- depositFeeLimit: a("EUR:0.1"),
- wireFeeLimit: a("EUR:0"),
- wireFeeAmortization: 1,
- });
-
- if (!res) {
- t.fail();
- return;
- }
- t.true(res.coinPubs.length === 2);
- t.pass();
-});
-
-test("coin selection 2", (t) => {
- const acis: AvailableCoinInfo[] = [
- fakeAci("EUR:1.0", "EUR:0.5"),
- fakeAci("EUR:1.0", "EUR:0.0"),
- // Merchant covers the fee, this one shouldn't be used
- fakeAci("EUR:1.0", "EUR:0.0"),
- ];
-
- const res = selectPayCoins({
- candidates: {
- candidateCoins: acis,
- wireFeesPerExchange: {},
- },
- contractTermsAmount: a("EUR:2.0"),
- depositFeeLimit: a("EUR:0.5"),
- wireFeeLimit: a("EUR:0"),
- wireFeeAmortization: 1,
- });
-
- if (!res) {
- t.fail();
- return;
- }
- t.true(res.coinPubs.length === 2);
- t.pass();
-});
-
-test("coin selection 3", (t) => {
- const acis: AvailableCoinInfo[] = [
- fakeAci("EUR:1.0", "EUR:0.5"),
- fakeAci("EUR:1.0", "EUR:0.5"),
- // this coin should be selected instead of previous one with fee
- fakeAci("EUR:1.0", "EUR:0.0"),
- ];
-
- const res = selectPayCoins({
- candidates: {
- candidateCoins: acis,
- wireFeesPerExchange: {},
- },
- contractTermsAmount: a("EUR:2.0"),
- depositFeeLimit: a("EUR:0.5"),
- wireFeeLimit: a("EUR:0"),
- wireFeeAmortization: 1,
- });
-
- if (!res) {
- t.fail();
- return;
- }
- t.true(res.coinPubs.length === 2);
- t.pass();
-});
-
-test("coin selection 4", (t) => {
- const acis: AvailableCoinInfo[] = [
- fakeAci("EUR:1.0", "EUR:0.5"),
- fakeAci("EUR:1.0", "EUR:0.5"),
- fakeAci("EUR:1.0", "EUR:0.5"),
- ];
-
- const res = selectPayCoins({
- candidates: {
- candidateCoins: acis,
- wireFeesPerExchange: {},
- },
- contractTermsAmount: a("EUR:2.0"),
- depositFeeLimit: a("EUR:0.5"),
- wireFeeLimit: a("EUR:0"),
- wireFeeAmortization: 1,
- });
-
- if (!res) {
- t.fail();
- return;
- }
- t.true(res.coinPubs.length === 3);
- t.pass();
-});
-
-test("coin selection 5", (t) => {
- const acis: AvailableCoinInfo[] = [
- fakeAci("EUR:1.0", "EUR:0.5"),
- fakeAci("EUR:1.0", "EUR:0.5"),
- fakeAci("EUR:1.0", "EUR:0.5"),
- ];
-
- const res = selectPayCoins({
- candidates: {
- candidateCoins: acis,
- wireFeesPerExchange: {},
- },
- contractTermsAmount: a("EUR:4.0"),
- depositFeeLimit: a("EUR:0.2"),
- wireFeeLimit: a("EUR:0"),
- wireFeeAmortization: 1,
- });
-
- t.true(!res);
- t.pass();
-});
-
-test("coin selection 6", (t) => {
- const acis: AvailableCoinInfo[] = [
- fakeAci("EUR:1.0", "EUR:0.5"),
- fakeAci("EUR:1.0", "EUR:0.5"),
- ];
- const res = selectPayCoins({
- candidates: {
- candidateCoins: acis,
- wireFeesPerExchange: {},
- },
- contractTermsAmount: a("EUR:2.0"),
- depositFeeLimit: a("EUR:0.2"),
- wireFeeLimit: a("EUR:0"),
- wireFeeAmortization: 1,
- });
- t.true(!res);
- t.pass();
-});
-
-test("coin selection 7", (t) => {
- const acis: AvailableCoinInfo[] = [
- fakeAci("EUR:1.0", "EUR:0.1"),
- fakeAci("EUR:1.0", "EUR:0.1"),
- ];
- const res = selectPayCoins({
- candidates: {
- candidateCoins: acis,
- wireFeesPerExchange: {},
- },
- contractTermsAmount: a("EUR:2.0"),
- depositFeeLimit: a("EUR:0.2"),
- wireFeeLimit: a("EUR:0"),
- wireFeeAmortization: 1,
- });
- t.truthy(res);
- t.true(Amounts.cmp(res!.customerDepositFees, "EUR:0.0") === 0);
- t.true(
- Amounts.cmp(Amounts.sum(res!.coinContributions).amount, "EUR:2.0") === 0,
- );
- t.pass();
-});
-
-test("coin selection 8", (t) => {
- const acis: AvailableCoinInfo[] = [
- fakeAci("EUR:1.0", "EUR:0.2"),
- fakeAci("EUR:0.1", "EUR:0.2"),
- fakeAci("EUR:0.05", "EUR:0.05"),
- fakeAci("EUR:0.05", "EUR:0.05"),
- ];
- const res = selectPayCoins({
- candidates: {
- candidateCoins: acis,
- wireFeesPerExchange: {},
- },
- contractTermsAmount: a("EUR:1.1"),
- depositFeeLimit: a("EUR:0.4"),
- wireFeeLimit: a("EUR:0"),
- wireFeeAmortization: 1,
- });
- t.truthy(res);
- t.true(res!.coinContributions.length === 3);
- t.pass();
-});
-
-test("coin selection 9", (t) => {
- const acis: AvailableCoinInfo[] = [
- fakeAci("EUR:1.0", "EUR:0.2"),
- fakeAci("EUR:0.2", "EUR:0.2"),
- ];
- const res = selectPayCoins({
- candidates: {
- candidateCoins: acis,
- wireFeesPerExchange: {},
- },
- contractTermsAmount: a("EUR:1.2"),
- depositFeeLimit: a("EUR:0.4"),
- wireFeeLimit: a("EUR:0"),
- wireFeeAmortization: 1,
- });
- t.truthy(res);
- t.true(res!.coinContributions.length === 2);
- t.true(
- Amounts.cmp(Amounts.sum(res!.coinContributions).amount, "EUR:1.2") === 0,
- );
- t.pass();
-});
diff --git a/packages/taler-wallet-core/src/util/coinSelection.ts b/packages/taler-wallet-core/src/util/coinSelection.ts
deleted file mode 100644
index 500cee5d8..000000000
--- a/packages/taler-wallet-core/src/util/coinSelection.ts
+++ /dev/null
@@ -1,332 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- * Selection of coins for payments.
- *
- * @author Florian Dold
- */
-
-/**
- * Imports.
- */
-import { AmountJson, Amounts } from "@gnu-taler/taler-util";
-import { strcmp, Logger } from "@gnu-taler/taler-util";
-
-const logger = new Logger("coinSelection.ts");
-
-/**
- * Result of selecting coins, contains the exchange, and selected
- * coins with their denomination.
- */
-export interface PayCoinSelection {
- /**
- * Amount requested by the merchant.
- */
- paymentAmount: AmountJson;
-
- /**
- * Public keys of the coins that were selected.
- */
- coinPubs: string[];
-
- /**
- * Amount that each coin contributes.
- */
- coinContributions: AmountJson[];
-
- /**
- * How much of the wire fees is the customer paying?
- */
- customerWireFees: AmountJson;
-
- /**
- * How much of the deposit fees is the customer paying?
- */
- customerDepositFees: AmountJson;
-}
-
-/**
- * Structure to describe a coin that is available to be
- * used in a payment.
- */
-export interface AvailableCoinInfo {
- /**
- * Public key of the coin.
- */
- coinPub: string;
-
- /**
- * Coin's denomination public key.
- */
- denomPub: string;
-
- /**
- * Amount still remaining (typically the full amount,
- * as coins are always refreshed after use.)
- */
- availableAmount: AmountJson;
-
- /**
- * Deposit fee for the coin.
- */
- feeDeposit: AmountJson;
-
- exchangeBaseUrl: string;
-}
-
-export type PreviousPayCoins = {
- coinPub: string;
- contribution: AmountJson;
- feeDeposit: AmountJson;
- exchangeBaseUrl: string;
-}[];
-
-export interface CoinCandidateSelection {
- candidateCoins: AvailableCoinInfo[];
- wireFeesPerExchange: Record<string, AmountJson>;
-}
-
-export interface SelectPayCoinRequest {
- candidates: CoinCandidateSelection;
- contractTermsAmount: AmountJson;
- depositFeeLimit: AmountJson;
- wireFeeLimit: AmountJson;
- wireFeeAmortization: number;
- prevPayCoins?: PreviousPayCoins;
-}
-
-interface CoinSelectionTally {
- /**
- * Amount that still needs to be paid.
- * May increase during the computation when fees need to be covered.
- */
- amountPayRemaining: AmountJson;
-
- /**
- * Allowance given by the merchant towards wire fees
- */
- amountWireFeeLimitRemaining: AmountJson;
-
- /**
- * Allowance given by the merchant towards deposit fees
- * (and wire fees after wire fee limit is exhausted)
- */
- amountDepositFeeLimitRemaining: AmountJson;
-
- customerDepositFees: AmountJson;
-
- customerWireFees: AmountJson;
-
- wireFeeCoveredForExchange: Set<string>;
-}
-
-/**
- * Account for the fees of spending a coin.
- */
-function tallyFees(
- tally: CoinSelectionTally,
- wireFeesPerExchange: Record<string, AmountJson>,
- wireFeeAmortization: number,
- exchangeBaseUrl: string,
- feeDeposit: AmountJson,
-): CoinSelectionTally {
- const currency = tally.amountPayRemaining.currency;
- let amountWireFeeLimitRemaining = tally.amountWireFeeLimitRemaining;
- let amountDepositFeeLimitRemaining = tally.amountDepositFeeLimitRemaining;
- let customerDepositFees = tally.customerDepositFees;
- let customerWireFees = tally.customerWireFees;
- let amountPayRemaining = tally.amountPayRemaining;
- const wireFeeCoveredForExchange = new Set(tally.wireFeeCoveredForExchange);
-
- if (!tally.wireFeeCoveredForExchange.has(exchangeBaseUrl)) {
- const wf =
- wireFeesPerExchange[exchangeBaseUrl] ?? Amounts.getZero(currency);
- const wfForgiven = Amounts.min(amountWireFeeLimitRemaining, wf);
- amountWireFeeLimitRemaining = Amounts.sub(
- amountWireFeeLimitRemaining,
- wfForgiven,
- ).amount;
- // The remaining, amortized amount needs to be paid by the
- // wallet or covered by the deposit fee allowance.
- let wfRemaining = Amounts.divide(
- Amounts.sub(wf, wfForgiven).amount,
- wireFeeAmortization,
- );
-
- // This is the amount forgiven via the deposit fee allowance.
- const wfDepositForgiven = Amounts.min(
- amountDepositFeeLimitRemaining,
- wfRemaining,
- );
- amountDepositFeeLimitRemaining = Amounts.sub(
- amountDepositFeeLimitRemaining,
- wfDepositForgiven,
- ).amount;
-
- wfRemaining = Amounts.sub(wfRemaining, wfDepositForgiven).amount;
- customerWireFees = Amounts.add(customerWireFees, wfRemaining).amount;
- amountPayRemaining = Amounts.add(amountPayRemaining, wfRemaining).amount;
-
- wireFeeCoveredForExchange.add(exchangeBaseUrl);
- }
-
- const dfForgiven = Amounts.min(feeDeposit, amountDepositFeeLimitRemaining);
-
- amountDepositFeeLimitRemaining = Amounts.sub(
- amountDepositFeeLimitRemaining,
- dfForgiven,
- ).amount;
-
- // How much does the user spend on deposit fees for this coin?
- const dfRemaining = Amounts.sub(feeDeposit, dfForgiven).amount;
- customerDepositFees = Amounts.add(customerDepositFees, dfRemaining).amount;
- amountPayRemaining = Amounts.add(amountPayRemaining, dfRemaining).amount;
-
- return {
- amountDepositFeeLimitRemaining,
- amountPayRemaining,
- amountWireFeeLimitRemaining,
- customerDepositFees,
- customerWireFees,
- wireFeeCoveredForExchange,
- };
-}
-
-/**
- * Given a list of candidate coins, select coins to spend under the merchant's
- * constraints.
- *
- * The prevPayCoins can be specified to "repair" a coin selection
- * by adding additional coins, after a broken (e.g. double-spent) coin
- * has been removed from the selection.
- *
- * This function is only exported for the sake of unit tests.
- */
-export function selectPayCoins(
- req: SelectPayCoinRequest,
-): PayCoinSelection | undefined {
- const {
- candidates,
- contractTermsAmount,
- depositFeeLimit,
- wireFeeLimit,
- wireFeeAmortization,
- } = req;
-
- if (candidates.candidateCoins.length === 0) {
- return undefined;
- }
- const coinPubs: string[] = [];
- const coinContributions: AmountJson[] = [];
- const currency = contractTermsAmount.currency;
-
- let tally: CoinSelectionTally = {
- amountPayRemaining: contractTermsAmount,
- amountWireFeeLimitRemaining: wireFeeLimit,
- amountDepositFeeLimitRemaining: depositFeeLimit,
- customerDepositFees: Amounts.getZero(currency),
- customerWireFees: Amounts.getZero(currency),
- wireFeeCoveredForExchange: new Set(),
- };
-
- const prevPayCoins = req.prevPayCoins ?? [];
-
- // Look at existing pay coin selection and tally up
- for (const prev of prevPayCoins) {
- tally = tallyFees(
- tally,
- candidates.wireFeesPerExchange,
- wireFeeAmortization,
- prev.exchangeBaseUrl,
- prev.feeDeposit,
- );
- tally.amountPayRemaining = Amounts.sub(
- tally.amountPayRemaining,
- prev.contribution,
- ).amount;
-
- coinPubs.push(prev.coinPub);
- coinContributions.push(prev.contribution);
- }
-
- const prevCoinPubs = new Set(prevPayCoins.map((x) => x.coinPub));
-
- // Sort by available amount (descending), deposit fee (ascending) and
- // denomPub (ascending) if deposit fee is the same
- // (to guarantee deterministic results)
- const candidateCoins = [...candidates.candidateCoins].sort(
- (o1, o2) =>
- -Amounts.cmp(o1.availableAmount, o2.availableAmount) ||
- Amounts.cmp(o1.feeDeposit, o2.feeDeposit) ||
- strcmp(o1.denomPub, o2.denomPub),
- );
-
- // FIXME: Here, we should select coins in a smarter way.
- // Instead of always spending the next-largest coin,
- // we should try to find the smallest coin that covers the
- // amount.
-
- for (const aci of candidateCoins) {
- // Don't use this coin if depositing it is more expensive than
- // the amount it would give the merchant.
- if (Amounts.cmp(aci.feeDeposit, aci.availableAmount) > 0) {
- continue;
- }
-
- if (Amounts.isZero(tally.amountPayRemaining)) {
- // We have spent enough!
- break;
- }
-
- // The same coin can't contribute twice to the same payment,
- // by a fundamental, intentional limitation of the protocol.
- if (prevCoinPubs.has(aci.coinPub)) {
- continue;
- }
-
- tally = tallyFees(
- tally,
- candidates.wireFeesPerExchange,
- wireFeeAmortization,
- aci.exchangeBaseUrl,
- aci.feeDeposit,
- );
-
- let coinSpend = Amounts.max(
- Amounts.min(tally.amountPayRemaining, aci.availableAmount),
- aci.feeDeposit,
- );
-
- tally.amountPayRemaining = Amounts.sub(
- tally.amountPayRemaining,
- coinSpend,
- ).amount;
- coinPubs.push(aci.coinPub);
- coinContributions.push(coinSpend);
- }
-
- if (Amounts.isZero(tally.amountPayRemaining)) {
- return {
- paymentAmount: contractTermsAmount,
- coinContributions,
- coinPubs,
- customerDepositFees: tally.customerDepositFees,
- customerWireFees: tally.customerWireFees,
- };
- }
- return undefined;
-}
diff --git a/packages/taler-wallet-core/src/util/contractTerms.test.ts b/packages/taler-wallet-core/src/util/contractTerms.test.ts
deleted file mode 100644
index 74cae4ca7..000000000
--- a/packages/taler-wallet-core/src/util/contractTerms.test.ts
+++ /dev/null
@@ -1,122 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- * Imports.
- */
-import test from "ava";
-import { ContractTermsUtil } from "./contractTerms.js";
-
-test("contract terms canon hashing", (t) => {
- const cReq = {
- foo: 42,
- bar: "hello",
- $forgettable: {
- foo: true,
- },
- };
-
- const c1 = ContractTermsUtil.saltForgettable(cReq);
- const c2 = ContractTermsUtil.saltForgettable(cReq);
- t.assert(typeof cReq.$forgettable.foo === "boolean");
- t.assert(typeof c1.$forgettable.foo === "string");
- t.assert(c1.$forgettable.foo !== c2.$forgettable.foo);
-
- const h1 = ContractTermsUtil.hashContractTerms(c1);
-
- const c3 = ContractTermsUtil.scrub(JSON.parse(JSON.stringify(c1)));
-
- t.assert(c3.foo === undefined);
- t.assert(c3.bar === cReq.bar);
-
- const h2 = ContractTermsUtil.hashContractTerms(c3);
-
- t.deepEqual(h1, h2);
-});
-
-test("contract terms canon hashing (nested)", (t) => {
- const cReq = {
- foo: 42,
- bar: {
- prop1: "hello, world",
- $forgettable: {
- prop1: true,
- },
- },
- $forgettable: {
- bar: true,
- },
- };
-
- const c1 = ContractTermsUtil.saltForgettable(cReq);
-
- t.is(typeof c1.$forgettable.bar, "string");
- t.is(typeof c1.bar.$forgettable.prop1, "string");
-
- const forgetPath = (x: any, s: string) =>
- ContractTermsUtil.forgetAll(x, (p) => p.join(".") === s);
-
- // Forget bar first
- const c2 = forgetPath(c1, "bar");
-
- // Forget bar.prop1 first
- const c3 = forgetPath(forgetPath(c1, "bar.prop1"), "bar");
-
- // Forget everything
- const c4 = ContractTermsUtil.scrub(c1);
-
- const h1 = ContractTermsUtil.hashContractTerms(c1);
- const h2 = ContractTermsUtil.hashContractTerms(c2);
- const h3 = ContractTermsUtil.hashContractTerms(c3);
- const h4 = ContractTermsUtil.hashContractTerms(c4);
-
- t.is(h1, h2);
- t.is(h1, h3);
- t.is(h1, h4);
-
- // Doesn't contain salt
- t.false(ContractTermsUtil.validateForgettable(cReq));
-
- t.true(ContractTermsUtil.validateForgettable(c1));
- t.true(ContractTermsUtil.validateForgettable(c2));
- t.true(ContractTermsUtil.validateForgettable(c3));
- t.true(ContractTermsUtil.validateForgettable(c4));
-});
-
-test("contract terms reference vector", (t) => {
- const j = {
- k1: 1,
- $forgettable: {
- k1: "SALT",
- },
- k2: {
- n1: true,
- $forgettable: {
- n1: "salt",
- },
- },
- k3: {
- n1: "string",
- },
- };
-
- const h = ContractTermsUtil.hashContractTerms(j);
-
- t.deepEqual(
- h,
- "VDE8JPX0AEEE3EX1K8E11RYEWSZQKGGZCV6BWTE4ST1C8711P7H850Z7F2Q2HSSYETX87ERC2JNHWB7GTDWTDWMM716VKPSRBXD7SRR",
- );
-});
diff --git a/packages/taler-wallet-core/src/util/contractTerms.ts b/packages/taler-wallet-core/src/util/contractTerms.ts
deleted file mode 100644
index b064079e9..000000000
--- a/packages/taler-wallet-core/src/util/contractTerms.ts
+++ /dev/null
@@ -1,231 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-import { canonicalJson, Logger } from "@gnu-taler/taler-util";
-import { kdf } from "@gnu-taler/taler-util";
-import {
- decodeCrock,
- encodeCrock,
- getRandomBytes,
- hash,
- stringToBytes,
-} from "@gnu-taler/taler-util";
-
-const logger = new Logger("contractTerms.ts");
-
-export namespace ContractTermsUtil {
- export type PathPredicate = (path: string[]) => boolean;
-
- /**
- * Scrub all forgettable members from an object.
- */
- export function scrub(anyJson: any): any {
- return forgetAllImpl(anyJson, [], () => true);
- }
-
- /**
- * Recursively forget all forgettable members of an object,
- * where the path matches a predicate.
- */
- export function forgetAll(anyJson: any, pred: PathPredicate): any {
- return forgetAllImpl(anyJson, [], pred);
- }
-
- function forgetAllImpl(
- anyJson: any,
- path: string[],
- pred: PathPredicate,
- ): any {
- const dup = JSON.parse(JSON.stringify(anyJson));
- if (Array.isArray(dup)) {
- for (let i = 0; i < dup.length; i++) {
- dup[i] = forgetAllImpl(dup[i], [...path, `${i}`], pred);
- }
- } else if (typeof dup === "object" && dup != null) {
- if (typeof dup.$forgettable === "object") {
- for (const x of Object.keys(dup.$forgettable)) {
- if (!pred([...path, x])) {
- continue;
- }
- if (!dup.$forgotten) {
- dup.$forgotten = {};
- }
- if (!dup.$forgotten[x]) {
- const membValCanon = stringToBytes(
- canonicalJson(scrub(dup[x])) + "\0",
- );
- const membSalt = stringToBytes(dup.$forgettable[x] + "\0");
- const h = kdf(64, membValCanon, membSalt, new Uint8Array([]));
- dup.$forgotten[x] = encodeCrock(h);
- }
- delete dup[x];
- delete dup.$forgettable[x];
- }
- if (Object.keys(dup.$forgettable).length === 0) {
- delete dup.$forgettable;
- }
- }
- for (const x of Object.keys(dup)) {
- if (x.startsWith("$")) {
- continue;
- }
- dup[x] = forgetAllImpl(dup[x], [...path, x], pred);
- }
- }
- return dup;
- }
-
- /**
- * Generate a salt for all members marked as forgettable,
- * but which don't have an actual salt yet.
- */
- export function saltForgettable(anyJson: any): any {
- const dup = JSON.parse(JSON.stringify(anyJson));
- if (Array.isArray(dup)) {
- for (let i = 0; i < dup.length; i++) {
- dup[i] = saltForgettable(dup[i]);
- }
- } else if (typeof dup === "object" && dup !== null) {
- if (typeof dup.$forgettable === "object") {
- for (const k of Object.keys(dup.$forgettable)) {
- if (dup.$forgettable[k] === true) {
- dup.$forgettable[k] = encodeCrock(getRandomBytes(32));
- }
- }
- }
- for (const x of Object.keys(dup)) {
- if (x.startsWith("$")) {
- continue;
- }
- dup[x] = saltForgettable(dup[x]);
- }
- }
- return dup;
- }
-
- const nameRegex = /^[0-9A-Za-z_]+$/;
-
- /**
- * Check that the given JSON object is well-formed with regards
- * to forgettable fields and other restrictions for forgettable JSON.
- */
- export function validateForgettable(anyJson: any): boolean {
- if (typeof anyJson === "string") {
- return true;
- }
- if (typeof anyJson === "number") {
- return (
- Number.isInteger(anyJson) &&
- anyJson >= Number.MIN_SAFE_INTEGER &&
- anyJson <= Number.MAX_SAFE_INTEGER
- );
- }
- if (typeof anyJson === "boolean") {
- return true;
- }
- if (anyJson === null) {
- return true;
- }
- if (Array.isArray(anyJson)) {
- return anyJson.every((x) => validateForgettable(x));
- }
- if (typeof anyJson === "object") {
- for (const k of Object.keys(anyJson)) {
- if (k.match(nameRegex)) {
- if (validateForgettable(anyJson[k])) {
- continue;
- } else {
- return false;
- }
- }
- if (k === "$forgettable") {
- const fga = anyJson.$forgettable;
- if (!fga || typeof fga !== "object") {
- return false;
- }
- for (const fk of Object.keys(fga)) {
- if (!fk.match(nameRegex)) {
- return false;
- }
- if (!(fk in anyJson)) {
- return false;
- }
- const fv = anyJson.$forgettable[fk];
- if (typeof fv !== "string") {
- return false;
- }
- }
- } else if (k === "$forgotten") {
- const fgo = anyJson.$forgotten;
- if (!fgo || typeof fgo !== "object") {
- return false;
- }
- for (const fk of Object.keys(fgo)) {
- if (!fk.match(nameRegex)) {
- return false;
- }
- // Check that the value has actually been forgotten.
- if (fk in anyJson) {
- return false;
- }
- const fv = anyJson.$forgotten[fk];
- if (typeof fv !== "string") {
- return false;
- }
- try {
- const decFv = decodeCrock(fv);
- if (decFv.length != 64) {
- return false;
- }
- } catch (e) {
- return false;
- }
- // Check that salt has been deleted after forgetting.
- if (anyJson.$forgettable?.[k] !== undefined) {
- return false;
- }
- }
- } else {
- return false;
- }
- }
- return true;
- }
- return false;
- }
-
- /**
- * Check that no forgettable information has been forgotten.
- *
- * Must only be called on an object already validated with validateForgettable.
- */
- export function validateNothingForgotten(contractTerms: any): boolean {
- throw Error("not implemented yet");
- }
-
- /**
- * Hash a contract terms object. Forgettable fields
- * are scrubbed and JSON canonicalization is applied
- * before hashing.
- */
- export function hashContractTerms(contractTerms: unknown): string {
- const cleaned = scrub(contractTerms);
- const canon = canonicalJson(cleaned) + "\0";
- const bytes = stringToBytes(canon);
- logger.info(`contract terms before hashing: ${encodeCrock(bytes)}`);
- return encodeCrock(hash(bytes));
- }
-}
diff --git a/packages/taler-wallet-core/src/util/debugFlags.ts b/packages/taler-wallet-core/src/util/debugFlags.ts
deleted file mode 100644
index cea249d27..000000000
--- a/packages/taler-wallet-core/src/util/debugFlags.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along with
- GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
- */
-
-/**
- * Debug flags for wallet-core.
- *
- * @author Florian Dold
- */
-
-export interface WalletCoreDebugFlags {
- /**
- * Allow withdrawal of denominations even though they are about to expire.
- */
- denomselAllowLate: boolean;
-}
-
-export const walletCoreDebugFlags: WalletCoreDebugFlags = {
- denomselAllowLate: false,
-};
diff --git a/packages/taler-wallet-core/src/util/http.ts b/packages/taler-wallet-core/src/util/http.ts
deleted file mode 100644
index d01f2ee42..000000000
--- a/packages/taler-wallet-core/src/util/http.ts
+++ /dev/null
@@ -1,342 +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.
- *
- * The API is inspired by the HTML5 fetch API.
- */
-
-/**
- * Imports
- */
-import { OperationFailedError, makeErrorDetails } from "../errors.js";
-import {
- Logger,
- Duration,
- Timestamp,
- getTimestampNow,
- timestampAddDuration,
- timestampMax,
- TalerErrorDetails,
- Codec,
-} from "@gnu-taler/taler-util";
-import { TalerErrorCode } from "@gnu-taler/taler-util";
-
-const logger = new Logger("http.ts");
-
-/**
- * An HTTP response that is returned by all request methods of this library.
- */
-export interface HttpResponse {
- requestUrl: string;
- requestMethod: string;
- status: number;
- headers: Headers;
- json(): Promise<any>;
- text(): Promise<string>;
- bytes(): Promise<ArrayBuffer>;
-}
-
-export interface HttpRequestOptions {
- method?: "POST" | "PUT" | "GET";
- headers?: { [name: string]: string };
- timeout?: Duration;
- body?: string | ArrayBuffer | ArrayBufferView;
-}
-
-export enum HttpResponseStatus {
- Ok = 200,
- NoContent = 204,
- Gone = 210,
- NotModified = 304,
- BadRequest = 400,
- PaymentRequired = 402,
- NotFound = 404,
- Conflict = 409,
-}
-
-/**
- * Headers, roughly modeled after the fetch API's headers object.
- */
-export class Headers {
- private headerMap = new Map<string, string>();
-
- get(name: string): string | null {
- const r = this.headerMap.get(name.toLowerCase());
- if (r) {
- return r;
- }
- return null;
- }
-
- set(name: string, value: string): void {
- const normalizedName = name.toLowerCase();
- const existing = this.headerMap.get(normalizedName);
- if (existing !== undefined) {
- this.headerMap.set(normalizedName, existing + "," + value);
- } else {
- this.headerMap.set(normalizedName, value);
- }
- }
-
- toJSON(): any {
- const m: Record<string, string> = {};
- this.headerMap.forEach((v, k) => (m[k] = v));
- return m;
- }
-}
-
-/**
- * Interface for the HTTP request library used by the wallet.
- *
- * The request library is bundled into an interface to make mocking and
- * request tunneling easy.
- */
-export interface HttpRequestLibrary {
- /**
- * Make an HTTP 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>;
-
- /**
- * Make an HTTP POST request with a JSON body.
- */
- fetch(url: string, opt?: HttpRequestOptions): Promise<HttpResponse>;
-}
-
-type TalerErrorResponse = {
- code: number;
-} & unknown;
-
-type ResponseOrError<T> =
- | { isError: false; response: T }
- | { isError: true; talerErrorResponse: TalerErrorResponse };
-
-export async function readTalerErrorResponse(
- httpResponse: HttpResponse,
-): Promise<TalerErrorDetails> {
- 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,
- requestMethod: httpResponse.requestMethod,
- httpStatusCode: httpResponse.status,
- },
- ),
- );
- }
- return errJson;
-}
-
-export async function readUnexpectedResponseDetails(
- httpResponse: HttpResponse,
-): Promise<TalerErrorDetails> {
- const errJson = await httpResponse.json();
- const talerErrorCode = errJson.code;
- if (typeof talerErrorCode !== "number") {
- return makeErrorDetails(
- TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
- "Error response did not contain error code",
- {
- requestUrl: httpResponse.requestUrl,
- requestMethod: httpResponse.requestMethod,
- httpStatusCode: httpResponse.status,
- },
- );
- }
- return makeErrorDetails(
- TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
- "Unexpected error code in response",
- {
- requestUrl: httpResponse.requestUrl,
- httpStatusCode: httpResponse.status,
- errorResponse: errJson,
- },
- );
-}
-
-export async function readSuccessResponseJsonOrErrorCode<T>(
- httpResponse: HttpResponse,
- codec: Codec<T>,
-): Promise<ResponseOrError<T>> {
- if (!(httpResponse.status >= 200 && httpResponse.status < 300)) {
- return {
- isError: true,
- talerErrorResponse: await readTalerErrorResponse(httpResponse),
- };
- }
- const respJson = await httpResponse.json();
- let parsedResponse: T;
- try {
- parsedResponse = codec.decode(respJson);
- } catch (e: any) {
- 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 getHttpResponseErrorDetails(
- httpResponse: HttpResponse,
-): Record<string, unknown> {
- return {
- requestUrl: httpResponse.requestUrl,
- httpStatusCode: httpResponse.status,
- };
-}
-
-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",
- {
- httpStatusCode: httpResponse.status,
- requestUrl: httpResponse.requestUrl,
- requestMethod: httpResponse.requestMethod,
- },
- ),
- );
- }
- 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",
- {
- httpStatusCode: httpResponse.status,
- requestUrl: httpResponse.requestUrl,
- requestMethod: httpResponse.requestMethod,
- },
- ),
- );
- }
- throwUnexpectedRequestError(httpResponse, errJson);
- }
-}
-
-export async function readSuccessResponseTextOrThrow<T>(
- httpResponse: HttpResponse,
-): Promise<string> {
- const r = await readSuccessResponseTextOrErrorCode(httpResponse);
- if (!r.isError) {
- return r.response;
- }
- throwUnexpectedRequestError(httpResponse, r.talerErrorResponse);
-}
-
-/**
- * Get the timestamp at which the response's content is considered expired.
- */
-export function getExpiryTimestamp(
- httpResponse: HttpResponse,
- opt: { minDuration?: Duration },
-): Timestamp {
- const expiryDateMs = new Date(
- httpResponse.headers.get("expiry") ?? "",
- ).getTime();
- let t: Timestamp;
- if (Number.isNaN(expiryDateMs)) {
- t = getTimestampNow();
- } else {
- t = {
- t_ms: expiryDateMs,
- };
- }
- if (opt.minDuration) {
- const t2 = timestampAddDuration(getTimestampNow(), opt.minDuration);
- return timestampMax(t, t2);
- }
- return t;
-}
diff --git a/packages/taler-wallet-core/src/util/invariants.ts b/packages/taler-wallet-core/src/util/invariants.ts
deleted file mode 100644
index b788d044e..000000000
--- a/packages/taler-wallet-core/src/util/invariants.ts
+++ /dev/null
@@ -1,39 +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/>
- */
-
-/**
- * Helpers for invariants.
- */
-
-export function checkDbInvariant(b: boolean, m?: string): asserts b {
- if (!b) {
- if (m) {
- throw Error(`BUG: database invariant failed (${m})`);
- } else {
- throw Error("BUG: database invariant failed");
- }
- }
-}
-
-export function checkLogicInvariant(b: boolean, m?: string): asserts b {
- if (!b) {
- if (m) {
- throw Error(`BUG: logic invariant failed (${m})`);
- } else {
- throw Error("BUG: logic invariant failed");
- }
- }
-}
diff --git a/packages/taler-wallet-core/src/util/promiseUtils.ts b/packages/taler-wallet-core/src/util/promiseUtils.ts
deleted file mode 100644
index d409686d9..000000000
--- a/packages/taler-wallet-core/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/packages/taler-wallet-core/src/util/query.ts b/packages/taler-wallet-core/src/util/query.ts
deleted file mode 100644
index a95cbf1ff..000000000
--- a/packages/taler-wallet-core/src/util/query.ts
+++ /dev/null
@@ -1,615 +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.js";
-import {
- IDBRequest,
- IDBTransaction,
- IDBValidKey,
- IDBDatabase,
- IDBFactory,
- IDBVersionChangeEvent,
- IDBCursor,
- IDBKeyPath,
-} from "@gnu-taler/idb-bridge";
-import { Logger } from "@gnu-taler/taler-util";
-import { performanceNow } from "./timer.js";
-
-const logger = new Logger("query.ts");
-
-/**
- * Exception that should be thrown by client code to abort a transaction.
- */
-export const TransactionAbort = Symbol("transaction_abort");
-
-/**
- * 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;
-
- /**
- * Database version that this store was added in, or
- * undefined if added in the first version.
- */
- versionAdded?: number;
-}
-
-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.error("error in DB request", req.error);
- reject(req.error);
- console.error("Request failed:", stack);
- };
- });
-}
-
-type CursorResult<T> = CursorEmptyResult<T> | CursorValueResult<T>;
-
-interface CursorEmptyResult<T> {
- hasValue: false;
-}
-
-interface CursorValueResult<T> {
- hasValue: true;
- value: T;
-}
-
-class TransactionAbortedError extends Error {
- constructor(m: string) {
- super(m);
-
- // Set the prototype explicitly.
- Object.setPrototypeOf(this, TransactionAbortedError.prototype);
- }
-}
-
-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 };
- }
-}
-
-/**
- * Return a promise that resolves to the opened IndexedDB database.
- */
-export function openDatabase(
- idbFactory: IDBFactory,
- databaseName: string,
- databaseVersion: number,
- onVersionChange: () => void,
- onUpgradeNeeded: (
- db: IDBDatabase,
- oldVersion: number,
- newVersion: number,
- upgradeTransaction: IDBTransaction,
- ) => void,
-): Promise<IDBDatabase> {
- return new Promise<IDBDatabase>((resolve, reject) => {
- const req = idbFactory.open(databaseName, databaseVersion);
- req.onerror = (e) => {
- logger.error("database error", e);
- reject(new Error("database error"));
- };
- req.onsuccess = (e) => {
- req.result.onversionchange = (evt: IDBVersionChangeEvent) => {
- logger.info(
- `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");
- }
- const transaction = req.transaction;
- if (!transaction) {
- throw Error("no transaction handle available in upgrade handler");
- }
- onUpgradeNeeded(db, e.oldVersion, newVersion, transaction);
- };
- });
-}
-
-export interface IndexDescriptor {
- name: string;
- keyPath: IDBKeyPath | IDBKeyPath[];
- multiEntry?: boolean;
-}
-
-export interface StoreDescriptor<RecordType> {
- _dummy: undefined & RecordType;
- name: string;
- keyPath?: IDBKeyPath | IDBKeyPath[];
- autoIncrement?: boolean;
-}
-
-export interface StoreOptions {
- keyPath?: IDBKeyPath | IDBKeyPath[];
- autoIncrement?: boolean;
-}
-
-export function describeContents<RecordType = never>(
- name: string,
- options: StoreOptions,
-): StoreDescriptor<RecordType> {
- return { name, keyPath: options.keyPath, _dummy: undefined as any };
-}
-
-export function describeIndex(
- name: string,
- keyPath: IDBKeyPath | IDBKeyPath[],
- options: IndexOptions = {},
-): IndexDescriptor {
- return {
- keyPath,
- name,
- multiEntry: options.multiEntry,
- };
-}
-
-interface IndexReadOnlyAccessor<RecordType> {
- iter(query?: IDBValidKey): ResultStream<RecordType>;
- get(query: IDBValidKey): Promise<RecordType | undefined>;
- getAll(query: IDBValidKey, count?: number): Promise<RecordType[]>;
-}
-
-type GetIndexReadOnlyAccess<RecordType, IndexMap> = {
- [P in keyof IndexMap]: IndexReadOnlyAccessor<RecordType>;
-};
-
-interface IndexReadWriteAccessor<RecordType> {
- iter(query: IDBValidKey): ResultStream<RecordType>;
- get(query: IDBValidKey): Promise<RecordType | undefined>;
- getAll(query: IDBValidKey, count?: number): Promise<RecordType[]>;
-}
-
-type GetIndexReadWriteAccess<RecordType, IndexMap> = {
- [P in keyof IndexMap]: IndexReadWriteAccessor<RecordType>;
-};
-
-export interface StoreReadOnlyAccessor<RecordType, IndexMap> {
- get(key: IDBValidKey): Promise<RecordType | undefined>;
- iter(query?: IDBValidKey): ResultStream<RecordType>;
- indexes: GetIndexReadOnlyAccess<RecordType, IndexMap>;
-}
-
-export interface StoreReadWriteAccessor<RecordType, IndexMap> {
- get(key: IDBValidKey): Promise<RecordType | undefined>;
- iter(query?: IDBValidKey): ResultStream<RecordType>;
- put(r: RecordType): Promise<void>;
- add(r: RecordType): Promise<void>;
- delete(key: IDBValidKey): Promise<void>;
- indexes: GetIndexReadWriteAccess<RecordType, IndexMap>;
-}
-
-export interface StoreWithIndexes<
- SD extends StoreDescriptor<unknown>,
- IndexMap
-> {
- store: SD;
- indexMap: IndexMap;
-
- /**
- * Type marker symbol, to check that the descriptor
- * has been created through the right function.
- */
- mark: Symbol;
-}
-
-export type GetRecordType<T> = T extends StoreDescriptor<infer X> ? X : unknown;
-
-const storeWithIndexesSymbol = Symbol("StoreWithIndexesMark");
-
-export function describeStore<SD extends StoreDescriptor<unknown>, IndexMap>(
- s: SD,
- m: IndexMap,
-): StoreWithIndexes<SD, IndexMap> {
- return {
- store: s,
- indexMap: m,
- mark: storeWithIndexesSymbol,
- };
-}
-
-export type GetReadOnlyAccess<BoundStores> = {
- [P in keyof BoundStores]: BoundStores[P] extends StoreWithIndexes<
- infer SD,
- infer IM
- >
- ? StoreReadOnlyAccessor<GetRecordType<SD>, IM>
- : unknown;
-};
-
-export type GetReadWriteAccess<BoundStores> = {
- [P in keyof BoundStores]: BoundStores[P] extends StoreWithIndexes<
- infer SD,
- infer IM
- >
- ? StoreReadWriteAccessor<GetRecordType<SD>, IM>
- : unknown;
-};
-
-type ReadOnlyTransactionFunction<BoundStores, T> = (
- t: GetReadOnlyAccess<BoundStores>,
-) => Promise<T>;
-
-type ReadWriteTransactionFunction<BoundStores, T> = (
- t: GetReadWriteAccess<BoundStores>,
-) => Promise<T>;
-
-export interface TransactionContext<BoundStores> {
- runReadWrite<T>(f: ReadWriteTransactionFunction<BoundStores, T>): Promise<T>;
- runReadOnly<T>(f: ReadOnlyTransactionFunction<BoundStores, T>): Promise<T>;
-}
-
-type CheckDescriptor<T> = T extends StoreWithIndexes<infer SD, infer IM>
- ? StoreWithIndexes<SD, IM>
- : unknown;
-
-type GetPickerType<F, SM> = F extends (x: SM) => infer Out
- ? { [P in keyof Out]: CheckDescriptor<Out[P]> }
- : unknown;
-
-function runTx<Arg, Res>(
- tx: IDBTransaction,
- arg: Arg,
- f: (t: Arg) => Promise<Res>,
-): Promise<Res> {
- const stack = Error("Failed transaction was started here.");
- return new Promise((resolve, reject) => {
- let funResult: any = undefined;
- let gotFunResult = false;
- let transactionException: any = undefined;
- 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 = () => {
- logger.error("error in transaction");
- logger.error(`${stack}`);
- };
- tx.onabort = () => {
- let msg: string;
- if (tx.error) {
- msg = `Transaction aborted (transaction error): ${tx.error}`;
- } else if (transactionException !== undefined) {
- msg = `Transaction aborted (exception thrown): ${transactionException}`;
- } else {
- msg = "Transaction aborted (no DB error)";
- }
- logger.error(msg);
- reject(new TransactionAbortedError(msg));
- };
- const resP = Promise.resolve().then(() => f(arg));
- resP
- .then((result) => {
- gotFunResult = true;
- funResult = result;
- })
- .catch((e) => {
- if (e == TransactionAbort) {
- logger.trace("aborting transaction");
- } else {
- transactionException = e;
- console.error("Transaction failed:", e);
- console.error(stack);
- tx.abort();
- }
- })
- .catch((e) => {
- console.error("fatal: aborting transaction failed", e);
- });
- });
-}
-
-function makeReadContext(
- tx: IDBTransaction,
- storePick: { [n: string]: StoreWithIndexes<any, any> },
-): any {
- const ctx: { [s: string]: StoreReadOnlyAccessor<any, any> } = {};
- for (const storeAlias in storePick) {
- const indexes: { [s: string]: IndexReadOnlyAccessor<any> } = {};
- const swi = storePick[storeAlias];
- const storeName = swi.store.name;
- for (const indexAlias in storePick[storeAlias].indexMap) {
- const indexDescriptor: IndexDescriptor =
- storePick[storeAlias].indexMap[indexAlias];
- const indexName = indexDescriptor.name;
- indexes[indexAlias] = {
- get(key) {
- const req = tx.objectStore(storeName).index(indexName).get(key);
- return requestToPromise(req);
- },
- iter(query) {
- const req = tx
- .objectStore(storeName)
- .index(indexName)
- .openCursor(query);
- return new ResultStream<any>(req);
- },
- getAll(query, count) {
- const req = tx.objectStore(storeName).index(indexName).getAll(query, count);
- return requestToPromise(req);
- }
- };
- }
- ctx[storeAlias] = {
- indexes,
- get(key) {
- const req = tx.objectStore(storeName).get(key);
- return requestToPromise(req);
- },
- iter(query) {
- const req = tx.objectStore(storeName).openCursor(query);
- return new ResultStream<any>(req);
- },
- };
- }
- return ctx;
-}
-
-function makeWriteContext(
- tx: IDBTransaction,
- storePick: { [n: string]: StoreWithIndexes<any, any> },
-): any {
- const ctx: { [s: string]: StoreReadWriteAccessor<any, any> } = {};
- for (const storeAlias in storePick) {
- const indexes: { [s: string]: IndexReadWriteAccessor<any> } = {};
- const swi = storePick[storeAlias];
- const storeName = swi.store.name;
- for (const indexAlias in storePick[storeAlias].indexMap) {
- const indexDescriptor: IndexDescriptor =
- storePick[storeAlias].indexMap[indexAlias];
- const indexName = indexDescriptor.name;
- indexes[indexAlias] = {
- get(key) {
- const req = tx.objectStore(storeName).index(indexName).get(key);
- return requestToPromise(req);
- },
- iter(query) {
- const req = tx
- .objectStore(storeName)
- .index(indexName)
- .openCursor(query);
- return new ResultStream<any>(req);
- },
- getAll(query, count) {
- const req = tx.objectStore(storeName).index(indexName).getAll(query, count);
- return requestToPromise(req);
- }
- };
- }
- ctx[storeAlias] = {
- indexes,
- get(key) {
- const req = tx.objectStore(storeName).get(key);
- return requestToPromise(req);
- },
- iter(query) {
- const req = tx.objectStore(storeName).openCursor(query);
- return new ResultStream<any>(req);
- },
- add(r) {
- const req = tx.objectStore(storeName).add(r);
- return requestToPromise(req);
- },
- put(r) {
- const req = tx.objectStore(storeName).put(r);
- return requestToPromise(req);
- },
- delete(k) {
- const req = tx.objectStore(storeName).delete(k);
- return requestToPromise(req);
- },
- };
- }
- return ctx;
-}
-
-/**
- * Type-safe access to a database with a particular store map.
- *
- * A store map is the metadata that describes the store.
- */
-export class DbAccess<StoreMap> {
- constructor(private db: IDBDatabase, private stores: StoreMap) {}
-
- mktx<
- PickerType extends (x: StoreMap) => unknown,
- BoundStores extends GetPickerType<PickerType, StoreMap>
- >(f: PickerType): TransactionContext<BoundStores> {
- const storePick = f(this.stores) as any;
- if (typeof storePick !== "object" || storePick === null) {
- throw Error();
- }
- const storeNames: string[] = [];
- for (const storeAlias of Object.keys(storePick)) {
- const swi = (storePick as any)[storeAlias] as StoreWithIndexes<any, any>;
- if (swi.mark !== storeWithIndexesSymbol) {
- throw Error("invalid store descriptor returned from selector function");
- }
- storeNames.push(swi.store.name);
- }
-
- const runReadOnly = <T>(
- txf: ReadOnlyTransactionFunction<BoundStores, T>,
- ): Promise<T> => {
- const tx = this.db.transaction(storeNames, "readonly");
- const readContext = makeReadContext(tx, storePick);
- return runTx(tx, readContext, txf);
- };
-
- const runReadWrite = <T>(
- txf: ReadWriteTransactionFunction<BoundStores, T>,
- ): Promise<T> => {
- const tx = this.db.transaction(storeNames, "readwrite");
- const writeContext = makeWriteContext(tx, storePick);
- return runTx(tx, writeContext, txf);
- };
-
- return {
- runReadOnly,
- runReadWrite,
- };
- }
-}
diff --git a/packages/taler-wallet-core/src/util/retries.ts b/packages/taler-wallet-core/src/util/retries.ts
deleted file mode 100644
index cac7b1b52..000000000
--- a/packages/taler-wallet-core/src/util/retries.ts
+++ /dev/null
@@ -1,85 +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/>
- */
-
-/**
- * Helpers for dealing with retry timeouts.
- */
-
-/**
- * Imports.
- */
-import { Timestamp, Duration, getTimestampNow } from "@gnu-taler/taler-util";
-
-export interface RetryInfo {
- firstTry: Timestamp;
- nextRetry: Timestamp;
- retryCounter: number;
-}
-
-export interface RetryPolicy {
- readonly backoffDelta: Duration;
- readonly backoffBase: number;
-}
-
-const defaultRetryPolicy: RetryPolicy = {
- backoffBase: 1.5,
- backoffDelta: { d_ms: 200 },
-};
-
-export function updateRetryInfoTimeout(
- r: RetryInfo,
- p: RetryPolicy = defaultRetryPolicy,
-): void {
- const now = getTimestampNow();
- if (now.t_ms === "never") {
- throw Error("assertion failed");
- }
- if (p.backoffDelta.d_ms === "forever") {
- r.nextRetry = { t_ms: "never" };
- return;
- }
- const t =
- now.t_ms + p.backoffDelta.d_ms * Math.pow(p.backoffBase, r.retryCounter);
- r.nextRetry = { t_ms: t };
-}
-
-export function getRetryDuration(
- r: RetryInfo | undefined,
- p: RetryPolicy = defaultRetryPolicy,
-): Duration {
- if (!r) {
- // If we don't have any retry info, run immediately.
- return { d_ms: 0 };
- }
- if (p.backoffDelta.d_ms === "forever") {
- return { d_ms: "forever" };
- }
- const t = p.backoffDelta.d_ms * Math.pow(p.backoffBase, r.retryCounter);
- return { d_ms: t };
-}
-
-export function initRetryInfo(
- p: RetryPolicy = defaultRetryPolicy,
-): RetryInfo {
- const now = getTimestampNow();
- const info = {
- firstTry: now,
- nextRetry: now,
- retryCounter: 0,
- };
- updateRetryInfoTimeout(info, p);
- return info;
-}
diff --git a/packages/taler-wallet-core/src/util/timer.ts b/packages/taler-wallet-core/src/util/timer.ts
deleted file mode 100644
index d9fe3439b..000000000
--- a/packages/taler-wallet-core/src/util/timer.ts
+++ /dev/null
@@ -1,199 +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 { Logger, Duration } from "@gnu-taler/taler-util";
-
-const logger = new Logger("timer.ts");
-
-/**
- * Cancelable timer.
- */
-export interface TimerHandle {
- clear(): void;
-
- /**
- * Make sure the event loop exits when the timer is the
- * only event left. Has no effect in the browser.
- */
- unref(): void;
-}
-
-class IntervalHandle {
- constructor(public h: any) {}
-
- clear(): void {
- clearInterval(this.h);
- }
-
- /**
- * Make sure the event loop exits when the timer is the
- * only event left. Has no effect in the browser.
- */
- unref(): void {
- if (typeof this.h === "object") {
- this.h.unref();
- }
- }
-}
-
-class TimeoutHandle {
- constructor(public h: any) {}
-
- clear(): void {
- clearTimeout(this.h);
- }
-
- /**
- * Make sure the event loop exits when the timer is the
- * only event left. Has no effect in the browser.
- */
- unref(): void {
- if (typeof this.h === "object") {
- this.h.unref();
- }
- }
-}
-
-/**
- * Get a performance counter in nanoseconds.
- */
-export const performanceNow: () => bigint = (() => {
- // @ts-ignore
- if (typeof process !== "undefined" && process.hrtime) {
- return () => {
- return process.hrtime.bigint();
- };
- }
-
- // @ts-ignore
- if (typeof performance !== "undefined") {
- // @ts-ignore
- return () => BigInt(Math.floor(performance.now() * 1000)) * BigInt(1000);
- }
-
- return () => BigInt(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;
- },
- unref() {
- // 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];
- },
- unref() {
- h.unref();
- },
- };
- }
-
- every(delayMs: number, callback: () => void): TimerHandle {
- if (this.stopped) {
- logger.warn("dropping timer since timer group is stopped");
- return nullTimerHandle;
- }
- const h = every(delayMs, callback);
- const myId = this.idGen++;
- this.timerMap[myId] = h;
-
- const tm = this.timerMap;
-
- return {
- clear() {
- h.clear();
- delete tm[myId];
- },
- unref() {
- h.unref();
- },
- };
- }
-}