summaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
authorFlorian Dold <florian.dold@gmail.com>2020-08-03 13:00:48 +0530
committerFlorian Dold <florian.dold@gmail.com>2020-08-03 13:01:05 +0530
commitffd2a62c3f7df94365980302fef3bc3376b48182 (patch)
tree270af6f16b4cc7f5da2afdba55c8bc9dbea5eca5 /packages
parentaa481e42675fb7c4dcbbeec0ba1c61e1953b9596 (diff)
downloadwallet-core-ffd2a62c3f7df94365980302fef3bc3376b48182.tar.gz
wallet-core-ffd2a62c3f7df94365980302fef3bc3376b48182.tar.bz2
wallet-core-ffd2a62c3f7df94365980302fef3bc3376b48182.zip
modularize repo, use pnpm, improve typechecking
Diffstat (limited to 'packages')
-rw-r--r--packages/README2
-rw-r--r--packages/idb-bridge/.vscode/settings.json4
-rw-r--r--packages/idb-bridge/package.json22
-rw-r--r--packages/idb-bridge/rollup.config.js31
-rw-r--r--packages/idb-bridge/src/BridgeIDBCursor.ts15
-rw-r--r--packages/idb-bridge/src/BridgeIDBCursorWithValue.ts6
-rw-r--r--packages/idb-bridge/src/BridgeIDBDatabase.ts21
-rw-r--r--packages/idb-bridge/src/BridgeIDBFactory.ts12
-rw-r--r--packages/idb-bridge/src/BridgeIDBIndex.ts22
-rw-r--r--packages/idb-bridge/src/BridgeIDBKeyRange.ts4
-rw-r--r--packages/idb-bridge/src/BridgeIDBObjectStore.ts18
-rw-r--r--packages/idb-bridge/src/BridgeIDBOpenDBRequest.ts26
-rw-r--r--packages/idb-bridge/src/BridgeIDBRequest.ts14
-rw-r--r--packages/idb-bridge/src/BridgeIDBTransaction.ts13
-rw-r--r--packages/idb-bridge/src/BridgeIDBVersionChangeEvent.ts40
-rw-r--r--packages/idb-bridge/src/MemoryBackend.test.ts34
-rw-r--r--packages/idb-bridge/src/MemoryBackend.ts10
-rw-r--r--packages/idb-bridge/src/backend-interface.ts21
-rw-r--r--packages/idb-bridge/src/idbtypes.ts736
-rw-r--r--packages/idb-bridge/src/index.ts21
-rw-r--r--packages/idb-bridge/src/tree/b+tree.ts1070
-rw-r--r--packages/idb-bridge/src/tree/interfaces.ts287
-rw-r--r--packages/idb-bridge/src/util/FakeEvent.ts103
-rw-r--r--packages/idb-bridge/src/util/FakeEventTarget.ts2
-rw-r--r--packages/idb-bridge/src/util/cmp.ts146
-rw-r--r--packages/idb-bridge/src/util/enforceRange.ts20
-rw-r--r--packages/idb-bridge/src/util/errors.ts159
-rw-r--r--packages/idb-bridge/src/util/getIndexKeys.test.ts28
-rw-r--r--packages/idb-bridge/src/util/injectKey.ts2
-rw-r--r--packages/idb-bridge/src/util/makeStoreKeyValue.test.ts26
-rw-r--r--packages/idb-bridge/src/util/makeStoreKeyValue.ts5
-rw-r--r--packages/idb-bridge/src/util/queueTask.ts8
-rw-r--r--packages/idb-bridge/src/util/structuredClone.ts3
-rw-r--r--packages/idb-bridge/src/util/types.ts21
-rw-r--r--packages/idb-bridge/src/util/validateKeyPath.ts98
-rw-r--r--packages/idb-bridge/src/util/valueToKey.ts1
-rw-r--r--packages/idb-bridge/tsconfig.json13
-rw-r--r--packages/idb-bridge/yarn.lock2689
-rw-r--r--packages/pogen/package.json2
-rw-r--r--packages/pogen/tsconfig.json3
-rw-r--r--packages/taler-wallet-android/package.json44
-rw-r--r--packages/taler-wallet-android/rollup.config.js30
-rw-r--r--packages/taler-wallet-android/src/index.d.ts26
-rw-r--r--packages/taler-wallet-android/src/index.js284
-rw-r--r--packages/taler-wallet-android/src/index.js.map1
-rw-r--r--packages/taler-wallet-android/src/index.ts285
-rw-r--r--packages/taler-wallet-android/tsconfig.json32
-rwxr-xr-xpackages/taler-wallet-cli/bin/taler-wallet-cli7
-rw-r--r--packages/taler-wallet-cli/package.json48
-rw-r--r--packages/taler-wallet-cli/rollup.config.js30
-rw-r--r--packages/taler-wallet-cli/src/clk.ts614
-rw-r--r--packages/taler-wallet-cli/src/index.ts640
-rw-r--r--packages/taler-wallet-cli/tsconfig.json30
-rw-r--r--packages/taler-wallet-core/.gitignore1
-rw-r--r--packages/taler-wallet-core/package.json81
-rw-r--r--packages/taler-wallet-core/rollup.config.js31
-rw-r--r--packages/taler-wallet-core/src/TalerErrorCode.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/TalerErrorCode.ts3097
-rw-r--r--packages/taler-wallet-core/src/crypto/primitives/kdf.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/crypto/primitives/kdf.ts92
-rw-r--r--packages/taler-wallet-core/src/crypto/primitives/nacl-fast.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/crypto/primitives/nacl-fast.ts1941
-rw-r--r--packages/taler-wallet-core/src/crypto/primitives/sha256.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/crypto/primitives/sha256.ts426
-rw-r--r--packages/taler-wallet-core/src/crypto/talerCrypto-test.ts197
-rw-r--r--packages/taler-wallet-core/src/crypto/talerCrypto.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/crypto/talerCrypto.ts391
-rw-r--r--packages/taler-wallet-core/src/crypto/workers/cryptoApi.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/crypto/workers/cryptoApi.ts446
-rw-r--r--packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts579
-rw-r--r--packages/taler-wallet-core/src/crypto/workers/cryptoWorker.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/crypto/workers/cryptoWorker.ts8
-rw-r--r--packages/taler-wallet-core/src/crypto/workers/nodeThreadWorker.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/crypto/workers/nodeThreadWorker.ts183
-rw-r--r--packages/taler-wallet-core/src/crypto/workers/synchronousWorker.ts136
-rw-r--r--packages/taler-wallet-core/src/db.ts66
-rw-r--r--packages/taler-wallet-core/src/headless/NodeHttpLib.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/headless/NodeHttpLib.ts133
-rw-r--r--packages/taler-wallet-core/src/headless/helpers.ts135
-rw-r--r--packages/taler-wallet-core/src/i18n/de.po363
-rw-r--r--packages/taler-wallet-core/src/i18n/en-US.po294
-rw-r--r--packages/taler-wallet-core/src/i18n/fr.po290
-rw-r--r--packages/taler-wallet-core/src/i18n/index.ts78
-rw-r--r--packages/taler-wallet-core/src/i18n/it.po290
-rw-r--r--packages/taler-wallet-core/src/i18n/poheader26
-rw-r--r--packages/taler-wallet-core/src/i18n/strings-prelude17
-rw-r--r--packages/taler-wallet-core/src/i18n/strings.ts373
-rw-r--r--packages/taler-wallet-core/src/i18n/sv.po388
-rw-r--r--packages/taler-wallet-core/src/i18n/taler-wallet-webex.pot290
-rw-r--r--packages/taler-wallet-core/src/index.ts75
-rw-r--r--packages/taler-wallet-core/src/operations/balance.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/operations/balance.ts153
-rw-r--r--packages/taler-wallet-core/src/operations/errors.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/operations/errors.ts121
-rw-r--r--packages/taler-wallet-core/src/operations/exchanges.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/operations/exchanges.ts555
-rw-r--r--packages/taler-wallet-core/src/operations/pay.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/operations/pay.ts1148
-rw-r--r--packages/taler-wallet-core/src/operations/pending.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/operations/pending.ts458
-rw-r--r--packages/taler-wallet-core/src/operations/recoup.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/operations/recoup.ts412
-rw-r--r--packages/taler-wallet-core/src/operations/refresh.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/operations/refresh.ts573
-rw-r--r--packages/taler-wallet-core/src/operations/refund.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/operations/refund.ts438
-rw-r--r--packages/taler-wallet-core/src/operations/reserves.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/operations/reserves.ts841
-rw-r--r--packages/taler-wallet-core/src/operations/state.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/operations/state.ts65
-rw-r--r--packages/taler-wallet-core/src/operations/testing.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/operations/testing.ts156
-rw-r--r--packages/taler-wallet-core/src/operations/tip.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/operations/tip.ts343
-rw-r--r--packages/taler-wallet-core/src/operations/transactions.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/operations/transactions.ts288
-rw-r--r--packages/taler-wallet-core/src/operations/versions.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/operations/versions.ts38
-rw-r--r--packages/taler-wallet-core/src/operations/withdraw-test.ts332
-rw-r--r--packages/taler-wallet-core/src/operations/withdraw.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/operations/withdraw.ts759
-rw-r--r--packages/taler-wallet-core/src/types/ReserveStatus.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/types/ReserveStatus.ts57
-rw-r--r--packages/taler-wallet-core/src/types/ReserveTransaction.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/types/ReserveTransaction.ts250
-rw-r--r--packages/taler-wallet-core/src/types/dbTypes.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/types/dbTypes.ts1819
-rw-r--r--packages/taler-wallet-core/src/types/notifications.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/types/notifications.ts255
-rw-r--r--packages/taler-wallet-core/src/types/pending.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/types/pending.ts258
-rw-r--r--packages/taler-wallet-core/src/types/schemacore.ts58
-rw-r--r--packages/taler-wallet-core/src/types/talerTypes.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/types/talerTypes.ts1272
-rw-r--r--packages/taler-wallet-core/src/types/transactions.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/types/transactions.ts314
-rw-r--r--packages/taler-wallet-core/src/types/types-test.ts55
-rw-r--r--packages/taler-wallet-core/src/types/walletTypes.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/types/walletTypes.ts516
-rw-r--r--packages/taler-wallet-core/src/util/RequestThrottler.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/util/RequestThrottler.ts129
-rw-r--r--packages/taler-wallet-core/src/util/amounts-test.ts140
-rw-r--r--packages/taler-wallet-core/src/util/amounts.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/util/amounts.ts384
-rw-r--r--packages/taler-wallet-core/src/util/assertUnreachable.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/util/assertUnreachable.ts19
-rw-r--r--packages/taler-wallet-core/src/util/asyncMemo.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/util/asyncMemo.ts87
-rw-r--r--packages/taler-wallet-core/src/util/codec-test.ts78
-rw-r--r--packages/taler-wallet-core/src/util/codec.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/util/codec.ts406
-rw-r--r--packages/taler-wallet-core/src/util/helpers-test.ts46
-rw-r--r--packages/taler-wallet-core/src/util/helpers.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/util/helpers.ts148
-rw-r--r--packages/taler-wallet-core/src/util/http.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/util/http.ts237
-rw-r--r--packages/taler-wallet-core/src/util/libtoolVersion-test.ts48
-rw-r--r--packages/taler-wallet-core/src/util/libtoolVersion.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/util/libtoolVersion.ts88
-rw-r--r--packages/taler-wallet-core/src/util/logging.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/util/logging.ts89
-rw-r--r--packages/taler-wallet-core/src/util/payto-test.ts31
-rw-r--r--packages/taler-wallet-core/src/util/payto.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/util/payto.ts71
-rw-r--r--packages/taler-wallet-core/src/util/promiseUtils.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/util/promiseUtils.ts60
-rw-r--r--packages/taler-wallet-core/src/util/query.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/util/query.ts576
-rw-r--r--packages/taler-wallet-core/src/util/reserveHistoryUtil-test.ts285
-rw-r--r--packages/taler-wallet-core/src/util/reserveHistoryUtil.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/util/reserveHistoryUtil.ts360
-rw-r--r--packages/taler-wallet-core/src/util/talerconfig.ts120
-rw-r--r--packages/taler-wallet-core/src/util/taleruri-test.ts184
-rw-r--r--packages/taler-wallet-core/src/util/taleruri.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/util/taleruri.ts215
-rw-r--r--packages/taler-wallet-core/src/util/testvectors.ts36
-rw-r--r--packages/taler-wallet-core/src/util/time.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/util/time.ts198
-rw-r--r--packages/taler-wallet-core/src/util/timer.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/util/timer.ts165
-rw-r--r--packages/taler-wallet-core/src/util/url.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/util/url.ts74
-rw-r--r--packages/taler-wallet-core/src/util/wire.ts51
-rw-r--r--packages/taler-wallet-core/src/wallet-test.ts121
-rw-r--r--packages/taler-wallet-core/src/wallet.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/wallet.ts882
-rw-r--r--packages/taler-wallet-core/src/walletCoreApiHandler.d.ts.map1
-rw-r--r--packages/taler-wallet-core/src/walletCoreApiHandler.ts318
-rw-r--r--packages/taler-wallet-core/tsconfig.json31
-rw-r--r--packages/taler-wallet-webextension/package.json41
-rw-r--r--packages/taler-wallet-webextension/rollup.config.js212
-rw-r--r--packages/taler-wallet-webextension/src/background.ts30
-rw-r--r--packages/taler-wallet-webextension/src/browserCryptoWorkerFactory.js44
-rw-r--r--packages/taler-wallet-webextension/src/browserCryptoWorkerFactory.js.map1
-rw-r--r--packages/taler-wallet-webextension/src/browserCryptoWorkerFactory.ts43
-rw-r--r--packages/taler-wallet-webextension/src/browserHttpLib.ts129
-rw-r--r--packages/taler-wallet-webextension/src/browserWorkerEntry.ts74
-rw-r--r--packages/taler-wallet-webextension/src/chromeBadge.ts288
-rw-r--r--packages/taler-wallet-webextension/src/compat.js61
-rw-r--r--packages/taler-wallet-webextension/src/compat.ts85
-rw-r--r--packages/taler-wallet-webextension/src/i18n-test.tsx68
-rw-r--r--packages/taler-wallet-webextension/src/i18n.tsx206
-rw-r--r--packages/taler-wallet-webextension/src/pageEntryPoint.ts72
-rw-r--r--packages/taler-wallet-webextension/src/pages/pay.tsx180
-rw-r--r--packages/taler-wallet-webextension/src/pages/payback.tsx30
-rw-r--r--packages/taler-wallet-webextension/src/pages/popup.tsx502
-rw-r--r--packages/taler-wallet-webextension/src/pages/refund.tsx89
-rw-r--r--packages/taler-wallet-webextension/src/pages/reset-required.tsx93
-rw-r--r--packages/taler-wallet-webextension/src/pages/return-coins.tsx30
-rw-r--r--packages/taler-wallet-webextension/src/pages/tip.tsx103
-rw-r--r--packages/taler-wallet-webextension/src/pages/welcome.tsx190
-rw-r--r--packages/taler-wallet-webextension/src/pages/withdraw.tsx229
-rw-r--r--packages/taler-wallet-webextension/src/permissions.ts20
-rw-r--r--packages/taler-wallet-webextension/src/renderHtml.tsx341
-rw-r--r--packages/taler-wallet-webextension/src/wxApi.ts239
-rw-r--r--packages/taler-wallet-webextension/src/wxBackend.ts566
-rw-r--r--packages/taler-wallet-webextension/tsconfig.json19
-rw-r--r--packages/taler-wallet-webextension/webextension/manifest.json49
-rwxr-xr-xpackages/taler-wallet-webextension/webextension/pack.sh23
-rw-r--r--packages/taler-wallet-webextension/webextension/static/add-auditor.html33
-rw-r--r--packages/taler-wallet-webextension/webextension/static/auditors.html35
-rw-r--r--packages/taler-wallet-webextension/webextension/static/background.html11
-rw-r--r--packages/taler-wallet-webextension/webextension/static/benchmark.html16
-rw-r--r--packages/taler-wallet-webextension/webextension/static/img/icon.pngbin0 -> 830 bytes
-rw-r--r--packages/taler-wallet-webextension/webextension/static/img/logo-2015-medium.pngbin0 -> 65674 bytes
l---------packages/taler-wallet-webextension/webextension/static/img/logo.png1
-rw-r--r--packages/taler-wallet-webextension/webextension/static/img/spinner-bars.svg53
-rw-r--r--packages/taler-wallet-webextension/webextension/static/pay.html73
-rw-r--r--packages/taler-wallet-webextension/webextension/static/payback.html34
-rw-r--r--packages/taler-wallet-webextension/webextension/static/popup.html15
-rw-r--r--packages/taler-wallet-webextension/webextension/static/refund.html19
-rw-r--r--packages/taler-wallet-webextension/webextension/static/reset-required.html25
-rw-r--r--packages/taler-wallet-webextension/webextension/static/return-coins.html16
-rw-r--r--packages/taler-wallet-webextension/webextension/static/style/popup.css185
-rw-r--r--packages/taler-wallet-webextension/webextension/static/style/pure.css1513
-rw-r--r--packages/taler-wallet-webextension/webextension/static/style/wallet.css290
-rw-r--r--packages/taler-wallet-webextension/webextension/static/tip.html19
-rw-r--r--packages/taler-wallet-webextension/webextension/static/welcome.html24
-rw-r--r--packages/taler-wallet-webextension/webextension/static/withdraw.html22
240 files changed, 38317 insertions, 3643 deletions
diff --git a/packages/README b/packages/README
deleted file mode 100644
index c06e9941a..000000000
--- a/packages/README
+++ /dev/null
@@ -1,2 +0,0 @@
-This directory contains NPM packages that are used by the wallet. They are
-uploaded to the registry.
diff --git a/packages/idb-bridge/.vscode/settings.json b/packages/idb-bridge/.vscode/settings.json
deleted file mode 100644
index ec71f9aae..000000000
--- a/packages/idb-bridge/.vscode/settings.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "editor.tabSize": 2,
- "typescript.tsdk": "node_modules/typescript/lib"
-} \ No newline at end of file
diff --git a/packages/idb-bridge/package.json b/packages/idb-bridge/package.json
index b0aa74a71..b2c22921f 100644
--- a/packages/idb-bridge/package.json
+++ b/packages/idb-bridge/package.json
@@ -2,17 +2,31 @@
"name": "idb-bridge",
"version": "0.0.15",
"description": "IndexedDB implementation that uses SQLite3 as storage",
- "main": "./build/index.js",
- "types": "./build/index.d.ts",
+ "main": "./dist/idb-bridge.js",
+ "module": "./lib/index.js",
+ "types": "./lib/index.d.ts",
"author": "Florian Dold",
"license": "AGPL-3.0-or-later",
"private": false,
"scripts": {
"test": "tsc && ava",
- "build": "tsc"
+ "compile": "tsc && rollup -c",
+ "clean": "rimraf dist lib",
+ "pretty": "prettier --config ../../.prettierrc --write src"
},
"devDependencies": {
- "ava": "2.3.0",
+ "@rollup/plugin-typescript": "^5.0.2",
+ "ava": "^3.10.1",
+ "esm": "^3.2.25",
+ "rimraf": "^3.0.2",
+ "rollup": "^2.23.0",
"typescript": "^3.7.0"
+ },
+ "dependencies": {
+ "@types/node": "^14.0.27",
+ "tslib": "^2.0.0"
+ },
+ "ava": {
+ "require": ["esm"]
}
}
diff --git a/packages/idb-bridge/rollup.config.js b/packages/idb-bridge/rollup.config.js
new file mode 100644
index 000000000..76214f22d
--- /dev/null
+++ b/packages/idb-bridge/rollup.config.js
@@ -0,0 +1,31 @@
+// rollup.config.js
+import commonjs from "@rollup/plugin-commonjs";
+import nodeResolve from "@rollup/plugin-node-resolve";
+import json from "@rollup/plugin-json";
+import builtins from "builtin-modules";
+import pkg from "./package.json";
+
+export default {
+ input: "lib/index.js",
+ output: {
+ file: pkg.main,
+ format: "cjs",
+ sourcemap: true
+ },
+ external: builtins,
+ plugins: [
+ nodeResolve({
+ preferBuiltins: true,
+ }),
+
+ commonjs({
+ include: [/node_modules/],
+ extensions: [".js", ".ts"],
+ ignoreGlobal: false,
+ sourceMap: false,
+ }),
+
+ json(),
+ ],
+}
+
diff --git a/packages/idb-bridge/src/BridgeIDBCursor.ts b/packages/idb-bridge/src/BridgeIDBCursor.ts
index ebf71da0d..a85f41f2b 100644
--- a/packages/idb-bridge/src/BridgeIDBCursor.ts
+++ b/packages/idb-bridge/src/BridgeIDBCursor.ts
@@ -16,9 +16,9 @@
permissions and limitations under the License.
*/
-import BridgeIDBKeyRange from "./BridgeIDBKeyRange";
-import BridgeIDBObjectStore from "./BridgeIDBObjectStore";
-import BridgeIDBRequest from "./BridgeIDBRequest";
+import { BridgeIDBKeyRange } from "./BridgeIDBKeyRange";
+import { BridgeIDBObjectStore } from "./BridgeIDBObjectStore";
+import { BridgeIDBRequest } from "./BridgeIDBRequest";
import compareKeys from "./util/cmp";
import {
DataError,
@@ -42,7 +42,7 @@ import {
RecordStoreRequest,
StoreLevel,
} from "./backend-interface";
-import BridgeIDBFactory from "./BridgeIDBFactory";
+import { BridgeIDBFactory } from "./BridgeIDBFactory";
/**
* http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#cursor
@@ -136,7 +136,8 @@ export class BridgeIDBCursor {
console.log(
`iterating cursor os=${this._objectStoreName},idx=${this._indexName}`,
);
- BridgeIDBFactory.enableTracing && console.log("cursor type ", this.toString());
+ BridgeIDBFactory.enableTracing &&
+ console.log("cursor type ", this.toString());
const recordGetRequest: RecordGetRequest = {
direction: this.direction,
indexName: this._indexName,
@@ -232,7 +233,7 @@ export class BridgeIDBCursor {
const operation = async () => {
if (BridgeIDBFactory.enableTracing) {
- console.log("updating at cursor")
+ console.log("updating at cursor");
}
const { btx } = this.source._confirmActiveTransaction();
await this._backend.storeRecord(btx, storeReq);
@@ -359,5 +360,3 @@ export class BridgeIDBCursor {
return "[object IDBCursor]";
}
}
-
-export default BridgeIDBCursor;
diff --git a/packages/idb-bridge/src/BridgeIDBCursorWithValue.ts b/packages/idb-bridge/src/BridgeIDBCursorWithValue.ts
index d75bd21e6..8561879cf 100644
--- a/packages/idb-bridge/src/BridgeIDBCursorWithValue.ts
+++ b/packages/idb-bridge/src/BridgeIDBCursorWithValue.ts
@@ -14,7 +14,7 @@
permissions and limitations under the License.
*/
-import BridgeIDBCursor from "./BridgeIDBCursor";
+import { BridgeIDBCursor } from "./BridgeIDBCursor";
import {
CursorRange,
CursorSource,
@@ -22,7 +22,7 @@ import {
Value,
} from "./util/types";
-class BridgeIDBCursorWithValue extends BridgeIDBCursor {
+export class BridgeIDBCursorWithValue extends BridgeIDBCursor {
get value(): Value {
return this._value;
}
@@ -46,5 +46,3 @@ class BridgeIDBCursorWithValue extends BridgeIDBCursor {
return "[object IDBCursorWithValue]";
}
}
-
-export default BridgeIDBCursorWithValue;
diff --git a/packages/idb-bridge/src/BridgeIDBDatabase.ts b/packages/idb-bridge/src/BridgeIDBDatabase.ts
index bc2e8acca..a22ad5400 100644
--- a/packages/idb-bridge/src/BridgeIDBDatabase.ts
+++ b/packages/idb-bridge/src/BridgeIDBDatabase.ts
@@ -14,7 +14,7 @@
* permissions and limitations under the License.
*/
-import BridgeIDBTransaction from "./BridgeIDBTransaction";
+import { BridgeIDBTransaction } from "./BridgeIDBTransaction";
import {
ConstraintError,
InvalidAccessError,
@@ -61,9 +61,8 @@ const confirmActiveVersionchangeTransaction = (database: BridgeIDBDatabase) => {
return transaction;
};
-
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#database-interface
-class BridgeIDBDatabase extends FakeEventTarget {
+export class BridgeIDBDatabase extends FakeEventTarget {
_closePending = false;
_closed = false;
_runningVersionchangeTransaction = false;
@@ -152,7 +151,12 @@ class BridgeIDBDatabase extends FakeEventTarget {
throw new InvalidAccessError();
}
- transaction._backend.createObjectStore(backendTx, name, keyPath, autoIncrement);
+ transaction._backend.createObjectStore(
+ backendTx,
+ name,
+ keyPath,
+ autoIncrement,
+ );
this._schema = this._backend.getSchema(this._backendConnection);
@@ -212,7 +216,12 @@ class BridgeIDBDatabase extends FakeEventTarget {
}
}
- const tx = new BridgeIDBTransaction(storeNames, mode, this, backendTransaction);
+ const tx = new BridgeIDBTransaction(
+ storeNames,
+ mode,
+ this,
+ backendTransaction,
+ );
this._transactions.push(tx);
queueTask(() => tx._start());
return tx;
@@ -236,5 +245,3 @@ class BridgeIDBDatabase extends FakeEventTarget {
return "[object IDBDatabase]";
}
}
-
-export default BridgeIDBDatabase;
diff --git a/packages/idb-bridge/src/BridgeIDBFactory.ts b/packages/idb-bridge/src/BridgeIDBFactory.ts
index 0fbcd7630..7002222d8 100644
--- a/packages/idb-bridge/src/BridgeIDBFactory.ts
+++ b/packages/idb-bridge/src/BridgeIDBFactory.ts
@@ -15,9 +15,9 @@
* permissions and limitations under the License.
*/
-import BridgeIDBDatabase from "./BridgeIDBDatabase";
-import BridgeIDBOpenDBRequest from "./BridgeIDBOpenDBRequest";
-import BridgeIDBVersionChangeEvent from "./BridgeIDBVersionChangeEvent";
+import { BridgeIDBDatabase } from "./BridgeIDBDatabase";
+import { BridgeIDBOpenDBRequest } from "./BridgeIDBOpenDBRequest";
+import { BridgeIDBVersionChangeEvent } from "./BridgeIDBVersionChangeEvent";
import compareKeys from "./util/cmp";
import enforceRange from "./util/enforceRange";
import { AbortError, VersionError } from "./util/errors";
@@ -44,7 +44,7 @@ export class BridgeIDBFactory {
queueTask(async () => {
const databases = await this.backend.getDatabases();
- const dbInfo = databases.find(x => x.name == name);
+ const dbInfo = databases.find((x) => x.name == name);
if (!dbInfo) {
// Database already doesn't exist, success!
const event = new BridgeIDBVersionChangeEvent("success", {
@@ -219,8 +219,6 @@ export class BridgeIDBFactory {
}
private _anyOpen(): boolean {
- return this.connections.some(c => !c._closed && !c._closePending);
+ return this.connections.some((c) => !c._closed && !c._closePending);
}
}
-
-export default BridgeIDBFactory;
diff --git a/packages/idb-bridge/src/BridgeIDBIndex.ts b/packages/idb-bridge/src/BridgeIDBIndex.ts
index 5be80ba71..3c1b39435 100644
--- a/packages/idb-bridge/src/BridgeIDBIndex.ts
+++ b/packages/idb-bridge/src/BridgeIDBIndex.ts
@@ -15,11 +15,11 @@
permissions and limitations under the License.
*/
-import BridgeIDBCursor from "./BridgeIDBCursor";
-import BridgeIDBCursorWithValue from "./BridgeIDBCursorWithValue";
-import BridgeIDBKeyRange from "./BridgeIDBKeyRange";
-import BridgeIDBObjectStore from "./BridgeIDBObjectStore";
-import BridgeIDBRequest from "./BridgeIDBRequest";
+import { BridgeIDBCursor } from "./BridgeIDBCursor";
+import { BridgeIDBCursorWithValue } from "./BridgeIDBCursorWithValue";
+import { BridgeIDBKeyRange } from "./BridgeIDBKeyRange";
+import { BridgeIDBObjectStore } from "./BridgeIDBObjectStore";
+import { BridgeIDBRequest } from "./BridgeIDBRequest";
import {
ConstraintError,
InvalidStateError,
@@ -27,7 +27,7 @@ import {
} from "./util/errors";
import { BridgeIDBCursorDirection, Key, KeyPath } from "./util/types";
import valueToKey from "./util/valueToKey";
-import BridgeIDBTransaction from "./BridgeIDBTransaction";
+import { BridgeIDBTransaction } from "./BridgeIDBTransaction";
import {
Schema,
Backend,
@@ -59,15 +59,18 @@ export class BridgeIDBIndex {
}
get keyPath(): KeyPath {
- return this._schema.objectStores[this.objectStore.name].indexes[this._name].keyPath;
+ return this._schema.objectStores[this.objectStore.name].indexes[this._name]
+ .keyPath;
}
get multiEntry(): boolean {
- return this._schema.objectStores[this.objectStore.name].indexes[this._name].multiEntry;
+ return this._schema.objectStores[this.objectStore.name].indexes[this._name]
+ .multiEntry;
}
get unique(): boolean {
- return this._schema.objectStores[this.objectStore.name].indexes[this._name].unique;
+ return this._schema.objectStores[this.objectStore.name].indexes[this._name]
+ .unique;
}
get _backend(): Backend {
@@ -305,7 +308,6 @@ export class BridgeIDBIndex {
operation,
source: this,
});
-
}
public toString() {
diff --git a/packages/idb-bridge/src/BridgeIDBKeyRange.ts b/packages/idb-bridge/src/BridgeIDBKeyRange.ts
index 4055e092a..43f59fb14 100644
--- a/packages/idb-bridge/src/BridgeIDBKeyRange.ts
+++ b/packages/idb-bridge/src/BridgeIDBKeyRange.ts
@@ -21,7 +21,7 @@ import { Key } from "./util/types";
import valueToKey from "./util/valueToKey";
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#range-concept
-class BridgeIDBKeyRange {
+export class BridgeIDBKeyRange {
public static only(value: Key) {
if (arguments.length === 0) {
throw new TypeError();
@@ -128,5 +128,3 @@ class BridgeIDBKeyRange {
return BridgeIDBKeyRange.only(key);
}
}
-
-export default BridgeIDBKeyRange;
diff --git a/packages/idb-bridge/src/BridgeIDBObjectStore.ts b/packages/idb-bridge/src/BridgeIDBObjectStore.ts
index bb84b4454..a32c29275 100644
--- a/packages/idb-bridge/src/BridgeIDBObjectStore.ts
+++ b/packages/idb-bridge/src/BridgeIDBObjectStore.ts
@@ -15,12 +15,12 @@
permissions and limitations under the License.
*/
-import BridgeIDBCursor from "./BridgeIDBCursor";
-import BridgeIDBCursorWithValue from "./BridgeIDBCursorWithValue";
-import BridgeIDBIndex from "./BridgeIDBIndex";
-import BridgeIDBKeyRange from "./BridgeIDBKeyRange";
-import BridgeIDBRequest from "./BridgeIDBRequest";
-import BridgeIDBTransaction from "./BridgeIDBTransaction";
+import { BridgeIDBCursor } from "./BridgeIDBCursor";
+import { BridgeIDBCursorWithValue } from "./BridgeIDBCursorWithValue";
+import { BridgeIDBIndex } from "./BridgeIDBIndex";
+import { BridgeIDBKeyRange } from "./BridgeIDBKeyRange";
+import { BridgeIDBRequest } from "./BridgeIDBRequest";
+import { BridgeIDBTransaction } from "./BridgeIDBTransaction";
import {
ConstraintError,
@@ -44,10 +44,10 @@ import {
ResultLevel,
StoreLevel,
} from "./backend-interface";
-import BridgeIDBFactory from "./BridgeIDBFactory";
+import { BridgeIDBFactory } from "./BridgeIDBFactory";
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#object-store
-class BridgeIDBObjectStore {
+export class BridgeIDBObjectStore {
_indexesCache: Map<string, BridgeIDBIndex> = new Map();
transaction: BridgeIDBTransaction;
@@ -455,5 +455,3 @@ class BridgeIDBObjectStore {
return "[object IDBObjectStore]";
}
}
-
-export default BridgeIDBObjectStore;
diff --git a/packages/idb-bridge/src/BridgeIDBOpenDBRequest.ts b/packages/idb-bridge/src/BridgeIDBOpenDBRequest.ts
index 7b636193f..71a6495e5 100644
--- a/packages/idb-bridge/src/BridgeIDBOpenDBRequest.ts
+++ b/packages/idb-bridge/src/BridgeIDBOpenDBRequest.ts
@@ -15,22 +15,20 @@
permissions and limitations under the License.
*/
-import BridgeIDBRequest from "./BridgeIDBRequest";
+import { BridgeIDBRequest } from "./BridgeIDBRequest";
import { EventCallback } from "./util/types";
-class BridgeIDBOpenDBRequest extends BridgeIDBRequest {
- public onupgradeneeded: EventCallback | null = null;
- public onblocked: EventCallback | null = null;
+export class BridgeIDBOpenDBRequest extends BridgeIDBRequest {
+ public onupgradeneeded: EventCallback | null = null;
+ public onblocked: EventCallback | null = null;
- constructor() {
- super();
- // https://www.w3.org/TR/IndexedDB/#open-requests
- this.source = null;
- }
+ constructor() {
+ super();
+ // https://www.w3.org/TR/IndexedDB/#open-requests
+ this.source = null;
+ }
- public toString() {
- return "[object IDBOpenDBRequest]";
- }
+ public toString() {
+ return "[object IDBOpenDBRequest]";
+ }
}
-
-export default BridgeIDBOpenDBRequest;
diff --git a/packages/idb-bridge/src/BridgeIDBRequest.ts b/packages/idb-bridge/src/BridgeIDBRequest.ts
index 1a6bdf501..3ed2f9244 100644
--- a/packages/idb-bridge/src/BridgeIDBRequest.ts
+++ b/packages/idb-bridge/src/BridgeIDBRequest.ts
@@ -15,19 +15,19 @@
* permissions and limitations under the License.
*/
-import BridgeFDBCursor from "./BridgeIDBCursor";
-import BridgeIDBIndex from "./BridgeIDBIndex";
-import BridgeIDBObjectStore from "./BridgeIDBObjectStore";
-import BridgeIDBTransaction from "./BridgeIDBTransaction";
+import { BridgeIDBCursor as BridgeFIBCursor } from "./BridgeIDBCursor";
+import { BridgeIDBIndex } from "./BridgeIDBIndex";
+import { BridgeIDBObjectStore } from "./BridgeIDBObjectStore";
+import { BridgeIDBTransaction } from "./BridgeIDBTransaction";
import { InvalidStateError } from "./util/errors";
import FakeEventTarget from "./util/FakeEventTarget";
import { EventCallback } from "./util/types";
import FakeEvent from "./util/FakeEvent";
-class BridgeIDBRequest extends FakeEventTarget {
+export class BridgeIDBRequest extends FakeEventTarget {
_result: any = null;
_error: Error | null | undefined = null;
- source: BridgeFDBCursor | BridgeIDBIndex | BridgeIDBObjectStore | null = null;
+ source: BridgeFIBCursor | BridgeIDBIndex | BridgeIDBObjectStore | null = null;
transaction: BridgeIDBTransaction | null = null;
readyState: "done" | "pending" = "pending";
onsuccess: EventCallback | null = null;
@@ -83,5 +83,3 @@ class BridgeIDBRequest extends FakeEventTarget {
this.dispatchEvent(event);
}
}
-
-export default BridgeIDBRequest;
diff --git a/packages/idb-bridge/src/BridgeIDBTransaction.ts b/packages/idb-bridge/src/BridgeIDBTransaction.ts
index 250e27149..56a4d59ef 100644
--- a/packages/idb-bridge/src/BridgeIDBTransaction.ts
+++ b/packages/idb-bridge/src/BridgeIDBTransaction.ts
@@ -1,6 +1,6 @@
-import BridgeIDBDatabase from "./BridgeIDBDatabase";
-import BridgeIDBObjectStore from "./BridgeIDBObjectStore";
-import BridgeIDBRequest from "./BridgeIDBRequest";
+import { BridgeIDBDatabase } from "./BridgeIDBDatabase";
+import { BridgeIDBObjectStore } from "./BridgeIDBObjectStore";
+import { BridgeIDBRequest } from "./BridgeIDBRequest";
import {
AbortError,
InvalidStateError,
@@ -19,11 +19,10 @@ import {
import queueTask from "./util/queueTask";
import openPromise from "./util/openPromise";
import { DatabaseTransaction, Backend } from "./backend-interface";
-import { array } from "prop-types";
-import BridgeIDBFactory from "./BridgeIDBFactory";
+import { BridgeIDBFactory } from "./BridgeIDBFactory";
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#transaction
-class BridgeIDBTransaction extends FakeEventTarget {
+export class BridgeIDBTransaction extends FakeEventTarget {
public _state: "active" | "inactive" | "committing" | "finished" = "active";
public _started = false;
public _objectStoresCache: Map<string, BridgeIDBObjectStore> = new Map();
@@ -328,5 +327,3 @@ class BridgeIDBTransaction extends FakeEventTarget {
return this._waitPromise;
}
}
-
-export default BridgeIDBTransaction;
diff --git a/packages/idb-bridge/src/BridgeIDBVersionChangeEvent.ts b/packages/idb-bridge/src/BridgeIDBVersionChangeEvent.ts
index 6fc63ee35..43e822d86 100644
--- a/packages/idb-bridge/src/BridgeIDBVersionChangeEvent.ts
+++ b/packages/idb-bridge/src/BridgeIDBVersionChangeEvent.ts
@@ -17,25 +17,23 @@
import FakeEvent from "./util/FakeEvent";
-class BridgeIDBVersionChangeEvent extends FakeEvent {
- public newVersion: number | null;
- public oldVersion: number;
-
- constructor(
- type: "blocked" | "success" | "upgradeneeded" | "versionchange",
- parameters: { newVersion?: number | null; oldVersion?: number } = {},
- ) {
- super(type);
-
- this.newVersion =
- parameters.newVersion !== undefined ? parameters.newVersion : null;
- this.oldVersion =
- parameters.oldVersion !== undefined ? parameters.oldVersion : 0;
- }
-
- public toString() {
- return "[object IDBVersionChangeEvent]";
- }
+export class BridgeIDBVersionChangeEvent extends FakeEvent {
+ public newVersion: number | null;
+ public oldVersion: number;
+
+ constructor(
+ type: "blocked" | "success" | "upgradeneeded" | "versionchange",
+ parameters: { newVersion?: number | null; oldVersion?: number } = {},
+ ) {
+ super(type);
+
+ this.newVersion =
+ parameters.newVersion !== undefined ? parameters.newVersion : null;
+ this.oldVersion =
+ parameters.oldVersion !== undefined ? parameters.oldVersion : 0;
+ }
+
+ public toString() {
+ return "[object IDBVersionChangeEvent]";
+ }
}
-
-export default BridgeIDBVersionChangeEvent;
diff --git a/packages/idb-bridge/src/MemoryBackend.test.ts b/packages/idb-bridge/src/MemoryBackend.test.ts
index a48fafb1e..21325379d 100644
--- a/packages/idb-bridge/src/MemoryBackend.test.ts
+++ b/packages/idb-bridge/src/MemoryBackend.test.ts
@@ -14,15 +14,14 @@
permissions and limitations under the License.
*/
-
import test from "ava";
import MemoryBackend from "./MemoryBackend";
-import BridgeIDBFactory from "./BridgeIDBFactory";
-import BridgeIDBRequest from "./BridgeIDBRequest";
-import BridgeIDBDatabase from "./BridgeIDBDatabase";
-import BridgeIDBTransaction from "./BridgeIDBTransaction";
-import BridgeIDBKeyRange from "./BridgeIDBKeyRange";
-import BridgeIDBCursorWithValue from "./BridgeIDBCursorWithValue";
+import { BridgeIDBFactory } from "./BridgeIDBFactory";
+import { BridgeIDBRequest } from "./BridgeIDBRequest";
+import { BridgeIDBDatabase } from "./BridgeIDBDatabase";
+import { BridgeIDBTransaction } from "./BridgeIDBTransaction";
+import { BridgeIDBKeyRange } from "./BridgeIDBKeyRange";
+import { BridgeIDBCursorWithValue } from "./BridgeIDBCursorWithValue";
function promiseFromRequest(request: BridgeIDBRequest): Promise<any> {
return new Promise((resolve, reject) => {
@@ -48,7 +47,7 @@ function promiseFromTransaction(
});
}
-test("Spec: Example 1 Part 1", async t => {
+test("Spec: Example 1 Part 1", async (t) => {
const backend = new MemoryBackend();
const idb = new BridgeIDBFactory(backend);
@@ -69,7 +68,7 @@ test("Spec: Example 1 Part 1", async t => {
t.pass();
});
-test("Spec: Example 1 Part 2", async t => {
+test("Spec: Example 1 Part 2", async (t) => {
const backend = new MemoryBackend();
const idb = new BridgeIDBFactory(backend);
@@ -101,7 +100,7 @@ test("Spec: Example 1 Part 2", async t => {
t.pass();
});
-test("Spec: Example 1 Part 3", async t => {
+test("Spec: Example 1 Part 3", async (t) => {
const backend = new MemoryBackend();
const idb = new BridgeIDBFactory(backend);
@@ -229,7 +228,6 @@ test("Spec: Example 1 Part 3", async t => {
cursor = request6.result;
t.is(cursor, null);
-
const request7 = index5.openCursor(null, "prevunique");
await promiseFromRequest(request7);
cursor = request7.result;
@@ -251,8 +249,7 @@ test("Spec: Example 1 Part 3", async t => {
t.pass();
});
-
-test("simple deletion", async t => {
+test("simple deletion", async (t) => {
const backend = new MemoryBackend();
const idb = new BridgeIDBFactory(backend);
@@ -307,8 +304,7 @@ test("simple deletion", async t => {
t.pass();
});
-
-test("export", async t => {
+test("export", async (t) => {
const backend = new MemoryBackend();
const idb = new BridgeIDBFactory(backend);
@@ -322,7 +318,6 @@ test("export", async t => {
const db: BridgeIDBDatabase = await promiseFromRequest(request);
-
const tx = db.transaction("books", "readwrite");
tx.oncomplete = () => {
console.log("oncomplete called");
@@ -341,10 +336,13 @@ test("export", async t => {
backend2.importDump(exportedData);
const exportedData2 = backend2.exportDump();
- t.assert(exportedData.databases["library"].objectStores["books"].records.length === 3);
+ t.assert(
+ exportedData.databases["library"].objectStores["books"].records.length ===
+ 3,
+ );
t.deepEqual(exportedData, exportedData2);
t.is(exportedData.databases["library"].schema.databaseVersion, 42);
t.is(exportedData2.databases["library"].schema.databaseVersion, 42);
t.pass();
-}); \ No newline at end of file
+});
diff --git a/packages/idb-bridge/src/MemoryBackend.ts b/packages/idb-bridge/src/MemoryBackend.ts
index 08103d722..c5fac41a9 100644
--- a/packages/idb-bridge/src/MemoryBackend.ts
+++ b/packages/idb-bridge/src/MemoryBackend.ts
@@ -40,7 +40,7 @@ import { Key, Value, KeyPath } from "./util/types";
import { StoreKeyResult, makeStoreKeyValue } from "./util/makeStoreKeyValue";
import getIndexKeys from "./util/getIndexKeys";
import openPromise from "./util/openPromise";
-import BridgeIDBKeyRange from "./BridgeIDBKeyRange";
+import { BridgeIDBKeyRange } from "./BridgeIDBKeyRange";
enum TransactionLevel {
Disconnected = 0,
@@ -863,9 +863,9 @@ export class MemoryBackend implements Backend {
!db.txRestrictObjectStores.includes(objectStoreName)
) {
throw Error(
- `Not allowed to access store '${
- objectStoreName
- }', transaction is over ${JSON.stringify(db.txRestrictObjectStores)}`,
+ `Not allowed to access store '${objectStoreName}', transaction is over ${JSON.stringify(
+ db.txRestrictObjectStores,
+ )}`,
);
}
if (typeof range !== "object") {
@@ -986,7 +986,7 @@ export class MemoryBackend implements Backend {
throw Error("db inconsistent: expected index entry missing");
}
const newPrimaryKeys = existingRecord.primaryKeys.filter(
- x => compareKeys(x, primaryKey) !== 0,
+ (x) => compareKeys(x, primaryKey) !== 0,
);
if (newPrimaryKeys.length === 0) {
index.modifiedData = indexData.without(indexKey);
diff --git a/packages/idb-bridge/src/backend-interface.ts b/packages/idb-bridge/src/backend-interface.ts
index bcb1e8a85..3617d21bc 100644
--- a/packages/idb-bridge/src/backend-interface.ts
+++ b/packages/idb-bridge/src/backend-interface.ts
@@ -22,7 +22,7 @@ import {
KeyPath,
BridgeIDBDatabaseInfo,
} from "./util/types";
-import BridgeIDBKeyRange from "./BridgeIDBKeyRange";
+import { BridgeIDBKeyRange } from "./BridgeIDBKeyRange";
export interface ObjectStoreProperties {
keyPath: KeyPath | null;
@@ -76,7 +76,7 @@ export interface RecordGetRequest {
* Last cursor position in terms of the index key.
* Can only be specified if indexName is defined and
* lastObjectStorePosition is defined.
- *
+ *
* Must either be undefined or within range.
*/
lastIndexPosition?: Key;
@@ -87,7 +87,7 @@ export interface RecordGetRequest {
/**
* If specified, the index key of the results must be
* greater or equal to advanceIndexKey.
- *
+ *
* Only applicable if indexName is specified.
*/
advanceIndexKey?: Key;
@@ -144,7 +144,7 @@ export interface Backend {
/**
* Even though the standard interface for indexedDB doesn't require
* the client to run deleteDatabase in a version transaction, there is
- * implicitly one running.
+ * implicitly one running.
*/
deleteDatabase(btx: DatabaseTransaction, name: string): Promise<void>;
@@ -152,9 +152,18 @@ export interface Backend {
getSchema(db: DatabaseConnection): Schema;
- renameIndex(btx: DatabaseTransaction, objectStoreName: string, oldName: string, newName: string): void;
+ renameIndex(
+ btx: DatabaseTransaction,
+ objectStoreName: string,
+ oldName: string,
+ newName: string,
+ ): void;
- deleteIndex(btx: DatabaseTransaction, objectStoreName: string, indexName: string): void;
+ deleteIndex(
+ btx: DatabaseTransaction,
+ objectStoreName: string,
+ indexName: string,
+ ): void;
rollback(btx: DatabaseTransaction): Promise<void>;
diff --git a/packages/idb-bridge/src/idbtypes.ts b/packages/idb-bridge/src/idbtypes.ts
new file mode 100644
index 000000000..53f93d451
--- /dev/null
+++ b/packages/idb-bridge/src/idbtypes.ts
@@ -0,0 +1,736 @@
+/*! *****************************************************************************
+Copyright (c) Microsoft Corporation. All rights reserved.
+Licensed under the Apache License, Version 2.0 (the "License"); you may not use
+this file except in compliance with the License. You may obtain a copy of the
+License at http://www.apache.org/licenses/LICENSE-2.0
+
+THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
+WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
+MERCHANTABLITY OR NON-INFRINGEMENT.
+
+See the Apache Version 2.0 License for specific language governing permissions
+and limitations under the License.
+***************************************************************************** */
+
+/**
+ * Type declarations for IndexedDB, adapted from the TypeScript lib.dom.d.ts.
+ *
+ * Instead of ambient types, we export type declarations.
+ */
+
+export type IDBKeyPath = string;
+
+export interface EventListener {
+ (evt: Event): void;
+}
+
+export interface EventListenerObject {
+ handleEvent(evt: Event): void;
+}
+
+export interface EventListenerOptions {
+ capture?: boolean;
+}
+
+export interface AddEventListenerOptions extends EventListenerOptions {
+ once?: boolean;
+ passive?: boolean;
+}
+
+export type IDBTransactionMode = "readonly" | "readwrite" | "versionchange";
+
+export type EventListenerOrEventListenerObject =
+ | EventListener
+ | EventListenerObject;
+
+/** EventTarget is a DOM interface implemented by objects that can receive events and may have listeners for them. */
+export interface EventTarget {
+ /**
+ * Appends an event listener for events whose type attribute value is type. The callback argument sets the callback that will be invoked when the event is dispatched.
+ *
+ * The options argument sets listener-specific options. For compatibility this can be a boolean, in which case the method behaves exactly as if the value was specified as options's capture.
+ *
+ * When set to true, options's capture prevents callback from being invoked when the event's eventPhase attribute value is BUBBLING_PHASE. When false (or not present), callback will not be invoked when event's eventPhase attribute value is CAPTURING_PHASE. Either way, callback will be invoked if event's eventPhase attribute value is AT_TARGET.
+ *
+ * When set to true, options's passive indicates that the callback will not cancel the event by invoking preventDefault(). This is used to enable performance optimizations described in § 2.8 Observing event listeners.
+ *
+ * When set to true, options's once indicates that the callback will only be invoked once after which the event listener will be removed.
+ *
+ * The event listener is appended to target's event listener list and is not appended if it has the same type, callback, and capture.
+ */
+ addEventListener(
+ type: string,
+ listener: EventListenerOrEventListenerObject | null,
+ options?: boolean | AddEventListenerOptions,
+ ): void;
+ /**
+ * Dispatches a synthetic event event to target and returns true if either event's cancelable attribute value is false or its preventDefault() method was not invoked, and false otherwise.
+ */
+ dispatchEvent(event: Event): boolean;
+ /**
+ * Removes the event listener in target's event listener list with the same type, callback, and options.
+ */
+ removeEventListener(
+ type: string,
+ callback: EventListenerOrEventListenerObject | null,
+ options?: EventListenerOptions | boolean,
+ ): void;
+}
+
+/** An event which takes place in the DOM. */
+export interface Event {
+ /**
+ * Returns true or false depending on how event was initialized. True if event goes through its target's ancestors in reverse tree order, and false otherwise.
+ */
+ readonly bubbles: boolean;
+ cancelBubble: boolean;
+ /**
+ * Returns true or false depending on how event was initialized. Its return value does not always carry meaning, but true can indicate that part of the operation during which event was dispatched, can be canceled by invoking the preventDefault() method.
+ */
+ readonly cancelable: boolean;
+ /**
+ * Returns true or false depending on how event was initialized. True if event invokes listeners past a ShadowRoot node that is the root of its target, and false otherwise.
+ */
+ readonly composed: boolean;
+ /**
+ * Returns the object whose event listener's callback is currently being invoked.
+ */
+ readonly currentTarget: EventTarget | null;
+ /**
+ * Returns true if preventDefault() was invoked successfully to indicate cancelation, and false otherwise.
+ */
+ readonly defaultPrevented: boolean;
+ /**
+ * Returns the event's phase, which is one of NONE, CAPTURING_PHASE, AT_TARGET, and BUBBLING_PHASE.
+ */
+ readonly eventPhase: number;
+ /**
+ * Returns true if event was dispatched by the user agent, and false otherwise.
+ */
+ readonly isTrusted: boolean;
+ returnValue: boolean;
+ /** @deprecated */
+ readonly srcElement: EventTarget | null;
+ /**
+ * Returns the object to which event is dispatched (its target).
+ */
+ readonly target: EventTarget | null;
+ /**
+ * Returns the event's timestamp as the number of milliseconds measured relative to the time origin.
+ */
+ readonly timeStamp: number;
+ /**
+ * Returns the type of event, e.g. "click", "hashchange", or "submit".
+ */
+ readonly type: string;
+ /**
+ * Returns the invocation target objects of event's path (objects on which listeners will be invoked), except for any nodes in shadow trees of which the shadow root's mode is "closed" that are not reachable from event's currentTarget.
+ */
+ composedPath(): EventTarget[];
+ initEvent(type: string, bubbles?: boolean, cancelable?: boolean): void;
+ /**
+ * If invoked when the cancelable attribute value is true, and while executing a listener for the event with passive set to false, signals to the operation that caused event to be dispatched that it needs to be canceled.
+ */
+ preventDefault(): void;
+ /**
+ * Invoking this method prevents event from reaching any registered event listeners after the current one finishes running and, when dispatched in a tree, also prevents event from reaching any other objects.
+ */
+ stopImmediatePropagation(): void;
+ /**
+ * When dispatched in a tree, invoking this method prevents event from reaching any objects other than the current object.
+ */
+ stopPropagation(): void;
+ readonly AT_TARGET: number;
+ readonly BUBBLING_PHASE: number;
+ readonly CAPTURING_PHASE: number;
+ readonly NONE: number;
+}
+
+/** A type returned by some APIs which contains a list of DOMString (strings). */
+export interface DOMStringList {
+ /**
+ * Returns the number of strings in strings.
+ */
+ readonly length: number;
+ /**
+ * Returns true if strings contains string, and false otherwise.
+ */
+ contains(string: string): boolean;
+ /**
+ * Returns the string with index index from strings.
+ */
+ item(index: number): string | null;
+ [index: number]: string;
+}
+
+export type BufferSource = ArrayBufferView | ArrayBuffer;
+
+export type IDBValidKey = number | string | Date | BufferSource | IDBArrayKey;
+
+export interface IDBIndexParameters {
+ multiEntry?: boolean;
+ unique?: boolean;
+}
+
+export interface IDBObjectStoreParameters {
+ autoIncrement?: boolean;
+ keyPath?: string | string[] | null;
+}
+
+export interface EventInit {
+ bubbles?: boolean;
+ cancelable?: boolean;
+ composed?: boolean;
+}
+
+export interface IDBArrayKey extends Array<IDBValidKey> {}
+
+export type IDBCursorDirection = "next" | "nextunique" | "prev" | "prevunique";
+
+/** This IndexedDB API interface represents a cursor for traversing or iterating over multiple records in a database. */
+export interface IDBCursor {
+ /**
+ * Returns the direction ("next", "nextunique", "prev" or "prevunique") of the cursor.
+ */
+ readonly direction: IDBCursorDirection;
+ /**
+ * Returns the key of the cursor. Throws a "InvalidStateError" DOMException if the cursor is advancing or is finished.
+ */
+ readonly key: IDBValidKey;
+ /**
+ * Returns the effective key of the cursor. Throws a "InvalidStateError" DOMException if the cursor is advancing or is finished.
+ */
+ readonly primaryKey: IDBValidKey;
+ /**
+ * Returns the IDBObjectStore or IDBIndex the cursor was opened from.
+ */
+ readonly source: IDBObjectStore | IDBIndex;
+ /**
+ * Advances the cursor through the next count records in range.
+ */
+ advance(count: number): void;
+ /**
+ * Advances the cursor to the next record in range.
+ */
+ continue(key?: IDBValidKey): void;
+ /**
+ * Advances the cursor to the next record in range matching or after key and primaryKey. Throws an "InvalidAccessError" DOMException if the source is not an index.
+ */
+ continuePrimaryKey(key: IDBValidKey, primaryKey: IDBValidKey): void;
+ /**
+ * Delete the record pointed at by the cursor with a new value.
+ *
+ * If successful, request's result will be undefined.
+ */
+ delete(): IDBRequest<undefined>;
+ /**
+ * Updated the record pointed at by the cursor with a new value.
+ *
+ * Throws a "DataError" DOMException if the effective object store uses in-line keys and the key would have changed.
+ *
+ * If successful, request's result will be the record's key.
+ */
+ update(value: any): IDBRequest<IDBValidKey>;
+}
+
+/** This IndexedDB API interface represents a cursor for traversing or iterating over multiple records in a database. It is the same as the IDBCursor, except that it includes the value property. */
+export interface IDBCursorWithValue extends IDBCursor {
+ /**
+ * Returns the cursor's current value.
+ */
+ readonly value: any;
+}
+
+export interface IDBDatabaseEventMap {
+ abort: Event;
+ close: Event;
+ error: Event;
+ versionchange: IDBVersionChangeEvent;
+}
+
+/** This IndexedDB API interface provides a connection to a database; you can use an IDBDatabase object to open a transaction on your database then create, manipulate, and delete objects (data) in that database. The interface provides the only way to get and manage versions of the database. */
+export interface IDBDatabase extends EventTarget {
+ /**
+ * Returns the name of the database.
+ */
+ readonly name: string;
+ /**
+ * Returns a list of the names of object stores in the database.
+ */
+ readonly objectStoreNames: DOMStringList;
+ onabort: ((this: IDBDatabase, ev: Event) => any) | null;
+ onclose: ((this: IDBDatabase, ev: Event) => any) | null;
+ onerror: ((this: IDBDatabase, ev: Event) => any) | null;
+ onversionchange:
+ | ((this: IDBDatabase, ev: IDBVersionChangeEvent) => any)
+ | null;
+ /**
+ * Returns the version of the database.
+ */
+ readonly version: number;
+ /**
+ * Closes the connection once all running transactions have finished.
+ */
+ close(): void;
+ /**
+ * Creates a new object store with the given name and options and returns a new IDBObjectStore.
+ *
+ * Throws a "InvalidStateError" DOMException if not called within an upgrade transaction.
+ */
+ createObjectStore(
+ name: string,
+ optionalParameters?: IDBObjectStoreParameters,
+ ): IDBObjectStore;
+ /**
+ * Deletes the object store with the given name.
+ *
+ * Throws a "InvalidStateError" DOMException if not called within an upgrade transaction.
+ */
+ deleteObjectStore(name: string): void;
+ /**
+ * Returns a new transaction with the given mode ("readonly" or "readwrite") and scope which can be a single object store name or an array of names.
+ */
+ transaction(
+ storeNames: string | string[],
+ mode?: IDBTransactionMode,
+ ): IDBTransaction;
+ addEventListener<K extends keyof IDBDatabaseEventMap>(
+ type: K,
+ listener: (this: IDBDatabase, ev: IDBDatabaseEventMap[K]) => any,
+ options?: boolean | AddEventListenerOptions,
+ ): void;
+ addEventListener(
+ type: string,
+ listener: EventListenerOrEventListenerObject,
+ options?: boolean | AddEventListenerOptions,
+ ): void;
+ removeEventListener<K extends keyof IDBDatabaseEventMap>(
+ type: K,
+ listener: (this: IDBDatabase, ev: IDBDatabaseEventMap[K]) => any,
+ options?: boolean | EventListenerOptions,
+ ): void;
+ removeEventListener(
+ type: string,
+ listener: EventListenerOrEventListenerObject,
+ options?: boolean | EventListenerOptions,
+ ): void;
+}
+
+/** In the following code snippet, we make a request to open a database, and include handlers for the success and error cases. For a full working example, see our To-do Notifications app (view example live.) */
+export interface IDBFactory {
+ /**
+ * Compares two values as keys. Returns -1 if key1 precedes key2, 1 if key2 precedes key1, and 0 if the keys are equal.
+ *
+ * Throws a "DataError" DOMException if either input is not a valid key.
+ */
+ cmp(first: any, second: any): number;
+ /**
+ * Attempts to delete the named database. If the database already exists and there are open connections that don't close in response to a versionchange event, the request will be blocked until all they close. If the request is successful request's result will be null.
+ */
+ deleteDatabase(name: string): IDBOpenDBRequest;
+ /**
+ * Attempts to open a connection to the named database with the current version, or 1 if it does not already exist. If the request is successful request's result will be the connection.
+ */
+ open(name: string, version?: number): IDBOpenDBRequest;
+}
+
+/** IDBIndex interface of the IndexedDB API provides asynchronous access to an index in a database. An index is a kind of object store for looking up records in another object store, called the referenced object store. You use this interface to retrieve data. */
+export interface IDBIndex {
+ readonly keyPath: string | string[];
+ readonly multiEntry: boolean;
+ /**
+ * Returns the name of the index.
+ */
+ name: string;
+ /**
+ * Returns the IDBObjectStore the index belongs to.
+ */
+ readonly objectStore: IDBObjectStore;
+ readonly unique: boolean;
+ /**
+ * Retrieves the number of records matching the given key or key range in query.
+ *
+ * If successful, request's result will be the count.
+ */
+ count(key?: IDBValidKey | IDBKeyRange): IDBRequest<number>;
+ /**
+ * Retrieves the value of the first record matching the given key or key range in query.
+ *
+ * If successful, request's result will be the value, or undefined if there was no matching record.
+ */
+ get(key: IDBValidKey | IDBKeyRange): IDBRequest<any | undefined>;
+ /**
+ * Retrieves the values of the records matching the given key or key range in query (up to count if given).
+ *
+ * If successful, request's result will be an Array of the values.
+ */
+ getAll(
+ query?: IDBValidKey | IDBKeyRange | null,
+ count?: number,
+ ): IDBRequest<any[]>;
+ /**
+ * Retrieves the keys of records matching the given key or key range in query (up to count if given).
+ *
+ * If successful, request's result will be an Array of the keys.
+ */
+ getAllKeys(
+ query?: IDBValidKey | IDBKeyRange | null,
+ count?: number,
+ ): IDBRequest<IDBValidKey[]>;
+ /**
+ * Retrieves the key of the first record matching the given key or key range in query.
+ *
+ * If successful, request's result will be the key, or undefined if there was no matching record.
+ */
+ getKey(key: IDBValidKey | IDBKeyRange): IDBRequest<IDBValidKey | undefined>;
+ /**
+ * Opens a cursor over the records matching query, ordered by direction. If query is null, all records in index are matched.
+ *
+ * If successful, request's result will be an IDBCursorWithValue, or null if there were no matching records.
+ */
+ openCursor(
+ query?: IDBValidKey | IDBKeyRange | null,
+ direction?: IDBCursorDirection,
+ ): IDBRequest<IDBCursorWithValue | null>;
+ /**
+ * Opens a cursor with key only flag set over the records matching query, ordered by direction. If query is null, all records in index are matched.
+ *
+ * If successful, request's result will be an IDBCursor, or null if there were no matching records.
+ */
+ openKeyCursor(
+ query?: IDBValidKey | IDBKeyRange | null,
+ direction?: IDBCursorDirection,
+ ): IDBRequest<IDBCursor | null>;
+}
+
+/** A key range can be a single value or a range with upper and lower bounds or endpoints. If the key range has both upper and lower bounds, then it is bounded; if it has no bounds, it is unbounded. A bounded key range can either be open (the endpoints are excluded) or closed (the endpoints are included). To retrieve all keys within a certain range, you can use the following code constructs: */
+export interface IDBKeyRange {
+ /**
+ * Returns lower bound, or undefined if none.
+ */
+ readonly lower: any;
+ /**
+ * Returns true if the lower open flag is set, and false otherwise.
+ */
+ readonly lowerOpen: boolean;
+ /**
+ * Returns upper bound, or undefined if none.
+ */
+ readonly upper: any;
+ /**
+ * Returns true if the upper open flag is set, and false otherwise.
+ */
+ readonly upperOpen: boolean;
+ /**
+ * Returns true if key is included in the range, and false otherwise.
+ */
+ includes(key: any): boolean;
+}
+
+/** This example shows a variety of different uses of object stores, from updating the data structure with IDBObjectStore.createIndex inside an onupgradeneeded function, to adding a new item to our object store with IDBObjectStore.add. For a full working example, see our To-do Notifications app (view example live.) */
+export interface IDBObjectStore {
+ /**
+ * Returns true if the store has a key generator, and false otherwise.
+ */
+ readonly autoIncrement: boolean;
+ /**
+ * Returns a list of the names of indexes in the store.
+ */
+ readonly indexNames: DOMStringList;
+ /**
+ * Returns the key path of the store, or null if none.
+ */
+ readonly keyPath: string | string[];
+ /**
+ * Returns the name of the store.
+ */
+ name: string;
+ /**
+ * Returns the associated transaction.
+ */
+ readonly transaction: IDBTransaction;
+ /**
+ * Adds or updates a record in store with the given value and key.
+ *
+ * If the store uses in-line keys and key is specified a "DataError" DOMException will be thrown.
+ *
+ * If put() is used, any existing record with the key will be replaced. If add() is used, and if a record with the key already exists the request will fail, with request's error set to a "ConstraintError" DOMException.
+ *
+ * If successful, request's result will be the record's key.
+ */
+ add(value: any, key?: IDBValidKey): IDBRequest<IDBValidKey>;
+ /**
+ * Deletes all records in store.
+ *
+ * If successful, request's result will be undefined.
+ */
+ clear(): IDBRequest<undefined>;
+ /**
+ * Retrieves the number of records matching the given key or key range in query.
+ *
+ * If successful, request's result will be the count.
+ */
+ count(key?: IDBValidKey | IDBKeyRange): IDBRequest<number>;
+ /**
+ * Creates a new index in store with the given name, keyPath and options and returns a new IDBIndex. If the keyPath and options define constraints that cannot be satisfied with the data already in store the upgrade transaction will abort with a "ConstraintError" DOMException.
+ *
+ * Throws an "InvalidStateError" DOMException if not called within an upgrade transaction.
+ */
+ createIndex(
+ name: string,
+ keyPath: string | string[],
+ options?: IDBIndexParameters,
+ ): IDBIndex;
+ /**
+ * Deletes records in store with the given key or in the given key range in query.
+ *
+ * If successful, request's result will be undefined.
+ */
+ delete(key: IDBValidKey | IDBKeyRange): IDBRequest<undefined>;
+ /**
+ * Deletes the index in store with the given name.
+ *
+ * Throws an "InvalidStateError" DOMException if not called within an upgrade transaction.
+ */
+ deleteIndex(name: string): void;
+ /**
+ * Retrieves the value of the first record matching the given key or key range in query.
+ *
+ * If successful, request's result will be the value, or undefined if there was no matching record.
+ */
+ get(query: IDBValidKey | IDBKeyRange): IDBRequest<any | undefined>;
+ /**
+ * Retrieves the values of the records matching the given key or key range in query (up to count if given).
+ *
+ * If successful, request's result will be an Array of the values.
+ */
+ getAll(
+ query?: IDBValidKey | IDBKeyRange | null,
+ count?: number,
+ ): IDBRequest<any[]>;
+ /**
+ * Retrieves the keys of records matching the given key or key range in query (up to count if given).
+ *
+ * If successful, request's result will be an Array of the keys.
+ */
+ getAllKeys(
+ query?: IDBValidKey | IDBKeyRange | null,
+ count?: number,
+ ): IDBRequest<IDBValidKey[]>;
+ /**
+ * Retrieves the key of the first record matching the given key or key range in query.
+ *
+ * If successful, request's result will be the key, or undefined if there was no matching record.
+ */
+ getKey(query: IDBValidKey | IDBKeyRange): IDBRequest<IDBValidKey | undefined>;
+ index(name: string): IDBIndex;
+ /**
+ * Opens a cursor over the records matching query, ordered by direction. If query is null, all records in store are matched.
+ *
+ * If successful, request's result will be an IDBCursorWithValue pointing at the first matching record, or null if there were no matching records.
+ */
+ openCursor(
+ query?: IDBValidKey | IDBKeyRange | null,
+ direction?: IDBCursorDirection,
+ ): IDBRequest<IDBCursorWithValue | null>;
+ /**
+ * Opens a cursor with key only flag set over the records matching query, ordered by direction. If query is null, all records in store are matched.
+ *
+ * If successful, request's result will be an IDBCursor pointing at the first matching record, or null if there were no matching records.
+ */
+ openKeyCursor(
+ query?: IDBValidKey | IDBKeyRange | null,
+ direction?: IDBCursorDirection,
+ ): IDBRequest<IDBCursor | null>;
+ /**
+ * Adds or updates a record in store with the given value and key.
+ *
+ * If the store uses in-line keys and key is specified a "DataError" DOMException will be thrown.
+ *
+ * If put() is used, any existing record with the key will be replaced. If add() is used, and if a record with the key already exists the request will fail, with request's error set to a "ConstraintError" DOMException.
+ *
+ * If successful, request's result will be the record's key.
+ */
+ put(value: any, key?: IDBValidKey): IDBRequest<IDBValidKey>;
+}
+
+export interface IDBOpenDBRequestEventMap extends IDBRequestEventMap {
+ blocked: Event;
+ upgradeneeded: IDBVersionChangeEvent;
+}
+
+/** Also inherits methods from its parents IDBRequest and EventTarget. */
+export interface IDBOpenDBRequest extends IDBRequest<IDBDatabase> {
+ onblocked: ((this: IDBOpenDBRequest, ev: Event) => any) | null;
+ onupgradeneeded:
+ | ((this: IDBOpenDBRequest, ev: IDBVersionChangeEvent) => any)
+ | null;
+ addEventListener<K extends keyof IDBOpenDBRequestEventMap>(
+ type: K,
+ listener: (this: IDBOpenDBRequest, ev: IDBOpenDBRequestEventMap[K]) => any,
+ options?: boolean | AddEventListenerOptions,
+ ): void;
+ addEventListener(
+ type: string,
+ listener: EventListenerOrEventListenerObject,
+ options?: boolean | AddEventListenerOptions,
+ ): void;
+ removeEventListener<K extends keyof IDBOpenDBRequestEventMap>(
+ type: K,
+ listener: (this: IDBOpenDBRequest, ev: IDBOpenDBRequestEventMap[K]) => any,
+ options?: boolean | EventListenerOptions,
+ ): void;
+ removeEventListener(
+ type: string,
+ listener: EventListenerOrEventListenerObject,
+ options?: boolean | EventListenerOptions,
+ ): void;
+}
+
+export type IDBRequestReadyState = "done" | "pending";
+
+export interface IDBRequestEventMap {
+ error: Event;
+ success: Event;
+}
+
+/** An abnormal event (called an exception) which occurs as a result of calling a method or accessing a property of a web API. */
+export interface DOMException {
+ readonly code: number;
+ readonly message: string;
+ readonly name: string;
+ readonly ABORT_ERR: number;
+ readonly DATA_CLONE_ERR: number;
+ readonly DOMSTRING_SIZE_ERR: number;
+ readonly HIERARCHY_REQUEST_ERR: number;
+ readonly INDEX_SIZE_ERR: number;
+ readonly INUSE_ATTRIBUTE_ERR: number;
+ readonly INVALID_ACCESS_ERR: number;
+ readonly INVALID_CHARACTER_ERR: number;
+ readonly INVALID_MODIFICATION_ERR: number;
+ readonly INVALID_NODE_TYPE_ERR: number;
+ readonly INVALID_STATE_ERR: number;
+ readonly NAMESPACE_ERR: number;
+ readonly NETWORK_ERR: number;
+ readonly NOT_FOUND_ERR: number;
+ readonly NOT_SUPPORTED_ERR: number;
+ readonly NO_DATA_ALLOWED_ERR: number;
+ readonly NO_MODIFICATION_ALLOWED_ERR: number;
+ readonly QUOTA_EXCEEDED_ERR: number;
+ readonly SECURITY_ERR: number;
+ readonly SYNTAX_ERR: number;
+ readonly TIMEOUT_ERR: number;
+ readonly TYPE_MISMATCH_ERR: number;
+ readonly URL_MISMATCH_ERR: number;
+ readonly VALIDATION_ERR: number;
+ readonly WRONG_DOCUMENT_ERR: number;
+}
+
+/** The request object does not initially contain any information about the result of the operation, but once information becomes available, an event is fired on the request, and the information becomes available through the properties of the IDBRequest instance. */
+export interface IDBRequest<T = any> extends EventTarget {
+ /**
+ * When a request is completed, returns the error (a DOMException), or null if the request succeeded. Throws a "InvalidStateError" DOMException if the request is still pending.
+ */
+ readonly error: DOMException | null;
+ onerror: ((this: IDBRequest<T>, ev: Event) => any) | null;
+ onsuccess: ((this: IDBRequest<T>, ev: Event) => any) | null;
+ /**
+ * Returns "pending" until a request is complete, then returns "done".
+ */
+ readonly readyState: IDBRequestReadyState;
+ /**
+ * When a request is completed, returns the result, or undefined if the request failed. Throws a "InvalidStateError" DOMException if the request is still pending.
+ */
+ readonly result: T;
+ /**
+ * Returns the IDBObjectStore, IDBIndex, or IDBCursor the request was made against, or null if is was an open request.
+ */
+ readonly source: IDBObjectStore | IDBIndex | IDBCursor;
+ /**
+ * Returns the IDBTransaction the request was made within. If this as an open request, then it returns an upgrade transaction while it is running, or null otherwise.
+ */
+ readonly transaction: IDBTransaction | null;
+ addEventListener<K extends keyof IDBRequestEventMap>(
+ type: K,
+ listener: (this: IDBRequest<T>, ev: IDBRequestEventMap[K]) => any,
+ options?: boolean | AddEventListenerOptions,
+ ): void;
+ addEventListener(
+ type: string,
+ listener: EventListenerOrEventListenerObject,
+ options?: boolean | AddEventListenerOptions,
+ ): void;
+ removeEventListener<K extends keyof IDBRequestEventMap>(
+ type: K,
+ listener: (this: IDBRequest<T>, ev: IDBRequestEventMap[K]) => any,
+ options?: boolean | EventListenerOptions,
+ ): void;
+ removeEventListener(
+ type: string,
+ listener: EventListenerOrEventListenerObject,
+ options?: boolean | EventListenerOptions,
+ ): void;
+}
+
+export interface IDBTransactionEventMap {
+ abort: Event;
+ complete: Event;
+ error: Event;
+}
+
+export interface IDBTransaction extends EventTarget {
+ /**
+ * Returns the transaction's connection.
+ */
+ readonly db: IDBDatabase;
+ /**
+ * If the transaction was aborted, returns the error (a DOMException) providing the reason.
+ */
+ readonly error: DOMException;
+ /**
+ * Returns the mode the transaction was created with ("readonly" or "readwrite"), or "versionchange" for an upgrade transaction.
+ */
+ readonly mode: IDBTransactionMode;
+ /**
+ * Returns a list of the names of object stores in the transaction's scope. For an upgrade transaction this is all object stores in the database.
+ */
+ readonly objectStoreNames: DOMStringList;
+ onabort: ((this: IDBTransaction, ev: Event) => any) | null;
+ oncomplete: ((this: IDBTransaction, ev: Event) => any) | null;
+ onerror: ((this: IDBTransaction, ev: Event) => any) | null;
+ /**
+ * Aborts the transaction. All pending requests will fail with a "AbortError" DOMException and all changes made to the database will be reverted.
+ */
+ abort(): void;
+ /**
+ * Returns an IDBObjectStore in the transaction's scope.
+ */
+ objectStore(name: string): IDBObjectStore;
+ addEventListener<K extends keyof IDBTransactionEventMap>(
+ type: K,
+ listener: (this: IDBTransaction, ev: IDBTransactionEventMap[K]) => any,
+ options?: boolean | AddEventListenerOptions,
+ ): void;
+ addEventListener(
+ type: string,
+ listener: EventListenerOrEventListenerObject,
+ options?: boolean | AddEventListenerOptions,
+ ): void;
+ removeEventListener<K extends keyof IDBTransactionEventMap>(
+ type: K,
+ listener: (this: IDBTransaction, ev: IDBTransactionEventMap[K]) => any,
+ options?: boolean | EventListenerOptions,
+ ): void;
+ removeEventListener(
+ type: string,
+ listener: EventListenerOrEventListenerObject,
+ options?: boolean | EventListenerOptions,
+ ): void;
+}
+
+/** This IndexedDB API interface indicates that the version of the database has changed, as the result of an IDBOpenDBRequest.onupgradeneeded event handler function. */
+export interface IDBVersionChangeEvent extends Event {
+ readonly newVersion: number | null;
+ readonly oldVersion: number;
+}
diff --git a/packages/idb-bridge/src/index.ts b/packages/idb-bridge/src/index.ts
index a65458748..8f19c9a31 100644
--- a/packages/idb-bridge/src/index.ts
+++ b/packages/idb-bridge/src/index.ts
@@ -1,23 +1,23 @@
import { BridgeIDBFactory } from "./BridgeIDBFactory";
import { BridgeIDBCursor } from "./BridgeIDBCursor";
import { BridgeIDBIndex } from "./BridgeIDBIndex";
-import BridgeIDBDatabase from "./BridgeIDBDatabase";
-import BridgeIDBKeyRange from "./BridgeIDBKeyRange";
-import BridgeIDBObjectStore from "./BridgeIDBObjectStore";
-import BridgeIDBOpenDBRequest from "./BridgeIDBOpenDBRequest";
-import BridgeIDBRequest from "./BridgeIDBRequest";
-import BridgeIDBTransaction from "./BridgeIDBTransaction";
-import BridgeIDBVersionChangeEvent from "./BridgeIDBVersionChangeEvent";
+import { BridgeIDBDatabase } from "./BridgeIDBDatabase";
+import { BridgeIDBKeyRange } from "./BridgeIDBKeyRange";
+import { BridgeIDBObjectStore } from "./BridgeIDBObjectStore";
+import { BridgeIDBOpenDBRequest } from "./BridgeIDBOpenDBRequest";
+import { BridgeIDBRequest } from "./BridgeIDBRequest";
+import { BridgeIDBTransaction } from "./BridgeIDBTransaction";
+import { BridgeIDBVersionChangeEvent } from "./BridgeIDBVersionChangeEvent";
export { BridgeIDBFactory, BridgeIDBCursor };
export { MemoryBackend } from "./MemoryBackend";
// globalThis polyfill, see https://mathiasbynens.be/notes/globalthis
-(function() {
+(function () {
if (typeof globalThis === "object") return;
Object.defineProperty(Object.prototype, "__magic__", {
- get: function() {
+ get: function () {
return this;
},
configurable: true, // This makes it possible to `delete` the getter later.
@@ -58,3 +58,6 @@ export function shimIndexedDB(factory: BridgeIDBFactory): void {
// @ts-ignore: shimming
globalThis.IDBVersionChangeEvent = BridgeIDBVersionChangeEvent;
}
+
+import * as idbtypes from "./idbtypes";
+export type { idbtypes };
diff --git a/packages/idb-bridge/src/tree/b+tree.ts b/packages/idb-bridge/src/tree/b+tree.ts
index 783c6b049..59a49baa3 100644
--- a/packages/idb-bridge/src/tree/b+tree.ts
+++ b/packages/idb-bridge/src/tree/b+tree.ts
@@ -24,14 +24,29 @@ SPDX-License-Identifier: MIT
// Original repository: https://github.com/qwertie/btree-typescript
-
-import { ISortedMap, ISortedMapF } from './interfaces';
+import { ISortedMap, ISortedMapF } from "./interfaces";
export {
- ISetSource, ISetSink, ISet, ISetF, ISortedSetSource, ISortedSet, ISortedSetF,
- IMapSource, IMapSink, IMap, IMapF, ISortedMapSource, ISortedMap, ISortedMapF
-} from './interfaces';
-
-export type EditRangeResult<V,R=number> = {value?:V, break?:R, delete?:boolean};
+ ISetSource,
+ ISetSink,
+ ISet,
+ ISetF,
+ ISortedSetSource,
+ ISortedSet,
+ ISortedSetF,
+ IMapSource,
+ IMapSink,
+ IMap,
+ IMapF,
+ ISortedMapSource,
+ ISortedMap,
+ ISortedMapF,
+} from "./interfaces";
+
+export type EditRangeResult<V, R = number> = {
+ value?: V;
+ break?: R;
+ delete?: boolean;
+};
type index = number;
@@ -57,7 +72,7 @@ type index = number;
// - V8 source (NewElementsCapacity in src/objects.h): arrays grow by 50% + 16 elements
/** Compares two numbers, strings, arrays of numbers/strings, Dates,
- * or objects that have a valueOf() method returning a number or string.
+ * or objects that have a valueOf() method returning a number or string.
* Optimized for numbers. Returns 1 if a>b, -1 if a<b, and 0 if a===b.
*/
export function defaultComparator(a: any, b: any) {
@@ -66,42 +81,42 @@ export function defaultComparator(a: any, b: any) {
// General case (c is NaN): string / arrays / Date / incomparable things
if (a) a = a.valueOf();
if (b) b = b.valueOf();
- return a < b ? -1 : a > b ? 1 : a == b ? 0 : c;
-};
+ return a < b ? -1 : a > b ? 1 : a == b ? 0 : c;
+}
/**
- * A reasonably fast collection of key-value pairs with a powerful API.
+ * A reasonably fast collection of key-value pairs with a powerful API.
* Largely compatible with the standard Map. BTree is a B+ tree data structure,
* so the collection is sorted by key.
- *
+ *
* B+ trees tend to use memory more efficiently than hashtables such as the
- * standard Map, especially when the collection contains a large number of
- * items. However, maintaining the sort order makes them modestly slower:
+ * standard Map, especially when the collection contains a large number of
+ * items. However, maintaining the sort order makes them modestly slower:
* O(log size) rather than O(1). This B+ tree implementation supports O(1)
* fast cloning. It also supports freeze(), which can be used to ensure that
* a BTree is not changed accidentally.
- *
+ *
* Confusingly, the ES6 Map.forEach(c) method calls c(value,key) instead of
* c(key,value), in contrast to other methods such as set() and entries()
- * which put the key first. I can only assume that the order was reversed on
+ * which put the key first. I can only assume that the order was reversed on
* the theory that users would usually want to examine values and ignore keys.
- * BTree's forEach() therefore works the same way, but a second method
+ * BTree's forEach() therefore works the same way, but a second method
* `.forEachPair((key,value)=>{...})` is provided which sends you the key
- * first and the value second; this method is slightly faster because it is
+ * first and the value second; this method is slightly faster because it is
* the "native" for-each method for this class.
- *
- * Out of the box, BTree supports keys that are numbers, strings, arrays of
- * numbers/strings, Date, and objects that have a valueOf() method returning a
+ *
+ * Out of the box, BTree supports keys that are numbers, strings, arrays of
+ * numbers/strings, Date, and objects that have a valueOf() method returning a
* number or string. Other data types, such as arrays of Date or custom
- * objects, require a custom comparator, which you must pass as the second
- * argument to the constructor (the first argument is an optional list of
+ * objects, require a custom comparator, which you must pass as the second
+ * argument to the constructor (the first argument is an optional list of
* initial items). Symbols cannot be used as keys because they are unordered
* (one Symbol is never "greater" or "less" than another).
- *
+ *
* @example
* Given a {name: string, age: number} object, you can create a tree sorted by
* name and then by age like this:
- *
+ *
* var tree = new BTree(undefined, (a, b) => {
* if (a.name > b.name)
* return 1; // Return a number >0 when a > b
@@ -110,36 +125,36 @@ export function defaultComparator(a: any, b: any) {
* else // names are equal (or incomparable)
* return a.age - b.age; // Return >0 when a.age > b.age
* });
- *
+ *
* tree.set({name:"Bill", age:17}, "happy");
* tree.set({name:"Fran", age:40}, "busy & stressed");
* tree.set({name:"Bill", age:55}, "recently laid off");
* tree.forEachPair((k, v) => {
* console.log(`Name: ${k.name} Age: ${k.age} Status: ${v}`);
* });
- *
+ *
* @description
* The "range" methods (`forEach, forRange, editRange`) will return the number
* of elements that were scanned. In addition, the callback can return {break:R}
* to stop early and return R from the outer function.
- *
+ *
* - TODO: Test performance of preallocating values array at max size
* - TODO: Add fast initialization when a sorted array is provided to constructor
- *
+ *
* For more documentation see https://github.com/qwertie/btree-typescript
*
- * Are you a C# developer? You might like the similar data structures I made for C#:
+ * Are you a C# developer? You might like the similar data structures I made for C#:
* BDictionary, BList, etc. See http://core.loyc.net/collections/
- *
+ *
* @author David Piepgrass
*/
-export default class BTree<K=any, V=any> implements ISortedMapF<K,V>, ISortedMap<K,V>
-{
- private _root: BNode<K, V> = EmptyLeaf as BNode<K,V>;
+export default class BTree<K = any, V = any>
+ implements ISortedMapF<K, V>, ISortedMap<K, V> {
+ private _root: BNode<K, V> = EmptyLeaf as BNode<K, V>;
_size: number = 0;
_maxNodeSize: number;
- _compare: (a:K, b:K) => number;
-
+ _compare: (a: K, b: K) => number;
+
/**
* Initializes an empty B+ tree.
* @param compare Custom function to compare pairs of elements in the tree.
@@ -148,60 +163,78 @@ export default class BTree<K=any, V=any> implements ISortedMapF<K,V>, ISortedMap
* @param maxNodeSize Branching factor (maximum items or children per node)
* Must be in range 4..256. If undefined or <4 then default is used; if >256 then 256.
*/
- public constructor(entries?: [K,V][], compare?: (a: K, b: K) => number, maxNodeSize?: number) {
+ public constructor(
+ entries?: [K, V][],
+ compare?: (a: K, b: K) => number,
+ maxNodeSize?: number,
+ ) {
this._maxNodeSize = maxNodeSize! >= 4 ? Math.min(maxNodeSize!, 256) : 32;
this._compare = compare || defaultComparator;
- if (entries)
- this.setPairs(entries);
+ if (entries) this.setPairs(entries);
}
-
+
// ES6 Map<K,V> methods ///////////////////////////////////////////////////
/** Gets the number of key-value pairs in the tree. */
- get size() { return this._size; }
+ get size() {
+ return this._size;
+ }
/** Gets the number of key-value pairs in the tree. */
- get length() { return this._size; }
+ get length() {
+ return this._size;
+ }
/** Returns true iff the tree contains no key-value pairs. */
- get isEmpty() { return this._size === 0; }
+ get isEmpty() {
+ return this._size === 0;
+ }
/** Releases the tree so that its size is 0. */
clear() {
- this._root = EmptyLeaf as BNode<K,V>;
+ this._root = EmptyLeaf as BNode<K, V>;
this._size = 0;
}
- forEach(callback: (v:V, k:K, tree:BTree<K,V>) => void, thisArg?: any): number;
+ forEach(
+ callback: (v: V, k: K, tree: BTree<K, V>) => void,
+ thisArg?: any,
+ ): number;
- /** Runs a function for each key-value pair, in order from smallest to
+ /** Runs a function for each key-value pair, in order from smallest to
* largest key. For compatibility with ES6 Map, the argument order to
- * the callback is backwards: value first, then key. Call forEachPair
+ * the callback is backwards: value first, then key. Call forEachPair
* instead to receive the key as the first argument.
* @param thisArg If provided, this parameter is assigned as the `this`
* value for each callback.
* @returns the number of values that were sent to the callback,
* or the R value if the callback returned {break:R}. */
- forEach<R=number>(callback: (v:V, k:K, tree:BTree<K,V>) => {break?:R}|void, thisArg?: any): R|number {
- if (thisArg !== undefined)
- callback = callback.bind(thisArg);
+ forEach<R = number>(
+ callback: (v: V, k: K, tree: BTree<K, V>) => { break?: R } | void,
+ thisArg?: any,
+ ): R | number {
+ if (thisArg !== undefined) callback = callback.bind(thisArg);
return this.forEachPair((k, v) => callback(v, k, this));
}
- /** Runs a function for each key-value pair, in order from smallest to
+ /** Runs a function for each key-value pair, in order from smallest to
* largest key. The callback can return {break:R} (where R is any value
* except undefined) to stop immediately and return R from forEachPair.
- * @param onFound A function that is called for each key-value pair. This
+ * @param onFound A function that is called for each key-value pair. This
* function can return {break:R} to stop early with result R.
- * The reason that you must return {break:R} instead of simply R
- * itself is for consistency with editRange(), which allows
+ * The reason that you must return {break:R} instead of simply R
+ * itself is for consistency with editRange(), which allows
* multiple actions, not just breaking.
- * @param initialCounter This is the value of the third argument of
- * `onFound` the first time it is called. The counter increases
+ * @param initialCounter This is the value of the third argument of
+ * `onFound` the first time it is called. The counter increases
* by one each time `onFound` is called. Default value: 0
* @returns the number of pairs sent to the callback (plus initialCounter,
* if you provided one). If the callback returned {break:R} then
* the R value is returned instead. */
- forEachPair<R=number>(callback: (k:K, v:V, counter:number) => {break?:R}|void, initialCounter?: number): R|number {
- var low = this.minKey(), high = this.maxKey();
+ forEachPair<R = number>(
+ callback: (k: K, v: V, counter: number) => { break?: R } | void,
+ initialCounter?: number,
+ ): R | number {
+ var low = this.minKey(),
+ high = this.maxKey();
return this.forRange(low!, high!, true, callback, initialCounter);
}
@@ -214,13 +247,13 @@ export default class BTree<K=any, V=any> implements ISortedMapF<K,V>, ISortedMap
get(key: K, defaultValue?: V): V | undefined {
return this._root.get(key, defaultValue, this);
}
-
+
/**
* Adds or overwrites a key-value pair in the B+ tree.
* @param key the key is used to determine the sort order of
* data in the tree.
* @param value data to associate with the key (optional)
- * @param overwrite Whether to overwrite an existing key-value pair
+ * @param overwrite Whether to overwrite an existing key-value pair
* (default: true). If this is false and there is an existing
* key-value pair then this method has no effect.
* @returns true if a new key-value pair was added.
@@ -229,14 +262,12 @@ export default class BTree<K=any, V=any> implements ISortedMapF<K,V>, ISortedMap
* as well as the value. This has no effect unless the new key
* has data that does not affect its sort order.
*/
- set(key: K, value: V, overwrite?: boolean): boolean {
- if (this._root.isShared)
- this._root = this._root.clone();
+ set(key: K, value: V, overwrite?: boolean): boolean {
+ if (this._root.isShared) this._root = this._root.clone();
var result = this._root.set(key, value, overwrite, this);
- if (result === true || result === false)
- return result;
+ if (result === true || result === false) return result;
// Root node has split, so create a new root node.
- this._root = new BNodeInternal<K,V>([this._root, result]);
+ this._root = new BNodeInternal<K, V>([this._root, result]);
return true;
}
@@ -247,7 +278,7 @@ export default class BTree<K=any, V=any> implements ISortedMapF<K,V>, ISortedMap
* @param key Key to detect
* @description Computational complexity: O(log size)
*/
- has(key: K): boolean {
+ has(key: K): boolean {
return this.forRange(key, key, true, undefined) !== 0;
}
@@ -264,42 +295,50 @@ export default class BTree<K=any, V=any> implements ISortedMapF<K,V>, ISortedMap
// Clone-mutators /////////////////////////////////////////////////////////
/** Returns a copy of the tree with the specified key set (the value is undefined). */
- with(key: K): BTree<K,V|undefined>;
+ with(key: K): BTree<K, V | undefined>;
/** Returns a copy of the tree with the specified key-value pair set. */
- with<V2>(key: K, value: V2, overwrite?: boolean): BTree<K,V|V2>;
- with<V2>(key: K, value?: V2, overwrite?: boolean): BTree<K,V|V2|undefined> {
- let nu = this.clone() as BTree<K,V|V2|undefined>;
+ with<V2>(key: K, value: V2, overwrite?: boolean): BTree<K, V | V2>;
+ with<V2>(
+ key: K,
+ value?: V2,
+ overwrite?: boolean,
+ ): BTree<K, V | V2 | undefined> {
+ let nu = this.clone() as BTree<K, V | V2 | undefined>;
return nu.set(key, value, overwrite) || overwrite ? nu : this;
}
/** Returns a copy of the tree with the specified key-value pairs set. */
- withPairs<V2>(pairs: [K,V|V2][], overwrite: boolean): BTree<K,V|V2> {
- let nu = this.clone() as BTree<K,V|V2>;
+ withPairs<V2>(pairs: [K, V | V2][], overwrite: boolean): BTree<K, V | V2> {
+ let nu = this.clone() as BTree<K, V | V2>;
return nu.setPairs(pairs, overwrite) !== 0 || overwrite ? nu : this;
}
- /** Returns a copy of the tree with the specified keys present.
+ /** Returns a copy of the tree with the specified keys present.
* @param keys The keys to add. If a key is already present in the tree,
* neither the existing key nor the existing value is modified.
- * @param returnThisIfUnchanged if true, returns this if all keys already
+ * @param returnThisIfUnchanged if true, returns this if all keys already
* existed. Performance note: due to the architecture of this class, all
* node(s) leading to existing keys are cloned even if the collection is
* ultimately unchanged.
- */
- withKeys(keys: K[], returnThisIfUnchanged?: boolean): BTree<K,V|undefined> {
- let nu = this.clone() as BTree<K,V|undefined>, changed = false;
+ */
+ withKeys(
+ keys: K[],
+ returnThisIfUnchanged?: boolean,
+ ): BTree<K, V | undefined> {
+ let nu = this.clone() as BTree<K, V | undefined>,
+ changed = false;
for (var i = 0; i < keys.length; i++)
changed = nu.set(keys[i], undefined, false) || changed;
return returnThisIfUnchanged && !changed ? this : nu;
}
- /** Returns a copy of the tree with the specified key removed.
+ /** Returns a copy of the tree with the specified key removed.
* @param returnThisIfUnchanged if true, returns this if the key didn't exist.
* Performance note: due to the architecture of this class, node(s) leading
* to where the key would have been stored are cloned even when the key
* turns out not to exist and the collection is unchanged.
*/
- without(key: K, returnThisIfUnchanged?: boolean): BTree<K,V> {
+ without(key: K, returnThisIfUnchanged?: boolean): BTree<K, V> {
return this.withoutRange(key, key, true, returnThisIfUnchanged);
}
@@ -309,61 +348,92 @@ export default class BTree<K=any, V=any> implements ISortedMapF<K,V>, ISortedMap
* node(s) leading to where the key would have been stored are cloned
* even when the key turns out not to exist.
*/
- withoutKeys(keys: K[], returnThisIfUnchanged?: boolean): BTree<K,V> {
+ withoutKeys(keys: K[], returnThisIfUnchanged?: boolean): BTree<K, V> {
let nu = this.clone();
return nu.deleteKeys(keys) || !returnThisIfUnchanged ? nu : this;
}
/** Returns a copy of the tree with the specified range of keys removed. */
- withoutRange(low: K, high: K, includeHigh: boolean, returnThisIfUnchanged?: boolean): BTree<K,V> {
+ withoutRange(
+ low: K,
+ high: K,
+ includeHigh: boolean,
+ returnThisIfUnchanged?: boolean,
+ ): BTree<K, V> {
let nu = this.clone();
if (nu.deleteRange(low, high, includeHigh) === 0 && returnThisIfUnchanged)
return this;
return nu;
}
- /** Returns a copy of the tree with pairs removed whenever the callback
+ /** Returns a copy of the tree with pairs removed whenever the callback
* function returns false. `where()` is a synonym for this method. */
- filter(callback: (k:K,v:V,counter:number) => boolean, returnThisIfUnchanged?: boolean): BTree<K,V> {
+ filter(
+ callback: (k: K, v: V, counter: number) => boolean,
+ returnThisIfUnchanged?: boolean,
+ ): BTree<K, V> {
var nu = this.greedyClone();
var del: any;
- nu.editAll((k,v,i) => {
- if (!callback(k, v, i)) return del = Delete;
+ nu.editAll((k, v, i) => {
+ if (!callback(k, v, i)) return (del = Delete);
});
- if (!del && returnThisIfUnchanged)
- return this;
+ if (!del && returnThisIfUnchanged) return this;
return nu;
}
/** Returns a copy of the tree with all values altered by a callback function. */
- mapValues<R>(callback: (v:V,k:K,counter:number) => R): BTree<K,R> {
- var tmp = {} as {value:R};
+ mapValues<R>(callback: (v: V, k: K, counter: number) => R): BTree<K, R> {
+ var tmp = {} as { value: R };
var nu = this.greedyClone();
- nu.editAll((k,v,i) => {
- return tmp.value = callback(v, k, i), tmp as any;
+ nu.editAll((k, v, i) => {
+ return (tmp.value = callback(v, k, i)), tmp as any;
});
- return nu as any as BTree<K,R>;
+ return (nu as any) as BTree<K, R>;
}
- /** Performs a reduce operation like the `reduce` method of `Array`.
- * It is used to combine all pairs into a single value, or perform
+ /** Performs a reduce operation like the `reduce` method of `Array`.
+ * It is used to combine all pairs into a single value, or perform
* conversions. `reduce` is best understood by example. For example,
- * `tree.reduce((P, pair) => P * pair[0], 1)` multiplies all keys
- * together. It means "start with P=1, and for each pair multiply
- * it by the key in pair[0]". Another example would be converting
+ * `tree.reduce((P, pair) => P * pair[0], 1)` multiplies all keys
+ * together. It means "start with P=1, and for each pair multiply
+ * it by the key in pair[0]". Another example would be converting
* the tree to a Map (in this example, note that M.set returns M):
- *
+ *
* var M = tree.reduce((M, pair) => M.set(pair[0],pair[1]), new Map())
- *
+ *
* **Note**: the same array is sent to the callback on every iteration.
*/
- reduce<R>(callback: (previous:R,currentPair:[K,V],counter:number,tree:BTree<K,V>) => R, initialValue: R): R;
- reduce<R>(callback: (previous:R|undefined,currentPair:[K,V],counter:number,tree:BTree<K,V>) => R): R|undefined;
- reduce<R>(callback: (previous:R|undefined,currentPair:[K,V],counter:number,tree:BTree<K,V>) => R, initialValue?: R): R|undefined {
- let i = 0, p = initialValue;
- var it = this.entries(this.minKey(), ReusedArray), next;
- while (!(next = it.next()).done)
- p = callback(p, next.value, i++, this);
+ reduce<R>(
+ callback: (
+ previous: R,
+ currentPair: [K, V],
+ counter: number,
+ tree: BTree<K, V>,
+ ) => R,
+ initialValue: R,
+ ): R;
+ reduce<R>(
+ callback: (
+ previous: R | undefined,
+ currentPair: [K, V],
+ counter: number,
+ tree: BTree<K, V>,
+ ) => R,
+ ): R | undefined;
+ reduce<R>(
+ callback: (
+ previous: R | undefined,
+ currentPair: [K, V],
+ counter: number,
+ tree: BTree<K, V>,
+ ) => R,
+ initialValue?: R,
+ ): R | undefined {
+ let i = 0,
+ p = initialValue;
+ var it = this.entries(this.minKey(), ReusedArray),
+ next;
+ while (!(next = it.next()).done) p = callback(p, next.value, i++, this);
return p;
}
@@ -377,53 +447,59 @@ export default class BTree<K=any, V=any> implements ISortedMapF<K,V>, ISortedMap
* @param reusedArray Optional array used repeatedly to store key-value
* pairs, to avoid creating a new array on every iteration.
*/
- entries(lowestKey?: K, reusedArray?: (K|V)[]): IterableIterator<[K,V]> {
+ entries(lowestKey?: K, reusedArray?: (K | V)[]): IterableIterator<[K, V]> {
var info = this.findPath(lowestKey);
- if (info === undefined) return iterator<[K,V]>();
- var {nodequeue, nodeindex, leaf} = info;
+ if (info === undefined) return iterator<[K, V]>();
+ var { nodequeue, nodeindex, leaf } = info;
var state = reusedArray !== undefined ? 1 : 0;
- var i = (lowestKey === undefined ? -1 : leaf.indexOf(lowestKey, 0, this._compare) - 1);
+ var i =
+ lowestKey === undefined
+ ? -1
+ : leaf.indexOf(lowestKey, 0, this._compare) - 1;
- return iterator<[K,V]>(() => {
+ return iterator<[K, V]>(() => {
jump: for (;;) {
- switch(state) {
+ switch (state) {
case 0:
if (++i < leaf.keys.length)
- return {done: false, value: [leaf.keys[i], leaf.values[i]]};
+ return { done: false, value: [leaf.keys[i], leaf.values[i]] };
state = 2;
continue;
case 1:
if (++i < leaf.keys.length) {
- reusedArray![0] = leaf.keys[i], reusedArray![1] = leaf.values[i];
- return {done: false, value: reusedArray as [K,V]};
+ (reusedArray![0] = leaf.keys[i]),
+ (reusedArray![1] = leaf.values[i]);
+ return { done: false, value: reusedArray as [K, V] };
}
state = 2;
case 2:
// Advance to the next leaf node
- for (var level = -1;;) {
+ for (var level = -1; ; ) {
if (++level >= nodequeue.length) {
- state = 3; continue jump;
+ state = 3;
+ continue jump;
}
- if (++nodeindex[level] < nodequeue[level].length)
- break;
+ if (++nodeindex[level] < nodequeue[level].length) break;
}
for (; level > 0; level--) {
- nodequeue[level-1] = (nodequeue[level][nodeindex[level]] as BNodeInternal<K,V>).children;
- nodeindex[level-1] = 0;
+ nodequeue[level - 1] = (nodequeue[level][
+ nodeindex[level]
+ ] as BNodeInternal<K, V>).children;
+ nodeindex[level - 1] = 0;
}
leaf = nodequeue[0][nodeindex[0]];
i = -1;
state = reusedArray !== undefined ? 1 : 0;
continue;
case 3:
- return {done: true, value: undefined};
+ return { done: true, value: undefined };
}
}
});
}
/** Returns an iterator that provides items in reversed order.
- * @param highestKey Key at which to start iterating, or undefined to
+ * @param highestKey Key at which to start iterating, or undefined to
* start at minKey(). If the specified key doesn't exist then iteration
* starts at the next lower key (according to the comparator).
* @param reusedArray Optional array used repeatedly to store key-value
@@ -431,49 +507,56 @@ export default class BTree<K=any, V=any> implements ISortedMapF<K,V>, ISortedMap
* @param skipHighest Iff this flag is true and the highestKey exists in the
* collection, the pair matching highestKey is skipped, not iterated.
*/
- entriesReversed(highestKey?: K, reusedArray?: (K|V)[], skipHighest?: boolean): IterableIterator<[K,V]> {
+ entriesReversed(
+ highestKey?: K,
+ reusedArray?: (K | V)[],
+ skipHighest?: boolean,
+ ): IterableIterator<[K, V]> {
if ((highestKey = highestKey || this.maxKey()) === undefined)
- return iterator<[K,V]>(); // collection is empty
- var {nodequeue,nodeindex,leaf} = this.findPath(highestKey) || this.findPath(this.maxKey())!;
+ return iterator<[K, V]>(); // collection is empty
+ var { nodequeue, nodeindex, leaf } =
+ this.findPath(highestKey) || this.findPath(this.maxKey())!;
check(!nodequeue[0] || leaf === nodequeue[0][nodeindex[0]], "wat!");
var i = leaf.indexOf(highestKey, 0, this._compare);
- if (!(skipHighest || this._compare(leaf.keys[i], highestKey) > 0))
- i++;
+ if (!(skipHighest || this._compare(leaf.keys[i], highestKey) > 0)) i++;
var state = reusedArray !== undefined ? 1 : 0;
- return iterator<[K,V]>(() => {
+ return iterator<[K, V]>(() => {
jump: for (;;) {
- switch(state) {
+ switch (state) {
case 0:
if (--i >= 0)
- return {done: false, value: [leaf.keys[i], leaf.values[i]]};
+ return { done: false, value: [leaf.keys[i], leaf.values[i]] };
state = 2;
continue;
case 1:
if (--i >= 0) {
- reusedArray![0] = leaf.keys[i], reusedArray![1] = leaf.values[i];
- return {done: false, value: reusedArray as [K,V]};
+ (reusedArray![0] = leaf.keys[i]),
+ (reusedArray![1] = leaf.values[i]);
+ return { done: false, value: reusedArray as [K, V] };
}
state = 2;
case 2:
// Advance to the next leaf node
- for (var level = -1;;) {
+ for (var level = -1; ; ) {
if (++level >= nodequeue.length) {
- state = 3; continue jump;
+ state = 3;
+ continue jump;
}
- if (--nodeindex[level] >= 0)
- break;
+ if (--nodeindex[level] >= 0) break;
}
for (; level > 0; level--) {
- nodequeue[level-1] = (nodequeue[level][nodeindex[level]] as BNodeInternal<K,V>).children;
- nodeindex[level-1] = nodequeue[level-1].length-1;
+ nodequeue[level - 1] = (nodequeue[level][
+ nodeindex[level]
+ ] as BNodeInternal<K, V>).children;
+ nodeindex[level - 1] = nodequeue[level - 1].length - 1;
}
leaf = nodequeue[0][nodeindex[0]];
i = leaf.keys.length;
state = reusedArray !== undefined ? 1 : 0;
continue;
case 3:
- return {done: true, value: undefined};
+ return { done: true, value: undefined };
}
}
});
@@ -481,36 +564,39 @@ export default class BTree<K=any, V=any> implements ISortedMapF<K,V>, ISortedMap
/* Used by entries() and entriesReversed() to prepare to start iterating.
* It develops a "node queue" for each non-leaf level of the tree.
- * Levels are numbered "bottom-up" so that level 0 is a list of leaf
+ * Levels are numbered "bottom-up" so that level 0 is a list of leaf
* nodes from a low-level non-leaf node. The queue at a given level L
- * consists of nodequeue[L] which is the children of a BNodeInternal,
+ * consists of nodequeue[L] which is the children of a BNodeInternal,
* and nodeindex[L], the current index within that child list, such
* such that nodequeue[L-1] === nodequeue[L][nodeindex[L]].children.
* (However inside this function the order is reversed.)
*/
- private findPath(key?: K): { nodequeue: BNode<K,V>[][], nodeindex: number[], leaf: BNode<K,V> } | undefined
- {
+ private findPath(
+ key?: K,
+ ):
+ | { nodequeue: BNode<K, V>[][]; nodeindex: number[]; leaf: BNode<K, V> }
+ | undefined {
var nextnode = this._root;
- var nodequeue: BNode<K,V>[][], nodeindex: number[];
+ var nodequeue: BNode<K, V>[][], nodeindex: number[];
if (nextnode.isLeaf) {
- nodequeue = EmptyArray, nodeindex = EmptyArray; // avoid allocations
+ (nodequeue = EmptyArray), (nodeindex = EmptyArray); // avoid allocations
} else {
- nodequeue = [], nodeindex = [];
+ (nodequeue = []), (nodeindex = []);
for (var d = 0; !nextnode.isLeaf; d++) {
- nodequeue[d] = (nextnode as BNodeInternal<K,V>).children;
- nodeindex[d] = key === undefined ? 0 : nextnode.indexOf(key, 0, this._compare);
- if (nodeindex[d] >= nodequeue[d].length)
- return; // first key > maxKey()
+ nodequeue[d] = (nextnode as BNodeInternal<K, V>).children;
+ nodeindex[d] =
+ key === undefined ? 0 : nextnode.indexOf(key, 0, this._compare);
+ if (nodeindex[d] >= nodequeue[d].length) return; // first key > maxKey()
nextnode = nodequeue[d][nodeindex[d]];
}
nodequeue.reverse();
nodeindex.reverse();
}
- return {nodequeue, nodeindex, leaf:nextnode};
+ return { nodequeue, nodeindex, leaf: nextnode };
}
- /** Returns a new iterator for iterating the keys of each pair in ascending order.
+ /** Returns a new iterator for iterating the keys of each pair in ascending order.
* @param firstKey: Minimum key to include in the output. */
keys(firstKey?: K): IterableIterator<K> {
var it = this.entries(firstKey, ReusedArray);
@@ -520,8 +606,8 @@ export default class BTree<K=any, V=any> implements ISortedMapF<K,V>, ISortedMap
return n;
});
}
-
- /** Returns a new iterator for iterating the values of each pair in order by key.
+
+ /** Returns a new iterator for iterating the values of each pair in order by key.
* @param firstKey: Minimum key whose associated value is included in the output. */
values(firstKey?: K): IterableIterator<V> {
var it = this.entries(firstKey, ReusedArray);
@@ -540,57 +626,79 @@ export default class BTree<K=any, V=any> implements ISortedMapF<K,V>, ISortedMap
}
/** Gets the lowest key in the tree. Complexity: O(log size) */
- minKey(): K | undefined { return this._root.minKey(); }
-
+ minKey(): K | undefined {
+ return this._root.minKey();
+ }
+
/** Gets the highest key in the tree. Complexity: O(1) */
- maxKey(): K | undefined { return this._root.maxKey(); }
+ maxKey(): K | undefined {
+ return this._root.maxKey();
+ }
- /** Quickly clones the tree by marking the root node as shared.
+ /** Quickly clones the tree by marking the root node as shared.
* Both copies remain editable. When you modify either copy, any
* nodes that are shared (or potentially shared) between the two
* copies are cloned so that the changes do not affect other copies.
* This is known as copy-on-write behavior, or "lazy copying". */
- clone(): BTree<K,V> {
+ clone(): BTree<K, V> {
this._root.isShared = true;
- var result = new BTree<K,V>(undefined, this._compare, this._maxNodeSize);
+ var result = new BTree<K, V>(undefined, this._compare, this._maxNodeSize);
result._root = this._root;
result._size = this._size;
return result;
}
- /** Performs a greedy clone, immediately duplicating any nodes that are
+ /** Performs a greedy clone, immediately duplicating any nodes that are
* not currently marked as shared, in order to avoid marking any nodes
* as shared.
* @param force Clone all nodes, even shared ones.
*/
- greedyClone(force?: boolean): BTree<K,V> {
- var result = new BTree<K,V>(undefined, this._compare, this._maxNodeSize);
+ greedyClone(force?: boolean): BTree<K, V> {
+ var result = new BTree<K, V>(undefined, this._compare, this._maxNodeSize);
result._root = this._root.greedyClone(force);
result._size = this._size;
return result;
}
/** Gets an array filled with the contents of the tree, sorted by key */
- toArray(maxLength: number = 0x7FFFFFFF): [K,V][] {
- let min = this.minKey(), max = this.maxKey();
- if (min !== undefined)
- return this.getRange(min, max!, true, maxLength)
+ toArray(maxLength: number = 0x7fffffff): [K, V][] {
+ let min = this.minKey(),
+ max = this.maxKey();
+ if (min !== undefined) return this.getRange(min, max!, true, maxLength);
return [];
}
/** Gets an array of all keys, sorted */
keysArray() {
var results: K[] = [];
- this._root.forRange(this.minKey()!, this.maxKey()!, true, false, this, 0,
- (k,v) => { results.push(k); });
+ this._root.forRange(
+ this.minKey()!,
+ this.maxKey()!,
+ true,
+ false,
+ this,
+ 0,
+ (k, v) => {
+ results.push(k);
+ },
+ );
return results;
}
-
+
/** Gets an array of all values, sorted by key */
valuesArray() {
var results: V[] = [];
- this._root.forRange(this.minKey()!, this.maxKey()!, true, false, this, 0,
- (k,v) => { results.push(v); });
+ this._root.forRange(
+ this.minKey()!,
+ this.maxKey()!,
+ true,
+ false,
+ this,
+ 0,
+ (k, v) => {
+ results.push(v);
+ },
+ );
return results;
}
@@ -599,45 +707,44 @@ export default class BTree<K=any, V=any> implements ISortedMapF<K,V>, ISortedMap
return this.toArray().toString();
}
- /** Stores a key-value pair only if the key doesn't already exist in the tree.
+ /** Stores a key-value pair only if the key doesn't already exist in the tree.
* @returns true if a new key was added
- */
+ */
setIfNotPresent(key: K, value: V): boolean {
return this.set(key, value, false);
}
/** Returns the next pair whose key is larger than the specified key (or undefined if there is none) */
- nextHigherPair(key: K): [K,V]|undefined {
+ nextHigherPair(key: K): [K, V] | undefined {
var it = this.entries(key, ReusedArray);
var r = it.next();
- if (!r.done && this._compare(r.value[0], key) <= 0)
- r = it.next();
+ if (!r.done && this._compare(r.value[0], key) <= 0) r = it.next();
return r.value;
}
-
+
/** Returns the next key larger than the specified key (or undefined if there is none) */
- nextHigherKey(key: K): K|undefined {
+ nextHigherKey(key: K): K | undefined {
var p = this.nextHigherPair(key);
return p ? p[0] : p;
}
/** Returns the next pair whose key is smaller than the specified key (or undefined if there is none) */
- nextLowerPair(key: K): [K,V]|undefined {
+ nextLowerPair(key: K): [K, V] | undefined {
var it = this.entriesReversed(key, ReusedArray, true);
return it.next().value;
}
-
+
/** Returns the next key smaller than the specified key (or undefined if there is none) */
- nextLowerKey(key: K): K|undefined {
+ nextLowerKey(key: K): K | undefined {
var p = this.nextLowerPair(key);
return p ? p[0] : p;
}
- /** Edits the value associated with a key in the tree, if it already exists.
+ /** Edits the value associated with a key in the tree, if it already exists.
* @returns true if the key existed, false if not.
- */
- changeIfPresent(key: K, value: V): boolean {
- return this.editRange(key, key, true, (k,v) => ({value})) !== 0;
+ */
+ changeIfPresent(key: K, value: V): boolean {
+ return this.editRange(key, key, true, (k, v) => ({ value })) !== 0;
}
/**
@@ -648,106 +755,154 @@ export default class BTree<K=any, V=any> implements ISortedMapF<K,V>, ISortedMap
* @param includeHigh If the `high` key is present, its pair will be included
* in the output if and only if this parameter is true. Note: if the
* `low` key is present, it is always included in the output.
- * @param maxLength Length limit. getRange will stop scanning the tree when
+ * @param maxLength Length limit. getRange will stop scanning the tree when
* the array reaches this size.
* @description Computational complexity: O(result.length + log size)
*/
- getRange(low: K, high: K, includeHigh?: boolean, maxLength: number = 0x3FFFFFF): [K,V][] {
- var results: [K,V][] = [];
- this._root.forRange(low, high, includeHigh, false, this, 0, (k,v) => {
- results.push([k,v])
+ getRange(
+ low: K,
+ high: K,
+ includeHigh?: boolean,
+ maxLength: number = 0x3ffffff,
+ ): [K, V][] {
+ var results: [K, V][] = [];
+ this._root.forRange(low, high, includeHigh, false, this, 0, (k, v) => {
+ results.push([k, v]);
return results.length > maxLength ? Break : undefined;
});
return results;
}
/** Adds all pairs from a list of key-value pairs.
- * @param pairs Pairs to add to this tree. If there are duplicate keys,
- * later pairs currently overwrite earlier ones (e.g. [[0,1],[0,7]]
+ * @param pairs Pairs to add to this tree. If there are duplicate keys,
+ * later pairs currently overwrite earlier ones (e.g. [[0,1],[0,7]]
* associates 0 with 7.)
* @param overwrite Whether to overwrite pairs that already exist (if false,
* pairs[i] is ignored when the key pairs[i][0] already exists.)
* @returns The number of pairs added to the collection.
* @description Computational complexity: O(pairs.length * log(size + pairs.length))
*/
- setPairs(pairs: [K,V][], overwrite?: boolean): number {
+ setPairs(pairs: [K, V][], overwrite?: boolean): number {
var added = 0;
for (var i = 0; i < pairs.length; i++)
- if (this.set(pairs[i][0], pairs[i][1], overwrite))
- added++;
+ if (this.set(pairs[i][0], pairs[i][1], overwrite)) added++;
return added;
}
- forRange(low: K, high: K, includeHigh: boolean, onFound?: (k:K,v:V,counter:number) => void, initialCounter?: number): number;
+ forRange(
+ low: K,
+ high: K,
+ includeHigh: boolean,
+ onFound?: (k: K, v: V, counter: number) => void,
+ initialCounter?: number,
+ ): number;
/**
* Scans the specified range of keys, in ascending order by key.
* Note: the callback `onFound` must not insert or remove items in the
- * collection. Doing so may cause incorrect data to be sent to the
+ * collection. Doing so may cause incorrect data to be sent to the
* callback afterward.
* @param low The first key scanned will be greater than or equal to `low`.
* @param high Scanning stops when a key larger than this is reached.
* @param includeHigh If the `high` key is present, `onFound` is called for
* that final pair if and only if this parameter is true.
- * @param onFound A function that is called for each key-value pair. This
+ * @param onFound A function that is called for each key-value pair. This
* function can return {break:R} to stop early with result R.
- * @param initialCounter Initial third argument of onFound. This value
+ * @param initialCounter Initial third argument of onFound. This value
* increases by one each time `onFound` is called. Default: 0
- * @returns The number of values found, or R if the callback returned
+ * @returns The number of values found, or R if the callback returned
* `{break:R}` to stop early.
* @description Computational complexity: O(number of items scanned + log size)
*/
- forRange<R=number>(low: K, high: K, includeHigh: boolean, onFound?: (k:K,v:V,counter:number) => {break?:R}|void, initialCounter?: number): R|number {
- var r = this._root.forRange(low, high, includeHigh, false, this, initialCounter || 0, onFound);
+ forRange<R = number>(
+ low: K,
+ high: K,
+ includeHigh: boolean,
+ onFound?: (k: K, v: V, counter: number) => { break?: R } | void,
+ initialCounter?: number,
+ ): R | number {
+ var r = this._root.forRange(
+ low,
+ high,
+ includeHigh,
+ false,
+ this,
+ initialCounter || 0,
+ onFound,
+ );
return typeof r === "number" ? r : r.break!;
}
/**
* Scans and potentially modifies values for a subsequence of keys.
- * Note: the callback `onFound` should ideally be a pure function.
- * Specfically, it must not insert items, call clone(), or change
+ * Note: the callback `onFound` should ideally be a pure function.
+ * Specfically, it must not insert items, call clone(), or change
* the collection except via return value; out-of-band editing may
* cause an exception or may cause incorrect data to be sent to
- * the callback (duplicate or missed items). It must not cause a
+ * the callback (duplicate or missed items). It must not cause a
* clone() of the collection, otherwise the clone could be modified
* by changes requested by the callback.
* @param low The first key scanned will be greater than or equal to `low`.
* @param high Scanning stops when a key larger than this is reached.
* @param includeHigh If the `high` key is present, `onFound` is called for
* that final pair if and only if this parameter is true.
- * @param onFound A function that is called for each key-value pair. This
- * function can return `{value:v}` to change the value associated
+ * @param onFound A function that is called for each key-value pair. This
+ * function can return `{value:v}` to change the value associated
* with the current key, `{delete:true}` to delete the current pair,
* `{break:R}` to stop early with result R, or it can return nothing
* (undefined or {}) to cause no effect and continue iterating.
* `{break:R}` can be combined with one of the other two commands.
- * The third argument `counter` is the number of items iterated
+ * The third argument `counter` is the number of items iterated
* previously; it equals 0 when `onFound` is called the first time.
- * @returns The number of values scanned, or R if the callback returned
+ * @returns The number of values scanned, or R if the callback returned
* `{break:R}` to stop early.
- * @description
+ * @description
* Computational complexity: O(number of items scanned + log size)
* Note: if the tree has been cloned with clone(), any shared
- * nodes are copied before `onFound` is called. This takes O(n) time
+ * nodes are copied before `onFound` is called. This takes O(n) time
* where n is proportional to the amount of shared data scanned.
*/
- editRange<R=V>(low: K, high: K, includeHigh: boolean, onFound: (k:K,v:V,counter:number) => EditRangeResult<V,R>|void, initialCounter?: number): R|number {
+ editRange<R = V>(
+ low: K,
+ high: K,
+ includeHigh: boolean,
+ onFound: (k: K, v: V, counter: number) => EditRangeResult<V, R> | void,
+ initialCounter?: number,
+ ): R | number {
var root = this._root;
- if (root.isShared)
- this._root = root = root.clone();
+ if (root.isShared) this._root = root = root.clone();
try {
- var r = root.forRange(low, high, includeHigh, true, this, initialCounter || 0, onFound);
+ var r = root.forRange(
+ low,
+ high,
+ includeHigh,
+ true,
+ this,
+ initialCounter || 0,
+ onFound,
+ );
return typeof r === "number" ? r : r.break!;
} finally {
while (root.keys.length <= 1 && !root.isLeaf)
- this._root = root = root.keys.length === 0 ? EmptyLeaf :
- (root as any as BNodeInternal<K,V>).children[0];
+ this._root = root =
+ root.keys.length === 0
+ ? EmptyLeaf
+ : ((root as any) as BNodeInternal<K, V>).children[0];
}
}
/** Same as `editRange` except that the callback is called for all pairs. */
- editAll<R=V>(onFound: (k:K,v:V,counter:number) => EditRangeResult<V,R>|void, initialCounter?: number): R|number {
- return this.editRange(this.minKey()!, this.maxKey()!, true, onFound, initialCounter);
+ editAll<R = V>(
+ onFound: (k: K, v: V, counter: number) => EditRangeResult<V, R> | void,
+ initialCounter?: number,
+ ): R | number {
+ return this.editRange(
+ this.minKey()!,
+ this.maxKey()!,
+ true,
+ onFound,
+ initialCounter,
+ );
}
/**
@@ -764,13 +919,11 @@ export default class BTree<K=any, V=any> implements ISortedMapF<K,V>, ISortedMap
/** Deletes a series of keys from the collection. */
deleteKeys(keys: K[]): number {
- for (var i = 0, r = 0; i < keys.length; i++)
- if (this.delete(keys[i]))
- r++;
+ for (var i = 0, r = 0; i < keys.length; i++) if (this.delete(keys[i])) r++;
return r;
}
- /** Gets the height of the tree: the number of internal nodes between the
+ /** Gets the height of the tree: the number of internal nodes between the
* BTree object and its leaf nodes (zero if there are no internal nodes). */
get height(): number {
for (var node = this._root, h = -1; node != null; h++)
@@ -780,15 +933,15 @@ export default class BTree<K=any, V=any> implements ISortedMapF<K,V>, ISortedMap
/** Makes the object read-only to ensure it is not accidentally modified.
* Freezing does not have to be permanent; unfreeze() reverses the effect.
- * This is accomplished by replacing mutator functions with a function
- * that throws an Error. Compared to using a property (e.g. this.isFrozen)
+ * This is accomplished by replacing mutator functions with a function
+ * that throws an Error. Compared to using a property (e.g. this.isFrozen)
* this implementation gives better performance in non-frozen BTrees.
*/
freeze() {
var t = this as any;
- // Note: all other mutators ultimately call set() or editRange()
+ // Note: all other mutators ultimately call set() or editRange()
// so we don't need to override those others.
- t.clear = t.set = t.editRange = function() {
+ t.clear = t.set = t.editRange = function () {
throw new Error("Attempted to modify a frozen BTree");
};
}
@@ -802,7 +955,7 @@ export default class BTree<K=any, V=any> implements ISortedMapF<K,V>, ISortedMap
/** Returns true if the tree appears to be frozen. */
get isFrozen() {
- return this.hasOwnProperty('editRange');
+ return this.hasOwnProperty("editRange");
}
/** Scans the tree for signs of serious bugs (e.g. this.size doesn't match
@@ -812,65 +965,81 @@ export default class BTree<K=any, V=any> implements ISortedMapF<K,V>, ISortedMap
* does check that maxKey() of the children of internal nodes are sorted. */
checkValid() {
var size = this._root.checkValid(0, this);
- check(size === this.size, "size mismatch: counted ", size, "but stored", this.size);
+ check(
+ size === this.size,
+ "size mismatch: counted ",
+ size,
+ "but stored",
+ this.size,
+ );
}
}
declare const Symbol: any;
-if (Symbol && Symbol.iterator) // iterator is equivalent to entries()
+if (Symbol && Symbol.iterator)
+ // iterator is equivalent to entries()
(BTree as any).prototype[Symbol.iterator] = BTree.prototype.entries;
(BTree as any).prototype.where = BTree.prototype.filter;
(BTree as any).prototype.setRange = BTree.prototype.setPairs;
(BTree as any).prototype.add = BTree.prototype.set;
-function iterator<T>(next: () => {done?:boolean,value?:T} = (() => ({ done:true, value:undefined }))): IterableIterator<T> {
+function iterator<T>(
+ next: () => { done?: boolean; value?: T } = () => ({
+ done: true,
+ value: undefined,
+ }),
+): IterableIterator<T> {
var result: any = { next };
if (Symbol && Symbol.iterator)
- result[Symbol.iterator] = function() { return this; };
+ result[Symbol.iterator] = function () {
+ return this;
+ };
return result;
}
-
/** Leaf node / base class. **************************************************/
-class BNode<K,V> {
+class BNode<K, V> {
// If this is an internal node, _keys[i] is the highest key in children[i].
keys: K[];
values: V[];
isShared: true | undefined;
- get isLeaf() { return (this as any).children === undefined; }
-
+ get isLeaf() {
+ return (this as any).children === undefined;
+ }
+
constructor(keys: K[] = [], values?: V[]) {
this.keys = keys;
- this.values = values || undefVals as any[];
+ this.values = values || (undefVals as any[]);
this.isShared = undefined;
}
// Shared methods /////////////////////////////////////////////////////////
maxKey() {
- return this.keys[this.keys.length-1];
+ return this.keys[this.keys.length - 1];
}
// If key not found, returns i^failXor where i is the insertion index.
// Callers that don't care whether there was a match will set failXor=0.
- indexOf(key: K, failXor: number, cmp: (a:K, b:K) => number): index {
+ indexOf(key: K, failXor: number, cmp: (a: K, b: K) => number): index {
// TODO: benchmark multiple search strategies
const keys = this.keys;
- var lo = 0, hi = keys.length, mid = hi >> 1;
- while(lo < hi) {
+ var lo = 0,
+ hi = keys.length,
+ mid = hi >> 1;
+ while (lo < hi) {
var c = cmp(keys[mid], key);
- if (c < 0)
- lo = mid + 1;
- else if (c > 0) // key < keys[mid]
+ if (c < 0) lo = mid + 1;
+ else if (c > 0)
+ // key < keys[mid]
hi = mid;
- else if (c === 0)
- return mid;
+ else if (c === 0) return mid;
else {
// c is NaN or otherwise invalid
- if (key === key) // at least the search key is not NaN
+ if (key === key)
+ // at least the search key is not NaN
return keys.length;
- else
- throw new Error("BTree: NaN was used as a key");
+ else throw new Error("BTree: NaN was used as a key");
}
mid = (lo + hi) >> 1;
}
@@ -928,26 +1097,36 @@ class BNode<K,V> {
return this.keys[0];
}
- clone(): BNode<K,V> {
+ clone(): BNode<K, V> {
var v = this.values;
- return new BNode<K,V>(this.keys.slice(0), v === undefVals ? v : v.slice(0));
+ return new BNode<K, V>(
+ this.keys.slice(0),
+ v === undefVals ? v : v.slice(0),
+ );
}
- greedyClone(force?: boolean): BNode<K,V> {
+ greedyClone(force?: boolean): BNode<K, V> {
return this.isShared && !force ? this : this.clone();
}
- get(key: K, defaultValue: V|undefined, tree: BTree<K,V>): V|undefined {
+ get(key: K, defaultValue: V | undefined, tree: BTree<K, V>): V | undefined {
var i = this.indexOf(key, -1, tree._compare);
return i < 0 ? defaultValue : this.values[i];
}
- checkValid(depth: number, tree: BTree<K,V>): number {
- var kL = this.keys.length, vL = this.values.length;
- check(this.values === undefVals ? kL <= vL : kL === vL,
- "keys/values length mismatch: depth", depth, "with lengths", kL, vL);
+ checkValid(depth: number, tree: BTree<K, V>): number {
+ var kL = this.keys.length,
+ vL = this.values.length;
+ check(
+ this.values === undefVals ? kL <= vL : kL === vL,
+ "keys/values length mismatch: depth",
+ depth,
+ "with lengths",
+ kL,
+ vL,
+ );
// Note: we don't check for "node too small" because sometimes a node
- // can legitimately have size 1. This occurs if there is a batch
+ // can legitimately have size 1. This occurs if there is a batch
// deletion, leaving a node of size 1, and the siblings are full so
// it can't be merged with adjacent nodes. However, the parent will
// verify that the average node size is at least half of the maximum.
@@ -957,18 +1136,24 @@ class BNode<K,V> {
// Leaf Node: set & node splitting //////////////////////////////////////////
- set(key: K, value: V, overwrite: boolean|undefined, tree: BTree<K,V>): boolean|BNode<K,V> {
+ set(
+ key: K,
+ value: V,
+ overwrite: boolean | undefined,
+ tree: BTree<K, V>,
+ ): boolean | BNode<K, V> {
var i = this.indexOf(key, -1, tree._compare);
if (i < 0) {
// key does not exist yet
i = ~i;
tree._size++;
-
+
if (this.keys.length < tree._maxNodeSize) {
return this.insertInLeaf(i, key, value, tree);
} else {
// This leaf node is full and must split
- var newRightSibling = this.splitOffRightSide(), target: BNode<K,V> = this;
+ var newRightSibling = this.splitOffRightSide(),
+ target: BNode<K, V> = this;
if (i > this.keys.length) {
i -= this.keys.length;
target = newRightSibling;
@@ -979,8 +1164,7 @@ class BNode<K,V> {
} else {
// Key already exists
if (overwrite !== false) {
- if (value !== undefined)
- this.reifyValues();
+ if (value !== undefined) this.reifyValues();
// usually this is a no-op, but some users may wish to edit the key
this.keys[i] = key;
this.values[i] = value;
@@ -991,15 +1175,14 @@ class BNode<K,V> {
reifyValues() {
if (this.values === undefVals)
- return this.values = this.values.slice(0, this.keys.length);
+ return (this.values = this.values.slice(0, this.keys.length));
return this.values;
}
- insertInLeaf(i: index, key: K, value: V, tree: BTree<K,V>) {
+ insertInLeaf(i: index, key: K, value: V, tree: BTree<K, V>) {
this.keys.splice(i, 0, key);
if (this.values === undefVals) {
- while (undefVals.length < tree._maxNodeSize)
- undefVals.push(undefined);
+ while (undefVals.length < tree._maxNodeSize) undefVals.push(undefined);
if (value === undefined) {
return true;
} else {
@@ -1009,15 +1192,14 @@ class BNode<K,V> {
this.values.splice(i, 0, value);
return true;
}
-
- takeFromRight(rhs: BNode<K,V>) {
+
+ takeFromRight(rhs: BNode<K, V>) {
// Reminder: parent node must update its copy of key for this node
// assert: neither node is shared
// assert rhs.keys.length > (maxNodeSize/2 && this.keys.length<maxNodeSize)
var v = this.values;
if (rhs.values === undefVals) {
- if (v !== undefVals)
- v.push(undefined as any);
+ if (v !== undefVals) v.push(undefined as any);
} else {
v = this.reifyValues();
v.push(rhs.values.shift()!);
@@ -1025,14 +1207,13 @@ class BNode<K,V> {
this.keys.push(rhs.keys.shift()!);
}
- takeFromLeft(lhs: BNode<K,V>) {
+ takeFromLeft(lhs: BNode<K, V>) {
// Reminder: parent node must update its copy of key for this node
// assert: neither node is shared
// assert rhs.keys.length > (maxNodeSize/2 && this.keys.length<maxNodeSize)
var v = this.values;
if (lhs.values === undefVals) {
- if (v !== undefVals)
- v.unshift(undefined as any);
+ if (v !== undefVals) v.unshift(undefined as any);
} else {
v = this.reifyValues();
v.unshift(lhs.values.pop()!);
@@ -1040,36 +1221,42 @@ class BNode<K,V> {
this.keys.unshift(lhs.keys.pop()!);
}
- splitOffRightSide(): BNode<K,V> {
+ splitOffRightSide(): BNode<K, V> {
// Reminder: parent node must update its copy of key for this node
- var half = this.keys.length >> 1, keys = this.keys.splice(half);
- var values = this.values === undefVals ? undefVals : this.values.splice(half);
- return new BNode<K,V>(keys, values);
+ var half = this.keys.length >> 1,
+ keys = this.keys.splice(half);
+ var values =
+ this.values === undefVals ? undefVals : this.values.splice(half);
+ return new BNode<K, V>(keys, values);
}
// Leaf Node: scanning & deletions //////////////////////////////////////////
- forRange<R>(low: K, high: K, includeHigh: boolean|undefined, editMode: boolean, tree: BTree<K,V>, count: number,
- onFound?: (k:K, v:V, counter:number) => EditRangeResult<V,R>|void): EditRangeResult<V,R>|number {
+ forRange<R>(
+ low: K,
+ high: K,
+ includeHigh: boolean | undefined,
+ editMode: boolean,
+ tree: BTree<K, V>,
+ count: number,
+ onFound?: (k: K, v: V, counter: number) => EditRangeResult<V, R> | void,
+ ): EditRangeResult<V, R> | number {
var cmp = tree._compare;
var iLow, iHigh;
if (high === low) {
- if (!includeHigh)
- return count;
+ if (!includeHigh) return count;
iHigh = (iLow = this.indexOf(low, -1, cmp)) + 1;
- if (iLow < 0)
- return count;
+ if (iLow < 0) return count;
} else {
iLow = this.indexOf(low, 0, cmp);
iHigh = this.indexOf(high, -1, cmp);
- if (iHigh < 0)
- iHigh = ~iHigh;
- else if (includeHigh === true)
- iHigh++;
+ if (iHigh < 0) iHigh = ~iHigh;
+ else if (includeHigh === true) iHigh++;
}
- var keys = this.keys, values = this.values;
+ var keys = this.keys,
+ values = this.values;
if (onFound !== undefined) {
- for(var i = iLow; i < iHigh; i++) {
+ for (var i = iLow; i < iHigh; i++) {
var key = keys[i];
var result = onFound(key, values[i], count++);
if (result !== undefined) {
@@ -1078,30 +1265,26 @@ class BNode<K,V> {
throw new Error("BTree illegally changed or cloned in editRange");
if (result.delete) {
this.keys.splice(i, 1);
- if (this.values !== undefVals)
- this.values.splice(i, 1);
+ if (this.values !== undefVals) this.values.splice(i, 1);
tree._size--;
i--;
iHigh--;
- } else if (result.hasOwnProperty('value')) {
+ } else if (result.hasOwnProperty("value")) {
values![i] = result.value!;
}
}
- if (result.break !== undefined)
- return result;
+ if (result.break !== undefined) return result;
}
}
- } else
- count += iHigh - iLow;
+ } else count += iHigh - iLow;
return count;
}
/** Adds entire contents of right-hand sibling (rhs is left unchanged) */
- mergeSibling(rhs: BNode<K,V>, _: number) {
+ mergeSibling(rhs: BNode<K, V>, _: number) {
this.keys.push.apply(this.keys, rhs.keys);
if (this.values === undefVals) {
- if (rhs.values === undefVals)
- return;
+ if (rhs.values === undefVals) return;
this.values = this.values.slice(0, this.keys.length);
}
this.values.push.apply(this.values, rhs.reifyValues());
@@ -1109,33 +1292,33 @@ class BNode<K,V> {
}
/** Internal node (non-leaf node) ********************************************/
-class BNodeInternal<K,V> extends BNode<K,V> {
- // Note: conventionally B+ trees have one fewer key than the number of
+class BNodeInternal<K, V> extends BNode<K, V> {
+ // Note: conventionally B+ trees have one fewer key than the number of
// children, but I find it easier to keep the array lengths equal: each
// keys[i] caches the value of children[i].maxKey().
- children: BNode<K,V>[];
+ children: BNode<K, V>[];
- constructor(children: BNode<K,V>[], keys?: K[]) {
+ constructor(children: BNode<K, V>[], keys?: K[]) {
if (!keys) {
keys = [];
- for (var i = 0; i < children.length; i++)
- keys[i] = children[i].maxKey();
+ for (var i = 0; i < children.length; i++) keys[i] = children[i].maxKey();
}
super(keys);
this.children = children;
}
- clone(): BNode<K,V> {
+ clone(): BNode<K, V> {
var children = this.children.slice(0);
- for (var i = 0; i < children.length; i++)
- children[i].isShared = true;
- return new BNodeInternal<K,V>(children, this.keys.slice(0));
+ for (var i = 0; i < children.length; i++) children[i].isShared = true;
+ return new BNodeInternal<K, V>(children, this.keys.slice(0));
}
- greedyClone(force?: boolean): BNode<K,V> {
- if (this.isShared && !force)
- return this;
- var nu = new BNodeInternal<K,V>(this.children.slice(0), this.keys.slice(0));
+ greedyClone(force?: boolean): BNode<K, V> {
+ if (this.isShared && !force) return this;
+ var nu = new BNodeInternal<K, V>(
+ this.children.slice(0),
+ this.keys.slice(0),
+ );
for (var i = 0; i < nu.children.length; i++)
nu.children[i] = nu.children[i].greedyClone();
return nu;
@@ -1145,141 +1328,229 @@ class BNodeInternal<K,V> extends BNode<K,V> {
return this.children[0].minKey();
}
- get(key: K, defaultValue: V|undefined, tree: BTree<K,V>): V|undefined {
- var i = this.indexOf(key, 0, tree._compare), children = this.children;
- return i < children.length ? children[i].get(key, defaultValue, tree) : undefined;
- }
-
- checkValid(depth: number, tree: BTree<K,V>) : number {
- var kL = this.keys.length, cL = this.children.length;
- check(kL === cL, "keys/children length mismatch: depth", depth, "lengths", kL, cL);
+ get(key: K, defaultValue: V | undefined, tree: BTree<K, V>): V | undefined {
+ var i = this.indexOf(key, 0, tree._compare),
+ children = this.children;
+ return i < children.length
+ ? children[i].get(key, defaultValue, tree)
+ : undefined;
+ }
+
+ checkValid(depth: number, tree: BTree<K, V>): number {
+ var kL = this.keys.length,
+ cL = this.children.length;
+ check(
+ kL === cL,
+ "keys/children length mismatch: depth",
+ depth,
+ "lengths",
+ kL,
+ cL,
+ );
check(kL > 1, "internal node has length", kL, "at depth", depth);
- var size = 0, c = this.children, k = this.keys, childSize = 0;
+ var size = 0,
+ c = this.children,
+ k = this.keys,
+ childSize = 0;
for (var i = 0; i < cL; i++) {
size += c[i].checkValid(depth + 1, tree);
childSize += c[i].keys.length;
check(size >= childSize, "wtf"); // no way this will ever fail
- check(i === 0 || c[i-1].constructor === c[i].constructor, "type mismatch");
+ check(
+ i === 0 || c[i - 1].constructor === c[i].constructor,
+ "type mismatch",
+ );
if (c[i].maxKey() != k[i])
- check(false, "keys[", i, "] =", k[i], "is wrong, should be ", c[i].maxKey(), "at depth", depth);
- if (!(i === 0 || tree._compare(k[i-1], k[i]) < 0))
- check(false, "sort violation at depth", depth, "index", i, "keys", k[i-1], k[i]);
+ check(
+ false,
+ "keys[",
+ i,
+ "] =",
+ k[i],
+ "is wrong, should be ",
+ c[i].maxKey(),
+ "at depth",
+ depth,
+ );
+ if (!(i === 0 || tree._compare(k[i - 1], k[i]) < 0))
+ check(
+ false,
+ "sort violation at depth",
+ depth,
+ "index",
+ i,
+ "keys",
+ k[i - 1],
+ k[i],
+ );
}
- var toofew = childSize < (tree.maxNodeSize >> 1)*cL;
- if (toofew || childSize > tree.maxNodeSize*cL)
- check(false, toofew ? "too few" : "too many", "children (", childSize, size, ") at depth", depth, ", maxNodeSize:", tree.maxNodeSize, "children.length:", cL);
+ var toofew = childSize < (tree.maxNodeSize >> 1) * cL;
+ if (toofew || childSize > tree.maxNodeSize * cL)
+ check(
+ false,
+ toofew ? "too few" : "too many",
+ "children (",
+ childSize,
+ size,
+ ") at depth",
+ depth,
+ ", maxNodeSize:",
+ tree.maxNodeSize,
+ "children.length:",
+ cL,
+ );
return size;
}
// Internal Node: set & node splitting //////////////////////////////////////
- set(key: K, value: V, overwrite: boolean|undefined, tree: BTree<K,V>): boolean|BNodeInternal<K,V> {
- var c = this.children, max = tree._maxNodeSize, cmp = tree._compare;
- var i = Math.min(this.indexOf(key, 0, cmp), c.length - 1), child = c[i];
-
- if (child.isShared)
- c[i] = child = child.clone();
+ set(
+ key: K,
+ value: V,
+ overwrite: boolean | undefined,
+ tree: BTree<K, V>,
+ ): boolean | BNodeInternal<K, V> {
+ var c = this.children,
+ max = tree._maxNodeSize,
+ cmp = tree._compare;
+ var i = Math.min(this.indexOf(key, 0, cmp), c.length - 1),
+ child = c[i];
+
+ if (child.isShared) c[i] = child = child.clone();
if (child.keys.length >= max) {
// child is full; inserting anything else will cause a split.
// Shifting an item to the left or right sibling may avoid a split.
// We can do a shift if the adjacent node is not full and if the
// current key can still be placed in the same node after the shift.
- var other: BNode<K,V>;
- if (i > 0 && (other = c[i-1]).keys.length < max && cmp(child.keys[0], key) < 0) {
- if (other.isShared)
- c[i-1] = other = other.clone();
+ var other: BNode<K, V>;
+ if (
+ i > 0 &&
+ (other = c[i - 1]).keys.length < max &&
+ cmp(child.keys[0], key) < 0
+ ) {
+ if (other.isShared) c[i - 1] = other = other.clone();
other.takeFromRight(child);
- this.keys[i-1] = other.maxKey();
- } else if ((other = c[i+1]) !== undefined && other.keys.length < max && cmp(child.maxKey(), key) < 0) {
- if (other.isShared)
- c[i+1] = other = other.clone();
+ this.keys[i - 1] = other.maxKey();
+ } else if (
+ (other = c[i + 1]) !== undefined &&
+ other.keys.length < max &&
+ cmp(child.maxKey(), key) < 0
+ ) {
+ if (other.isShared) c[i + 1] = other = other.clone();
other.takeFromLeft(child);
this.keys[i] = c[i].maxKey();
}
}
var result = child.set(key, value, overwrite, tree);
- if (result === false)
- return false;
+ if (result === false) return false;
this.keys[i] = child.maxKey();
- if (result === true)
- return true;
+ if (result === true) return true;
// The child has split and `result` is a new right child... does it fit?
- if (this.keys.length < max) { // yes
- this.insert(i+1, result);
+ if (this.keys.length < max) {
+ // yes
+ this.insert(i + 1, result);
return true;
- } else { // no, we must split also
- var newRightSibling = this.splitOffRightSide(), target: BNodeInternal<K,V> = this;
+ } else {
+ // no, we must split also
+ var newRightSibling = this.splitOffRightSide(),
+ target: BNodeInternal<K, V> = this;
if (cmp(result.maxKey(), this.maxKey()) > 0) {
target = newRightSibling;
i -= this.keys.length;
}
- target.insert(i+1, result);
+ target.insert(i + 1, result);
return newRightSibling;
}
}
- insert(i: index, child: BNode<K,V>) {
+ insert(i: index, child: BNode<K, V>) {
this.children.splice(i, 0, child);
this.keys.splice(i, 0, child.maxKey());
}
splitOffRightSide() {
var half = this.children.length >> 1;
- return new BNodeInternal<K,V>(this.children.splice(half), this.keys.splice(half));
+ return new BNodeInternal<K, V>(
+ this.children.splice(half),
+ this.keys.splice(half),
+ );
}
- takeFromRight(rhs: BNode<K,V>) {
+ takeFromRight(rhs: BNode<K, V>) {
// Reminder: parent node must update its copy of key for this node
// assert: neither node is shared
// assert rhs.keys.length > (maxNodeSize/2 && this.keys.length<maxNodeSize)
this.keys.push(rhs.keys.shift()!);
- this.children.push((rhs as BNodeInternal<K,V>).children.shift()!);
+ this.children.push((rhs as BNodeInternal<K, V>).children.shift()!);
}
- takeFromLeft(lhs: BNode<K,V>) {
+ takeFromLeft(lhs: BNode<K, V>) {
// Reminder: parent node must update its copy of key for this node
// assert: neither node is shared
// assert rhs.keys.length > (maxNodeSize/2 && this.keys.length<maxNodeSize)
this.keys.unshift(lhs.keys.pop()!);
- this.children.unshift((lhs as BNodeInternal<K,V>).children.pop()!);
+ this.children.unshift((lhs as BNodeInternal<K, V>).children.pop()!);
}
// Internal Node: scanning & deletions //////////////////////////////////////
- forRange<R>(low: K, high: K, includeHigh: boolean|undefined, editMode: boolean, tree: BTree<K,V>, count: number,
- onFound?: (k:K, v:V, counter:number) => EditRangeResult<V,R>|void): EditRangeResult<V,R>|number
- {
+ forRange<R>(
+ low: K,
+ high: K,
+ includeHigh: boolean | undefined,
+ editMode: boolean,
+ tree: BTree<K, V>,
+ count: number,
+ onFound?: (k: K, v: V, counter: number) => EditRangeResult<V, R> | void,
+ ): EditRangeResult<V, R> | number {
var cmp = tree._compare;
- var iLow = this.indexOf(low, 0, cmp), i = iLow;
- var iHigh = Math.min(high === low ? iLow : this.indexOf(high, 0, cmp), this.keys.length-1);
- var keys = this.keys, children = this.children;
+ var iLow = this.indexOf(low, 0, cmp),
+ i = iLow;
+ var iHigh = Math.min(
+ high === low ? iLow : this.indexOf(high, 0, cmp),
+ this.keys.length - 1,
+ );
+ var keys = this.keys,
+ children = this.children;
if (!editMode) {
// Simple case
- for(; i <= iHigh; i++) {
- var result = children[i].forRange(low, high, includeHigh, editMode, tree, count, onFound);
- if (typeof result !== 'number')
- return result;
+ for (; i <= iHigh; i++) {
+ var result = children[i].forRange(
+ low,
+ high,
+ includeHigh,
+ editMode,
+ tree,
+ count,
+ onFound,
+ );
+ if (typeof result !== "number") return result;
count = result;
}
} else if (i <= iHigh) {
try {
- for(; i <= iHigh; i++) {
- if (children[i].isShared)
- children[i] = children[i].clone();
- var result = children[i].forRange(low, high, includeHigh, editMode, tree, count, onFound);
+ for (; i <= iHigh; i++) {
+ if (children[i].isShared) children[i] = children[i].clone();
+ var result = children[i].forRange(
+ low,
+ high,
+ includeHigh,
+ editMode,
+ tree,
+ count,
+ onFound,
+ );
keys[i] = children[i].maxKey();
- if (typeof result !== 'number')
- return result;
+ if (typeof result !== "number") return result;
count = result;
}
} finally {
// Deletions may have occurred, so look for opportunities to merge nodes.
var half = tree._maxNodeSize >> 1;
- if (iLow > 0)
- iLow--;
- for(i = iHigh; i >= iLow; i--) {
+ if (iLow > 0) iLow--;
+ for (i = iHigh; i >= iLow; i--) {
if (children[i].keys.length <= half)
this.tryMerge(i, tree._maxNodeSize);
}
@@ -1298,10 +1569,11 @@ class BNodeInternal<K,V> extends BNode<K,V> {
tryMerge(i: index, maxSize: number): boolean {
var children = this.children;
if (i >= 0 && i + 1 < children.length) {
- if (children[i].keys.length + children[i+1].keys.length <= maxSize) {
- if (children[i].isShared) // cloned already UNLESS i is outside scan range
+ if (children[i].keys.length + children[i + 1].keys.length <= maxSize) {
+ if (children[i].isShared)
+ // cloned already UNLESS i is outside scan range
children[i] = children[i].clone();
- children[i].mergeSibling(children[i+1], maxSize);
+ children[i].mergeSibling(children[i + 1], maxSize);
children.splice(i + 1, 1);
this.keys.splice(i + 1, 1);
this.keys[i] = children[i].maxKey();
@@ -1311,15 +1583,18 @@ class BNodeInternal<K,V> extends BNode<K,V> {
return false;
}
- mergeSibling(rhs: BNode<K,V>, maxNodeSize: number) {
+ mergeSibling(rhs: BNode<K, V>, maxNodeSize: number) {
// assert !this.isShared;
var oldLength = this.keys.length;
this.keys.push.apply(this.keys, rhs.keys);
- this.children.push.apply(this.children, (rhs as any as BNodeInternal<K,V>).children);
+ this.children.push.apply(
+ this.children,
+ ((rhs as any) as BNodeInternal<K, V>).children,
+ );
// If our children are themselves almost empty due to a mass-delete,
// they may need to be merged too (but only the oldLength-1 and its
// right sibling should need this).
- this.tryMerge(oldLength-1, maxNodeSize);
+ this.tryMerge(oldLength - 1, maxNodeSize);
}
}
@@ -1332,20 +1607,27 @@ class BNodeInternal<K,V> extends BNode<K,V> {
// users from making this array too large, BTree has a maximum node size.
var undefVals: any[] = [];
-const Delete = {delete: true}, DeleteRange = () => Delete;
-const Break = {break: true};
-const EmptyLeaf = (function() {
- var n = new BNode<any,any>(); n.isShared = true; return n;
+const Delete = { delete: true },
+ DeleteRange = () => Delete;
+const Break = { break: true };
+const EmptyLeaf = (function () {
+ var n = new BNode<any, any>();
+ n.isShared = true;
+ return n;
})();
const EmptyArray: any[] = [];
const ReusedArray: any[] = []; // assumed thread-local
function check(fact: boolean, ...args: any[]) {
if (!fact) {
- args.unshift('B+ tree '); // at beginning of message
- throw new Error(args.join(' '));
+ args.unshift("B+ tree "); // at beginning of message
+ throw new Error(args.join(" "));
}
}
/** A BTree frozen in the empty state. */
-export const EmptyBTree = (() => { let t = new BTree(); t.freeze(); return t; })();
+export const EmptyBTree = (() => {
+ let t = new BTree();
+ t.freeze();
+ return t;
+})();
diff --git a/packages/idb-bridge/src/tree/interfaces.ts b/packages/idb-bridge/src/tree/interfaces.ts
index 6bd0cdf58..ce8808d09 100644
--- a/packages/idb-bridge/src/tree/interfaces.ts
+++ b/packages/idb-bridge/src/tree/interfaces.ts
@@ -24,15 +24,13 @@ SPDX-License-Identifier: MIT
// Original repository: https://github.com/qwertie/btree-typescript
-
/** Read-only set interface (subinterface of IMapSource<K,any>).
* The word "set" usually means that each item in the collection is unique
- * (appears only once, based on a definition of equality used by the
- * collection.) Objects conforming to this interface aren't guaranteed not
- * to contain duplicates, but as an example, BTree<K,V> implements this
+ * (appears only once, based on a definition of equality used by the
+ * collection.) Objects conforming to this interface aren't guaranteed not
+ * to contain duplicates, but as an example, BTree<K,V> implements this
* interface and does not allow duplicates. */
-export interface ISetSource<K=any>
-{
+export interface ISetSource<K = any> {
/** Returns the number of key/value pairs in the map object. */
size: number;
/** Returns a boolean asserting whether the key exists in the map object or not. */
@@ -42,21 +40,23 @@ export interface ISetSource<K=any>
}
/** Read-only map interface (i.e. a source of key-value pairs). */
-export interface IMapSource<K=any, V=any> extends ISetSource<K>
-{
+export interface IMapSource<K = any, V = any> extends ISetSource<K> {
/** Returns the number of key/value pairs in the map object. */
size: number;
/** Returns the value associated to the key, or undefined if there is none. */
- get(key: K): V|undefined;
+ get(key: K): V | undefined;
/** Returns a boolean asserting whether the key exists in the map object or not. */
has(key: K): boolean;
/** Calls callbackFn once for each key-value pair present in the map object.
* The ES6 Map class sends the value to the callback before the key, so
* this interface must do likewise. */
- forEach(callbackFn: (v:V, k:K, map:IMapSource<K,V>) => void, thisArg?: any): void;
-
+ forEach(
+ callbackFn: (v: V, k: K, map: IMapSource<K, V>) => void,
+ thisArg?: any,
+ ): void;
+
/** Returns an iterator that provides all key-value pairs from the collection (as arrays of length 2). */
- entries(): IterableIterator<[K,V]>;
+ entries(): IterableIterator<[K, V]>;
/** Returns a new iterator for iterating the keys of each pair. */
keys(): IterableIterator<K>;
/** Returns a new iterator for iterating the values of each pair. */
@@ -65,14 +65,13 @@ export interface IMapSource<K=any, V=any> extends ISetSource<K>
//[Symbol.iterator](): IterableIterator<[K,V]>;
}
-/** Write-only set interface (the set cannot be queried, but items can be added to it.)
+/** Write-only set interface (the set cannot be queried, but items can be added to it.)
* @description Note: BTree<K,V> does not officially implement this interface,
* but BTree<K> can be used as an instance of ISetSink<K>. */
-export interface ISetSink<K=any>
-{
+export interface ISetSink<K = any> {
/** Adds the specified item to the set, if it was not in the set already. */
add(key: K): any;
- /** Returns true if an element in the map object existed and has been
+ /** Returns true if an element in the map object existed and has been
* removed, or false if the element did not exist. */
delete(key: K): boolean;
/** Removes everything so that the set is empty. */
@@ -80,12 +79,11 @@ export interface ISetSink<K=any>
}
/** Write-only map interface (i.e. a drain into which key-value pairs can be "sunk") */
-export interface IMapSink<K=any, V=any>
-{
- /** Returns true if an element in the map object existed and has been
+export interface IMapSink<K = any, V = any> {
+ /** Returns true if an element in the map object existed and has been
* removed, or false if the element did not exist. */
delete(key: K): boolean;
- /** Sets the value for the key in the map object (the return value is
+ /** Sets the value for the key in the map object (the return value is
* boolean in BTree but Map returns the Map itself.) */
set(key: K, value: V): any;
/** Removes all key/value pairs from the IMap object. */
@@ -95,119 +93,154 @@ export interface IMapSink<K=any, V=any>
/** Set interface.
* @description Note: BTree<K,V> does not officially implement this interface,
* but BTree<K> can be used as an instance of ISet<K>. */
-export interface ISet<K=any> extends ISetSource<K>, ISetSink<K> { }
+export interface ISet<K = any> extends ISetSource<K>, ISetSink<K> {}
/** An interface compatible with ES6 Map and BTree. This interface does not
- * describe the complete interface of either class, but merely the common
+ * describe the complete interface of either class, but merely the common
* interface shared by both. */
-export interface IMap<K=any, V=any> extends IMapSource<K, V>, IMapSink<K, V> { }
+export interface IMap<K = any, V = any>
+ extends IMapSource<K, V>,
+ IMapSink<K, V> {}
/** An data source that provides read-only access to a set of items called
* "keys" in sorted order. This is a subinterface of ISortedMapSource. */
-export interface ISortedSetSource<K=any> extends ISetSource<K>
-{
+export interface ISortedSetSource<K = any> extends ISetSource<K> {
/** Gets the lowest key in the collection. */
minKey(): K | undefined;
/** Gets the highest key in the collection. */
maxKey(): K | undefined;
/** Returns the next key larger than the specified key (or undefined if there is none) */
- nextHigherKey(key: K): K|undefined;
+ nextHigherKey(key: K): K | undefined;
/** Returns the next key smaller than the specified key (or undefined if there is none) */
- nextLowerKey(key: K): K|undefined;
+ nextLowerKey(key: K): K | undefined;
/** Calls `callback` on the specified range of keys, in ascending order by key.
* @param low The first key scanned will be greater than or equal to `low`.
* @param high Scanning stops when a key larger than this is reached.
- * @param includeHigh If the `high` key is present in the map, `onFound` is called
+ * @param includeHigh If the `high` key is present in the map, `onFound` is called
* for that final pair if and only if this parameter is true.
* @param onFound A function that is called for each key pair. Because this
- * is a subinterface of ISortedMapSource, if there is a value
+ * is a subinterface of ISortedMapSource, if there is a value
* associated with the key, it is passed as the second parameter.
- * @param initialCounter Initial third argument of `onFound`. This value
+ * @param initialCounter Initial third argument of `onFound`. This value
* increases by one each time `onFound` is called. Default: 0
* @returns Number of pairs found and the number of times `onFound` was called.
*/
- forRange(low: K, high: K, includeHigh: boolean, onFound?: (k:K,v:any,counter:number) => void, initialCounter?: number): number;
- /** Returns a new iterator for iterating the keys of each pair in ascending order.
+ forRange(
+ low: K,
+ high: K,
+ includeHigh: boolean,
+ onFound?: (k: K, v: any, counter: number) => void,
+ initialCounter?: number,
+ ): number;
+ /** Returns a new iterator for iterating the keys of each pair in ascending order.
* @param firstKey: Minimum key to include in the output. */
keys(firstKey?: K): IterableIterator<K>;
}
/** An data source that provides read-only access to items in sorted order. */
-export interface ISortedMapSource<K=any, V=any> extends IMapSource<K, V>, ISortedSetSource<K>
-{
+export interface ISortedMapSource<K = any, V = any>
+ extends IMapSource<K, V>,
+ ISortedSetSource<K> {
/** Returns the next pair whose key is larger than the specified key (or undefined if there is none) */
- nextHigherPair(key: K): [K,V]|undefined;
+ nextHigherPair(key: K): [K, V] | undefined;
/** Returns the next pair whose key is smaller than the specified key (or undefined if there is none) */
- nextLowerPair(key: K): [K,V]|undefined;
+ nextLowerPair(key: K): [K, V] | undefined;
/** Builds an array of pairs from the specified range of keys, sorted by key.
* Each returned pair is also an array: pair[0] is the key, pair[1] is the value.
* @param low The first key in the array will be greater than or equal to `low`.
* @param high This method returns when a key larger than this is reached.
* @param includeHigh If the `high` key is present in the map, its pair will be
- * included in the output if and only if this parameter is true. Note:
+ * included in the output if and only if this parameter is true. Note:
* if the `low` key is present, it is always included in the output.
* @param maxLength Maximum length of the returned array (default: unlimited)
* @description Computational complexity: O(result.length + log size)
*/
- getRange(low: K, high: K, includeHigh?: boolean, maxLength?: number): [K,V][];
+ getRange(
+ low: K,
+ high: K,
+ includeHigh?: boolean,
+ maxLength?: number,
+ ): [K, V][];
/** Calls `callback` on the specified range of keys, in ascending order by key.
* @param low The first key scanned will be greater than or equal to `low`.
* @param high Scanning stops when a key larger than this is reached.
- * @param includeHigh If the `high` key is present in the map, `onFound` is called
+ * @param includeHigh If the `high` key is present in the map, `onFound` is called
* for that final pair if and only if this parameter is true.
* @param onFound A function that is called for each key-value pair.
- * @param initialCounter Initial third argument of onFound. This value
+ * @param initialCounter Initial third argument of onFound. This value
* increases by one each time `onFound` is called. Default: 0
* @returns Number of pairs found and the number of times `callback` was called.
*/
- forRange(low: K, high: K, includeHigh: boolean, onFound?: (k:K,v:V,counter:number) => void, initialCounter?: number): number;
+ forRange(
+ low: K,
+ high: K,
+ includeHigh: boolean,
+ onFound?: (k: K, v: V, counter: number) => void,
+ initialCounter?: number,
+ ): number;
/** Returns an iterator that provides items in order by key.
* @param firstKey: Minimum key to include in the output. */
- entries(firstKey?: K): IterableIterator<[K,V]>;
- /** Returns a new iterator for iterating the keys of each pair in ascending order.
+ entries(firstKey?: K): IterableIterator<[K, V]>;
+ /** Returns a new iterator for iterating the keys of each pair in ascending order.
* @param firstKey: Minimum key to include in the output. */
keys(firstKey?: K): IterableIterator<K>;
- /** Returns a new iterator for iterating the values of each pair in order by key.
+ /** Returns a new iterator for iterating the values of each pair in order by key.
* @param firstKey: Minimum key whose associated value is included in the output. */
values(firstKey?: K): IterableIterator<V>;
-
+
// This method should logically be in IMapSource but is not supported by ES6 Map
- /** Performs a reduce operation like the `reduce` method of `Array`.
+ /** Performs a reduce operation like the `reduce` method of `Array`.
* It is used to combine all pairs into a single value, or perform conversions. */
- reduce<R>(callback: (previous:R,currentPair:[K,V],counter:number,tree:IMapF<K,V>) => R, initialValue: R): R;
- /** Performs a reduce operation like the `reduce` method of `Array`.
+ reduce<R>(
+ callback: (
+ previous: R,
+ currentPair: [K, V],
+ counter: number,
+ tree: IMapF<K, V>,
+ ) => R,
+ initialValue: R,
+ ): R;
+ /** Performs a reduce operation like the `reduce` method of `Array`.
* It is used to combine all pairs into a single value, or perform conversions. */
- reduce<R>(callback: (previous:R|undefined,currentPair:[K,V],counter:number,tree:IMapF<K,V>) => R): R|undefined;
+ reduce<R>(
+ callback: (
+ previous: R | undefined,
+ currentPair: [K, V],
+ counter: number,
+ tree: IMapF<K, V>,
+ ) => R,
+ ): R | undefined;
}
/** An interface for a set of keys (the combination of ISortedSetSource<K> and ISetSink<K>) */
-export interface ISortedSet<K=any> extends ISortedSetSource<K>, ISetSink<K> { }
+export interface ISortedSet<K = any> extends ISortedSetSource<K>, ISetSink<K> {}
-/** An interface for a sorted map (dictionary),
+/** An interface for a sorted map (dictionary),
* not including functional/persistent methods. */
-export interface ISortedMap<K=any, V=any> extends IMap<K,V>, ISortedMapSource<K, V>
-{
+export interface ISortedMap<K = any, V = any>
+ extends IMap<K, V>,
+ ISortedMapSource<K, V> {
// All of the following methods should be in IMap but are left out of IMap
// so that IMap is compatible with ES6 Map.
/** Adds or overwrites a key-value pair in the sorted map.
* @param key the key is used to determine the sort order of data in the tree.
* @param value data to associate with the key
- * @param overwrite Whether to overwrite an existing key-value pair
+ * @param overwrite Whether to overwrite an existing key-value pair
* (default: true). If this is false and there is an existing
* key-value pair then the call to this method has no effect.
- * @returns true if a new key-value pair was added, false if the key
+ * @returns true if a new key-value pair was added, false if the key
* already existed. */
set(key: K, value: V, overwrite?: boolean): boolean;
/** Adds all pairs from a list of key-value pairs.
- * @param pairs Pairs to add to this tree. If there are duplicate keys,
- * later pairs currently overwrite earlier ones (e.g. [[0,1],[0,7]]
+ * @param pairs Pairs to add to this tree. If there are duplicate keys,
+ * later pairs currently overwrite earlier ones (e.g. [[0,1],[0,7]]
* associates 0 with 7.)
* @param overwrite Whether to overwrite pairs that already exist (if false,
* pairs[i] is ignored when the key pairs[i][0] already exists.)
* @returns The number of pairs added to the collection.
*/
- setPairs(pairs: [K,V][], overwrite?: boolean): number;
+ setPairs(pairs: [K, V][], overwrite?: boolean): number;
/** Deletes a series of keys from the collection. */
deleteKeys(keys: K[]): number;
/** Removes a range of key-value pairs from the B+ tree.
@@ -218,18 +251,18 @@ export interface ISortedMap<K=any, V=any> extends IMap<K,V>, ISortedMapSource<K,
deleteRange(low: K, high: K, includeHigh: boolean): number;
// TypeScript requires these methods of ISortedMapSource to be repeated
- entries(firstKey?: K): IterableIterator<[K,V]>;
+ entries(firstKey?: K): IterableIterator<[K, V]>;
keys(firstKey?: K): IterableIterator<K>;
values(firstKey?: K): IterableIterator<V>;
}
-/** An interface for a functional set, in which the set object could be read-only
- * but new versions of the set can be created by calling "with" or "without"
+/** An interface for a functional set, in which the set object could be read-only
+ * but new versions of the set can be created by calling "with" or "without"
* methods to add or remove keys. This is a subinterface of IMapF<K,V>,
* so the items in the set may be referred to as "keys". */
-export interface ISetF<K=any> extends ISetSource<K> {
- /** Returns a copy of the set with the specified key included.
- * @description You might wonder why this method accepts only one key
+export interface ISetF<K = any> extends ISetSource<K> {
+ /** Returns a copy of the set with the specified key included.
+ * @description You might wonder why this method accepts only one key
* instead of `...keys: K[]`. The reason is that the derived interface
* IMapF expects the second parameter to be a value. Therefore
* withKeys() is provided to set multiple keys at once. */
@@ -239,91 +272,133 @@ export interface ISetF<K=any> extends ISetSource<K> {
/** Returns a copy of the tree with all the keys in the specified array present.
* @param keys The keys to add.
* @param returnThisIfUnchanged If true, the method returns `this` when
- * all of the keys are already present in the collection. The
+ * all of the keys are already present in the collection. The
* default value may be true or false depending on the concrete
* implementation of the interface (in BTree, the default is false.) */
withKeys(keys: K[], returnThisIfUnchanged?: boolean): ISetF<K>;
/** Returns a copy of the tree with all the keys in the specified array removed. */
withoutKeys(keys: K[], returnThisIfUnchanged?: boolean): ISetF<K>;
- /** Returns a copy of the tree with items removed whenever the callback
+ /** Returns a copy of the tree with items removed whenever the callback
* function returns false.
* @param callback A function to call for each item in the set.
* The second parameter to `callback` exists because ISetF
* is a subinterface of IMapF. If the object is a map, v
* is the value associated with the key, otherwise v could be
* undefined or another copy of the third parameter (counter). */
- filter(callback: (k:K,v:any,counter:number) => boolean, returnThisIfUnchanged?: boolean): ISetF<K>;
+ filter(
+ callback: (k: K, v: any, counter: number) => boolean,
+ returnThisIfUnchanged?: boolean,
+ ): ISetF<K>;
}
/** An interface for a functional map, in which the map object could be read-only
- * but new versions of the map can be created by calling "with" or "without"
- * methods to add or remove keys or key-value pairs.
+ * but new versions of the map can be created by calling "with" or "without"
+ * methods to add or remove keys or key-value pairs.
*/
-export interface IMapF<K=any, V=any> extends IMapSource<K, V>, ISetF<K> {
+export interface IMapF<K = any, V = any> extends IMapSource<K, V>, ISetF<K> {
/** Returns a copy of the tree with the specified key set (the value is undefined). */
- with(key: K): IMapF<K,V|undefined>;
+ with(key: K): IMapF<K, V | undefined>;
/** Returns a copy of the tree with the specified key-value pair set. */
- with<V2>(key: K, value: V2, overwrite?: boolean): IMapF<K,V|V2>;
+ with<V2>(key: K, value: V2, overwrite?: boolean): IMapF<K, V | V2>;
/** Returns a copy of the tree with the specified key-value pairs set. */
- withPairs<V2>(pairs: [K,V|V2][], overwrite: boolean): IMapF<K,V|V2>;
+ withPairs<V2>(pairs: [K, V | V2][], overwrite: boolean): IMapF<K, V | V2>;
/** Returns a copy of the tree with all the keys in the specified array present.
* @param keys The keys to add. If a key is already present in the tree,
- * neither the existing key nor the existing value is modified.
+ * neither the existing key nor the existing value is modified.
* @param returnThisIfUnchanged If true, the method returns `this` when
- * all of the keys are already present in the collection. The
+ * all of the keys are already present in the collection. The
* default value may be true or false depending on the concrete
* implementation of the interface (in BTree, the default is false.) */
- withKeys(keys: K[], returnThisIfUnchanged?: boolean): IMapF<K,V|undefined>;
+ withKeys(keys: K[], returnThisIfUnchanged?: boolean): IMapF<K, V | undefined>;
/** Returns a copy of the tree with all values altered by a callback function. */
- mapValues<R>(callback: (v:V,k:K,counter:number) => R): IMapF<K,R>;
- /** Performs a reduce operation like the `reduce` method of `Array`.
+ mapValues<R>(callback: (v: V, k: K, counter: number) => R): IMapF<K, R>;
+ /** Performs a reduce operation like the `reduce` method of `Array`.
* It is used to combine all pairs into a single value, or perform conversions. */
- reduce<R>(callback: (previous:R,currentPair:[K,V],counter:number,tree:IMapF<K,V>) => R, initialValue: R): R;
- /** Performs a reduce operation like the `reduce` method of `Array`.
+ reduce<R>(
+ callback: (
+ previous: R,
+ currentPair: [K, V],
+ counter: number,
+ tree: IMapF<K, V>,
+ ) => R,
+ initialValue: R,
+ ): R;
+ /** Performs a reduce operation like the `reduce` method of `Array`.
* It is used to combine all pairs into a single value, or perform conversions. */
- reduce<R>(callback: (previous:R|undefined,currentPair:[K,V],counter:number,tree:IMapF<K,V>) => R): R|undefined;
+ reduce<R>(
+ callback: (
+ previous: R | undefined,
+ currentPair: [K, V],
+ counter: number,
+ tree: IMapF<K, V>,
+ ) => R,
+ ): R | undefined;
// Update return types in ISetF
- without(key: K): IMapF<K,V>;
- withoutKeys(keys: K[], returnThisIfUnchanged?: boolean): IMapF<K,V>;
- /** Returns a copy of the tree with pairs removed whenever the callback
+ without(key: K): IMapF<K, V>;
+ withoutKeys(keys: K[], returnThisIfUnchanged?: boolean): IMapF<K, V>;
+ /** Returns a copy of the tree with pairs removed whenever the callback
* function returns false. */
- filter(callback: (k:K,v:V,counter:number) => boolean, returnThisIfUnchanged?: boolean): IMapF<K,V>;
+ filter(
+ callback: (k: K, v: V, counter: number) => boolean,
+ returnThisIfUnchanged?: boolean,
+ ): IMapF<K, V>;
}
-/** An interface for a functional sorted set: a functional set in which the
+/** An interface for a functional sorted set: a functional set in which the
* keys (items) are sorted. This is a subinterface of ISortedMapF. */
-export interface ISortedSetF<K=any> extends ISetF<K>, ISortedSetSource<K>
-{
+export interface ISortedSetF<K = any> extends ISetF<K>, ISortedSetSource<K> {
// TypeScript requires this method of ISortedSetSource to be repeated
keys(firstKey?: K): IterableIterator<K>;
}
-export interface ISortedMapF<K=any,V=any> extends ISortedSetF<K>, IMapF<K,V>, ISortedMapSource<K,V>
-{
+export interface ISortedMapF<K = any, V = any>
+ extends ISortedSetF<K>,
+ IMapF<K, V>,
+ ISortedMapSource<K, V> {
/** Returns a copy of the tree with the specified range of keys removed. */
- withoutRange(low: K, high: K, includeHigh: boolean, returnThisIfUnchanged?: boolean): ISortedMapF<K,V>;
+ withoutRange(
+ low: K,
+ high: K,
+ includeHigh: boolean,
+ returnThisIfUnchanged?: boolean,
+ ): ISortedMapF<K, V>;
// TypeScript requires these methods of ISortedSetF and ISortedMapSource to be repeated
- entries(firstKey?: K): IterableIterator<[K,V]>;
+ entries(firstKey?: K): IterableIterator<[K, V]>;
keys(firstKey?: K): IterableIterator<K>;
values(firstKey?: K): IterableIterator<V>;
- forRange(low: K, high: K, includeHigh: boolean, onFound?: (k:K,v:V,counter:number) => void, initialCounter?: number): number;
+ forRange(
+ low: K,
+ high: K,
+ includeHigh: boolean,
+ onFound?: (k: K, v: V, counter: number) => void,
+ initialCounter?: number,
+ ): number;
// Update the return value of methods from base interfaces
- with(key: K): ISortedMapF<K,V|undefined>;
- with<V2>(key: K, value: V2, overwrite?: boolean): ISortedMapF<K,V|V2>;
- withKeys(keys: K[], returnThisIfUnchanged?: boolean): ISortedMapF<K,V|undefined>;
- withPairs<V2>(pairs: [K,V|V2][], overwrite: boolean): ISortedMapF<K,V|V2>;
- mapValues<R>(callback: (v:V,k:K,counter:number) => R): ISortedMapF<K,R>;
- without(key: K): ISortedMapF<K,V>;
- withoutKeys(keys: K[], returnThisIfUnchanged?: boolean): ISortedMapF<K,V>;
- filter(callback: (k:K,v:any,counter:number) => boolean, returnThisIfUnchanged?: boolean): ISortedMapF<K,V>;
+ with(key: K): ISortedMapF<K, V | undefined>;
+ with<V2>(key: K, value: V2, overwrite?: boolean): ISortedMapF<K, V | V2>;
+ withKeys(
+ keys: K[],
+ returnThisIfUnchanged?: boolean,
+ ): ISortedMapF<K, V | undefined>;
+ withPairs<V2>(
+ pairs: [K, V | V2][],
+ overwrite: boolean,
+ ): ISortedMapF<K, V | V2>;
+ mapValues<R>(callback: (v: V, k: K, counter: number) => R): ISortedMapF<K, R>;
+ without(key: K): ISortedMapF<K, V>;
+ withoutKeys(keys: K[], returnThisIfUnchanged?: boolean): ISortedMapF<K, V>;
+ filter(
+ callback: (k: K, v: any, counter: number) => boolean,
+ returnThisIfUnchanged?: boolean,
+ ): ISortedMapF<K, V>;
}
-export interface ISortedMapConstructor<K,V> {
- new (entries?: [K,V][], compare?: (a: K, b: K) => number): ISortedMap<K,V>;
+export interface ISortedMapConstructor<K, V> {
+ new (entries?: [K, V][], compare?: (a: K, b: K) => number): ISortedMap<K, V>;
+}
+export interface ISortedMapFConstructor<K, V> {
+ new (entries?: [K, V][], compare?: (a: K, b: K) => number): ISortedMapF<K, V>;
}
-export interface ISortedMapFConstructor<K,V> {
- new (entries?: [K,V][], compare?: (a: K, b: K) => number): ISortedMapF<K,V>;
-} \ No newline at end of file
diff --git a/packages/idb-bridge/src/util/FakeEvent.ts b/packages/idb-bridge/src/util/FakeEvent.ts
index ae62401c3..4457498f6 100644
--- a/packages/idb-bridge/src/util/FakeEvent.ts
+++ b/packages/idb-bridge/src/util/FakeEvent.ts
@@ -14,67 +14,64 @@
permissions and limitations under the License.
*/
-
import FakeEventTarget from "./FakeEventTarget";
import { EventType } from "./types";
export class Event {
- public eventPath: FakeEventTarget[] = [];
- public type: EventType;
-
- public readonly NONE = 0;
- public readonly CAPTURING_PHASE = 1;
- public readonly AT_TARGET = 2;
- public readonly BUBBLING_PHASE = 3;
-
- // Flags
- public propagationStopped = false;
- public immediatePropagationStopped = false;
- public canceled = false;
- public initialized = true;
- public dispatched = false;
-
- public target: FakeEventTarget | null = null;
- public currentTarget: FakeEventTarget | null = null;
-
- public eventPhase: 0 | 1 | 2 | 3 = 0;
-
- public defaultPrevented = false;
-
- public isTrusted = false;
- public timeStamp = Date.now();
-
- public bubbles: boolean;
- public cancelable: boolean;
-
- constructor(
- type: EventType,
- eventInitDict: { bubbles?: boolean; cancelable?: boolean } = {},
- ) {
- this.type = type;
-
- this.bubbles =
- eventInitDict.bubbles !== undefined ? eventInitDict.bubbles : false;
- this.cancelable =
- eventInitDict.cancelable !== undefined
- ? eventInitDict.cancelable
- : false;
- }
+ public eventPath: FakeEventTarget[] = [];
+ public type: EventType;
- public preventDefault() {
- if (this.cancelable) {
- this.canceled = true;
- }
- }
+ public readonly NONE = 0;
+ public readonly CAPTURING_PHASE = 1;
+ public readonly AT_TARGET = 2;
+ public readonly BUBBLING_PHASE = 3;
- public stopPropagation() {
- this.propagationStopped = true;
- }
+ // Flags
+ public propagationStopped = false;
+ public immediatePropagationStopped = false;
+ public canceled = false;
+ public initialized = true;
+ public dispatched = false;
+
+ public target: FakeEventTarget | null = null;
+ public currentTarget: FakeEventTarget | null = null;
+
+ public eventPhase: 0 | 1 | 2 | 3 = 0;
+
+ public defaultPrevented = false;
- public stopImmediatePropagation() {
- this.propagationStopped = true;
- this.immediatePropagationStopped = true;
+ public isTrusted = false;
+ public timeStamp = Date.now();
+
+ public bubbles: boolean;
+ public cancelable: boolean;
+
+ constructor(
+ type: EventType,
+ eventInitDict: { bubbles?: boolean; cancelable?: boolean } = {},
+ ) {
+ this.type = type;
+
+ this.bubbles =
+ eventInitDict.bubbles !== undefined ? eventInitDict.bubbles : false;
+ this.cancelable =
+ eventInitDict.cancelable !== undefined ? eventInitDict.cancelable : false;
+ }
+
+ public preventDefault() {
+ if (this.cancelable) {
+ this.canceled = true;
}
+ }
+
+ public stopPropagation() {
+ this.propagationStopped = true;
+ }
+
+ public stopImmediatePropagation() {
+ this.propagationStopped = true;
+ this.immediatePropagationStopped = true;
+ }
}
export default Event;
diff --git a/packages/idb-bridge/src/util/FakeEventTarget.ts b/packages/idb-bridge/src/util/FakeEventTarget.ts
index 025f21b4c..291eaca7d 100644
--- a/packages/idb-bridge/src/util/FakeEventTarget.ts
+++ b/packages/idb-bridge/src/util/FakeEventTarget.ts
@@ -117,7 +117,7 @@ abstract class FakeEventTarget {
callback: EventCallback,
capture = false,
) {
- const i = this.listeners.findIndex(listener => {
+ const i = this.listeners.findIndex((listener) => {
return (
listener.type === type &&
listener.callback === callback &&
diff --git a/packages/idb-bridge/src/util/cmp.ts b/packages/idb-bridge/src/util/cmp.ts
index 9d0dc99a2..ddd43f2a6 100644
--- a/packages/idb-bridge/src/util/cmp.ts
+++ b/packages/idb-bridge/src/util/cmp.ts
@@ -18,91 +18,91 @@ import { DataError } from "./errors";
import valueToKey from "./valueToKey";
const getType = (x: any) => {
- if (typeof x === "number") {
- return "Number";
- }
- if (x instanceof Date) {
- return "Date";
- }
- if (Array.isArray(x)) {
- return "Array";
- }
- if (typeof x === "string") {
- return "String";
- }
- if (x instanceof ArrayBuffer) {
- return "Binary";
- }
-
- throw new DataError();
+ if (typeof x === "number") {
+ return "Number";
+ }
+ if (x instanceof Date) {
+ return "Date";
+ }
+ if (Array.isArray(x)) {
+ return "Array";
+ }
+ if (typeof x === "string") {
+ return "String";
+ }
+ if (x instanceof ArrayBuffer) {
+ return "Binary";
+ }
+
+ throw new DataError();
};
// https://w3c.github.io/IndexedDB/#compare-two-keys
const compareKeys = (first: any, second: any): -1 | 0 | 1 => {
- if (second === undefined) {
- throw new TypeError();
- }
+ if (second === undefined) {
+ throw new TypeError();
+ }
- first = valueToKey(first);
- second = valueToKey(second);
-
- const t1 = getType(first);
- const t2 = getType(second);
-
- if (t1 !== t2) {
- if (t1 === "Array") {
- return 1;
- }
- if (
- t1 === "Binary" &&
- (t2 === "String" || t2 === "Date" || t2 === "Number")
- ) {
- return 1;
- }
- if (t1 === "String" && (t2 === "Date" || t2 === "Number")) {
- return 1;
- }
- if (t1 === "Date" && t2 === "Number") {
- return 1;
- }
- return -1;
- }
+ first = valueToKey(first);
+ second = valueToKey(second);
- if (t1 === "Binary") {
- first = new Uint8Array(first);
- second = new Uint8Array(second);
+ const t1 = getType(first);
+ const t2 = getType(second);
+
+ if (t1 !== t2) {
+ if (t1 === "Array") {
+ return 1;
+ }
+ if (
+ t1 === "Binary" &&
+ (t2 === "String" || t2 === "Date" || t2 === "Number")
+ ) {
+ return 1;
+ }
+ if (t1 === "String" && (t2 === "Date" || t2 === "Number")) {
+ return 1;
+ }
+ if (t1 === "Date" && t2 === "Number") {
+ return 1;
+ }
+ return -1;
+ }
+
+ if (t1 === "Binary") {
+ first = new Uint8Array(first);
+ second = new Uint8Array(second);
+ }
+
+ if (t1 === "Array" || t1 === "Binary") {
+ const length = Math.min(first.length, second.length);
+ for (let i = 0; i < length; i++) {
+ const result = compareKeys(first[i], second[i]);
+
+ if (result !== 0) {
+ return result;
+ }
}
- if (t1 === "Array" || t1 === "Binary") {
- const length = Math.min(first.length, second.length);
- for (let i = 0; i < length; i++) {
- const result = compareKeys(first[i], second[i]);
-
- if (result !== 0) {
- return result;
- }
- }
-
- if (first.length > second.length) {
- return 1;
- }
- if (first.length < second.length) {
- return -1;
- }
- return 0;
+ if (first.length > second.length) {
+ return 1;
+ }
+ if (first.length < second.length) {
+ return -1;
}
+ return 0;
+ }
- if (t1 === "Date") {
- if (first.getTime() === second.getTime()) {
- return 0;
- }
- } else {
- if (first === second) {
- return 0;
- }
+ if (t1 === "Date") {
+ if (first.getTime() === second.getTime()) {
+ return 0;
+ }
+ } else {
+ if (first === second) {
+ return 0;
}
+ }
- return first > second ? 1 : -1;
+ return first > second ? 1 : -1;
};
export default compareKeys;
diff --git a/packages/idb-bridge/src/util/enforceRange.ts b/packages/idb-bridge/src/util/enforceRange.ts
index 9ac472757..87e135798 100644
--- a/packages/idb-bridge/src/util/enforceRange.ts
+++ b/packages/idb-bridge/src/util/enforceRange.ts
@@ -18,18 +18,18 @@
// https://heycam.github.io/webidl/#EnforceRange
const enforceRange = (
- num: number,
- type: "MAX_SAFE_INTEGER" | "unsigned long",
+ num: number,
+ type: "MAX_SAFE_INTEGER" | "unsigned long",
) => {
- const min = 0;
- const max = type === "unsigned long" ? 4294967295 : 9007199254740991;
+ const min = 0;
+ const max = type === "unsigned long" ? 4294967295 : 9007199254740991;
- if (isNaN(num) || num < min || num > max) {
- throw new TypeError();
- }
- if (num >= 0) {
- return Math.floor(num);
- }
+ if (isNaN(num) || num < min || num > max) {
+ throw new TypeError();
+ }
+ if (num >= 0) {
+ return Math.floor(num);
+ }
};
export default enforceRange;
diff --git a/packages/idb-bridge/src/util/errors.ts b/packages/idb-bridge/src/util/errors.ts
index 92de2ea90..6c8f81811 100644
--- a/packages/idb-bridge/src/util/errors.ts
+++ b/packages/idb-bridge/src/util/errors.ts
@@ -14,117 +14,116 @@
permissions and limitations under the License.
*/
-
/* tslint:disable: max-classes-per-file max-line-length */
const messages = {
- AbortError:
- "A request was aborted, for example through a call to IDBTransaction.abort.",
- ConstraintError:
- "A mutation operation in the transaction failed because a constraint was not satisfied. For example, an object such as an object store or index already exists and a request attempted to create a new one.",
- DataCloneError:
- "The data being stored could not be cloned by the internal structured cloning algorithm.",
- DataError: "Data provided to an operation does not meet requirements.",
- InvalidAccessError:
- "An invalid operation was performed on an object. For example transaction creation attempt was made, but an empty scope was provided.",
- InvalidStateError:
- "An operation was called on an object on which it is not allowed or at a time when it is not allowed. Also occurs if a request is made on a source object that has been deleted or removed. Use TransactionInactiveError or ReadOnlyError when possible, as they are more specific variations of InvalidStateError.",
- NotFoundError:
- "The operation failed because the requested database object could not be found. For example, an object store did not exist but was being opened.",
- ReadOnlyError:
- 'The mutating operation was attempted in a "readonly" transaction.',
- TransactionInactiveError:
- "A request was placed against a transaction which is currently not active, or which is finished.",
- VersionError:
- "An attempt was made to open a database using a lower version than the existing version.",
+ AbortError:
+ "A request was aborted, for example through a call to IDBTransaction.abort.",
+ ConstraintError:
+ "A mutation operation in the transaction failed because a constraint was not satisfied. For example, an object such as an object store or index already exists and a request attempted to create a new one.",
+ DataCloneError:
+ "The data being stored could not be cloned by the internal structured cloning algorithm.",
+ DataError: "Data provided to an operation does not meet requirements.",
+ InvalidAccessError:
+ "An invalid operation was performed on an object. For example transaction creation attempt was made, but an empty scope was provided.",
+ InvalidStateError:
+ "An operation was called on an object on which it is not allowed or at a time when it is not allowed. Also occurs if a request is made on a source object that has been deleted or removed. Use TransactionInactiveError or ReadOnlyError when possible, as they are more specific variations of InvalidStateError.",
+ NotFoundError:
+ "The operation failed because the requested database object could not be found. For example, an object store did not exist but was being opened.",
+ ReadOnlyError:
+ 'The mutating operation was attempted in a "readonly" transaction.',
+ TransactionInactiveError:
+ "A request was placed against a transaction which is currently not active, or which is finished.",
+ VersionError:
+ "An attempt was made to open a database using a lower version than the existing version.",
};
export class AbortError extends Error {
- constructor(message = messages.AbortError) {
- super();
- Object.setPrototypeOf(this, ConstraintError.prototype);
- this.name = "AbortError";
- this.message = message;
- }
+ constructor(message = messages.AbortError) {
+ super();
+ Object.setPrototypeOf(this, ConstraintError.prototype);
+ this.name = "AbortError";
+ this.message = message;
+ }
}
export class ConstraintError extends Error {
- constructor(message = messages.ConstraintError) {
- super();
- Object.setPrototypeOf(this, ConstraintError.prototype);
- this.name = "ConstraintError";
- this.message = message;
- }
+ constructor(message = messages.ConstraintError) {
+ super();
+ Object.setPrototypeOf(this, ConstraintError.prototype);
+ this.name = "ConstraintError";
+ this.message = message;
+ }
}
export class DataCloneError extends Error {
- constructor(message = messages.DataCloneError) {
- super();
- Object.setPrototypeOf(this, DataCloneError.prototype);
- this.name = "DataCloneError";
- this.message = message;
- }
+ constructor(message = messages.DataCloneError) {
+ super();
+ Object.setPrototypeOf(this, DataCloneError.prototype);
+ this.name = "DataCloneError";
+ this.message = message;
+ }
}
export class DataError extends Error {
- constructor(message = messages.DataError) {
- super();
- Object.setPrototypeOf(this, DataError.prototype);
- this.name = "DataError";
- this.message = message;
- }
+ constructor(message = messages.DataError) {
+ super();
+ Object.setPrototypeOf(this, DataError.prototype);
+ this.name = "DataError";
+ this.message = message;
+ }
}
export class InvalidAccessError extends Error {
- constructor(message = messages.InvalidAccessError) {
- super();
- Object.setPrototypeOf(this, InvalidAccessError.prototype);
- this.name = "InvalidAccessError";
- this.message = message;
- }
+ constructor(message = messages.InvalidAccessError) {
+ super();
+ Object.setPrototypeOf(this, InvalidAccessError.prototype);
+ this.name = "InvalidAccessError";
+ this.message = message;
+ }
}
export class InvalidStateError extends Error {
- constructor(message = messages.InvalidStateError) {
- super();
- Object.setPrototypeOf(this, InvalidStateError.prototype);
- this.name = "InvalidStateError";
- this.message = message;
- }
+ constructor(message = messages.InvalidStateError) {
+ super();
+ Object.setPrototypeOf(this, InvalidStateError.prototype);
+ this.name = "InvalidStateError";
+ this.message = message;
+ }
}
export class NotFoundError extends Error {
- constructor(message = messages.NotFoundError) {
- super();
- Object.setPrototypeOf(this, NotFoundError.prototype);
- this.name = "NotFoundError";
- this.message = message;
- }
+ constructor(message = messages.NotFoundError) {
+ super();
+ Object.setPrototypeOf(this, NotFoundError.prototype);
+ this.name = "NotFoundError";
+ this.message = message;
+ }
}
export class ReadOnlyError extends Error {
- constructor(message = messages.ReadOnlyError) {
- super();
- Object.setPrototypeOf(this, ReadOnlyError.prototype);
- this.name = "ReadOnlyError";
- this.message = message;
- }
+ constructor(message = messages.ReadOnlyError) {
+ super();
+ Object.setPrototypeOf(this, ReadOnlyError.prototype);
+ this.name = "ReadOnlyError";
+ this.message = message;
+ }
}
export class TransactionInactiveError extends Error {
- constructor(message = messages.TransactionInactiveError) {
- super();
- Object.setPrototypeOf(this, TransactionInactiveError.prototype);
- this.name = "TransactionInactiveError";
- this.message = message;
- }
+ constructor(message = messages.TransactionInactiveError) {
+ super();
+ Object.setPrototypeOf(this, TransactionInactiveError.prototype);
+ this.name = "TransactionInactiveError";
+ this.message = message;
+ }
}
export class VersionError extends Error {
- constructor(message = messages.VersionError) {
- super();
- Object.setPrototypeOf(this, VersionError.prototype);
- this.name = "VersionError";
- this.message = message;
- }
+ constructor(message = messages.VersionError) {
+ super();
+ Object.setPrototypeOf(this, VersionError.prototype);
+ this.name = "VersionError";
+ this.message = message;
+ }
}
diff --git a/packages/idb-bridge/src/util/getIndexKeys.test.ts b/packages/idb-bridge/src/util/getIndexKeys.test.ts
index b9cdc769d..782b3da2f 100644
--- a/packages/idb-bridge/src/util/getIndexKeys.test.ts
+++ b/packages/idb-bridge/src/util/getIndexKeys.test.ts
@@ -19,23 +19,31 @@ import test from "ava";
import { getIndexKeys } from "./getIndexKeys";
test("basics", (t) => {
- t.deepEqual(getIndexKeys({foo: 42}, "foo", false), [42]);
- t.deepEqual(getIndexKeys({foo: {bar: 42}}, "foo.bar", false), [42]);
- t.deepEqual(getIndexKeys({foo: [42, 43]}, "foo.0", false), [42]);
- t.deepEqual(getIndexKeys({foo: [42, 43]}, "foo.1", false), [43]);
+ t.deepEqual(getIndexKeys({ foo: 42 }, "foo", false), [42]);
+ t.deepEqual(getIndexKeys({ foo: { bar: 42 } }, "foo.bar", false), [42]);
+ t.deepEqual(getIndexKeys({ foo: [42, 43] }, "foo.0", false), [42]);
+ t.deepEqual(getIndexKeys({ foo: [42, 43] }, "foo.1", false), [43]);
t.deepEqual(getIndexKeys([1, 2, 3], "", false), [[1, 2, 3]]);
t.throws(() => {
- getIndexKeys({foo: 42}, "foo.bar", false);
+ getIndexKeys({ foo: 42 }, "foo.bar", false);
});
- t.deepEqual(getIndexKeys({foo: 42}, "foo", true), [42]);
- t.deepEqual(getIndexKeys({foo: 42, bar: 10}, ["foo", "bar"], true), [42, 10]);
- t.deepEqual(getIndexKeys({foo: 42, bar: 10}, ["foo", "bar"], false), [[42, 10]]);
- t.deepEqual(getIndexKeys({foo: 42, bar: 10}, ["foo", "bar", "spam"], true), [42, 10]);
+ t.deepEqual(getIndexKeys({ foo: 42 }, "foo", true), [42]);
+ t.deepEqual(getIndexKeys({ foo: 42, bar: 10 }, ["foo", "bar"], true), [
+ 42,
+ 10,
+ ]);
+ t.deepEqual(getIndexKeys({ foo: 42, bar: 10 }, ["foo", "bar"], false), [
+ [42, 10],
+ ]);
+ t.deepEqual(
+ getIndexKeys({ foo: 42, bar: 10 }, ["foo", "bar", "spam"], true),
+ [42, 10],
+ );
t.throws(() => {
- getIndexKeys({foo: 42, bar: 10}, ["foo", "bar", "spam"], false);
+ getIndexKeys({ foo: 42, bar: 10 }, ["foo", "bar", "spam"], false);
});
});
diff --git a/packages/idb-bridge/src/util/injectKey.ts b/packages/idb-bridge/src/util/injectKey.ts
index 78d0c217e..38add33bd 100644
--- a/packages/idb-bridge/src/util/injectKey.ts
+++ b/packages/idb-bridge/src/util/injectKey.ts
@@ -62,4 +62,4 @@ export function injectKey(keyPath: KeyPath, value: Value, key: Key): Value {
return newValue;
}
-export default injectKey; \ No newline at end of file
+export default injectKey;
diff --git a/packages/idb-bridge/src/util/makeStoreKeyValue.test.ts b/packages/idb-bridge/src/util/makeStoreKeyValue.test.ts
index ecbae6508..df9748316 100644
--- a/packages/idb-bridge/src/util/makeStoreKeyValue.test.ts
+++ b/packages/idb-bridge/src/util/makeStoreKeyValue.test.ts
@@ -14,7 +14,7 @@
permissions and limitations under the License.
*/
-import test from 'ava';
+import test from "ava";
import { makeStoreKeyValue } from "./makeStoreKeyValue";
test("basics", (t) => {
@@ -26,19 +26,37 @@ test("basics", (t) => {
t.is(result.value.name, "Florian");
t.is(result.value.id, 42);
- result = makeStoreKeyValue({ name: "Florian", id: 10 }, undefined, 5, true, "id");
+ result = makeStoreKeyValue(
+ { name: "Florian", id: 10 },
+ undefined,
+ 5,
+ true,
+ "id",
+ );
t.is(result.updatedKeyGenerator, 11);
t.is(result.key, 10);
t.is(result.value.name, "Florian");
t.is(result.value.id, 10);
- result = makeStoreKeyValue({ name: "Florian", id: 5 }, undefined, 10, true, "id");
+ result = makeStoreKeyValue(
+ { name: "Florian", id: 5 },
+ undefined,
+ 10,
+ true,
+ "id",
+ );
t.is(result.updatedKeyGenerator, 10);
t.is(result.key, 5);
t.is(result.value.name, "Florian");
t.is(result.value.id, 5);
- result = makeStoreKeyValue({ name: "Florian", id: "foo" }, undefined, 10, true, "id");
+ result = makeStoreKeyValue(
+ { name: "Florian", id: "foo" },
+ undefined,
+ 10,
+ true,
+ "id",
+ );
t.is(result.updatedKeyGenerator, 10);
t.is(result.key, "foo");
t.is(result.value.name, "Florian");
diff --git a/packages/idb-bridge/src/util/makeStoreKeyValue.ts b/packages/idb-bridge/src/util/makeStoreKeyValue.ts
index 9b33158dd..f9006ef51 100644
--- a/packages/idb-bridge/src/util/makeStoreKeyValue.ts
+++ b/packages/idb-bridge/src/util/makeStoreKeyValue.ts
@@ -14,7 +14,6 @@
permissions and limitations under the License.
*/
-
import { Value, Key, KeyPath } from "./types";
import extractKey from "./extractKey";
import { DataError } from "./errors";
@@ -93,7 +92,7 @@ export function makeStoreKeyValue(
key: key,
value: value,
updatedKeyGenerator,
- }
+ };
} else {
// (no, yes, no)
key = extractKey(keyPath!, value);
@@ -111,7 +110,7 @@ export function makeStoreKeyValue(
key: currentKeyGenerator,
value: value,
updatedKeyGenerator: currentKeyGenerator + 1,
- }
+ };
} else {
// (no, no, no)
throw new DataError();
diff --git a/packages/idb-bridge/src/util/queueTask.ts b/packages/idb-bridge/src/util/queueTask.ts
index 7d59c2263..53563ffd2 100644
--- a/packages/idb-bridge/src/util/queueTask.ts
+++ b/packages/idb-bridge/src/util/queueTask.ts
@@ -14,8 +14,8 @@
permissions and limitations under the License.
*/
- export function queueTask(fn: () => void) {
- setImmediate(fn);
- }
+export function queueTask(fn: () => void) {
+ setImmediate(fn);
+}
- export default queueTask; \ No newline at end of file
+export default queueTask;
diff --git a/packages/idb-bridge/src/util/structuredClone.ts b/packages/idb-bridge/src/util/structuredClone.ts
index 165ed2f32..c49d0377f 100644
--- a/packages/idb-bridge/src/util/structuredClone.ts
+++ b/packages/idb-bridge/src/util/structuredClone.ts
@@ -14,7 +14,6 @@
permissions and limitations under the License.
*/
-
function structuredCloneImpl(val: any, visited: WeakMap<any, boolean>): any {
// FIXME: replace with real implementation!
return JSON.parse(JSON.stringify(val));
@@ -28,4 +27,4 @@ export function structuredClone(val: any): any {
return structuredCloneImpl(val, visited);
}
-export default structuredClone; \ No newline at end of file
+export default structuredClone;
diff --git a/packages/idb-bridge/src/util/types.ts b/packages/idb-bridge/src/util/types.ts
index 9bf80366d..b0142b0d8 100644
--- a/packages/idb-bridge/src/util/types.ts
+++ b/packages/idb-bridge/src/util/types.ts
@@ -15,11 +15,11 @@
permissions and limitations under the License.
*/
-import BridgeIDBRequest from "../BridgeIDBRequest";
-import BridgeIDBKeyRange from "../BridgeIDBKeyRange";
-import BridgeIDBIndex from "../BridgeIDBIndex";
-import BridgeIBObjectStore from "../BridgeIDBObjectStore";
+import { BridgeIDBRequest } from "../BridgeIDBRequest";
+import { BridgeIDBKeyRange } from "../BridgeIDBKeyRange";
+import { BridgeIDBIndex } from "../BridgeIDBIndex";
import { Event } from "../util/FakeEvent";
+import { BridgeIDBObjectStore } from "../BridgeIDBObjectStore";
interface EventInCallback extends Event {
target: any;
@@ -37,15 +37,18 @@ export type EventType =
| "upgradeneeded"
| "versionchange";
-export type CursorSource = BridgeIDBIndex | BridgeIBObjectStore;
-
+export type CursorSource = BridgeIDBIndex | BridgeIDBObjectStore;
export interface FakeDOMStringList extends Array<string> {
contains: (value: string) => boolean;
item: (i: number) => string | undefined;
}
-export type BridgeIDBCursorDirection = "next" | "nextunique" | "prev" | "prevunique";
+export type BridgeIDBCursorDirection =
+ | "next"
+ | "nextunique"
+ | "prev"
+ | "prevunique";
export type KeyPath = string | string[];
@@ -64,8 +67,8 @@ export type TransactionMode = "readonly" | "readwrite" | "versionchange";
export interface BridgeIDBDatabaseInfo {
name: string;
- version: number
-};
+ version: number;
+}
export interface RequestObj {
operation: () => Promise<any>;
diff --git a/packages/idb-bridge/src/util/validateKeyPath.ts b/packages/idb-bridge/src/util/validateKeyPath.ts
index 18552a5d4..072832190 100644
--- a/packages/idb-bridge/src/util/validateKeyPath.ts
+++ b/packages/idb-bridge/src/util/validateKeyPath.ts
@@ -18,60 +18,60 @@ import { KeyPath } from "./types";
// http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#dfn-valid-key-path
const validateKeyPath = (keyPath: KeyPath, parent?: "array" | "string") => {
- // This doesn't make sense to me based on the spec, but it is needed to pass the W3C KeyPath tests (see same
- // comment in extractKey)
- if (
- keyPath !== undefined &&
- keyPath !== null &&
- typeof keyPath !== "string" &&
- keyPath.toString &&
- (parent === "array" || !Array.isArray(keyPath))
- ) {
- keyPath = keyPath.toString();
- }
+ // This doesn't make sense to me based on the spec, but it is needed to pass the W3C KeyPath tests (see same
+ // comment in extractKey)
+ if (
+ keyPath !== undefined &&
+ keyPath !== null &&
+ typeof keyPath !== "string" &&
+ keyPath.toString &&
+ (parent === "array" || !Array.isArray(keyPath))
+ ) {
+ keyPath = keyPath.toString();
+ }
- if (typeof keyPath === "string") {
- if (keyPath === "" && parent !== "string") {
- return;
- }
- try {
- // https://mathiasbynens.be/demo/javascript-identifier-regex for ECMAScript 5.1 / Unicode v7.0.0, with
- // reserved words at beginning removed
- // tslint:disable-next-line max-line-length
- const validIdentifierRegex = /^(?:[\$A-Z_a-z\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0-\u08B2\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58\u0C59\u0C60\u0C61\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D60\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F4\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19C1-\u19C7\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FCC\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6EF\uA717-\uA71F\uA722-\uA788\uA78B-\uA78E\uA790-\uA7AD\uA7B0\uA7B1\uA7F7-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB5F\uAB64\uAB65\uABC0-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC])(?:[\$0-9A-Z_a-z\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0300-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u0483-\u0487\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u05D0-\u05EA\u05F0-\u05F2\u0610-\u061A\u0620-\u0669\u066E-\u06D3\u06D5-\u06DC\u06DF-\u06E8\u06EA-\u06FC\u06FF\u0710-\u074A\u074D-\u07B1\u07C0-\u07F5\u07FA\u0800-\u082D\u0840-\u085B\u08A0-\u08B2\u08E4-\u0963\u0966-\u096F\u0971-\u0983\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BC-\u09C4\u09C7\u09C8\u09CB-\u09CE\u09D7\u09DC\u09DD\u09DF-\u09E3\u09E6-\u09F1\u0A01-\u0A03\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A59-\u0A5C\u0A5E\u0A66-\u0A75\u0A81-\u0A83\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABC-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AD0\u0AE0-\u0AE3\u0AE6-\u0AEF\u0B01-\u0B03\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3C-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B5C\u0B5D\u0B5F-\u0B63\u0B66-\u0B6F\u0B71\u0B82\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD0\u0BD7\u0BE6-\u0BEF\u0C00-\u0C03\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C58\u0C59\u0C60-\u0C63\u0C66-\u0C6F\u0C81-\u0C83\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBC-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CDE\u0CE0-\u0CE3\u0CE6-\u0CEF\u0CF1\u0CF2\u0D01-\u0D03\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D-\u0D44\u0D46-\u0D48\u0D4A-\u0D4E\u0D57\u0D60-\u0D63\u0D66-\u0D6F\u0D7A-\u0D7F\u0D82\u0D83\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DE6-\u0DEF\u0DF2\u0DF3\u0E01-\u0E3A\u0E40-\u0E4E\u0E50-\u0E59\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB9\u0EBB-\u0EBD\u0EC0-\u0EC4\u0EC6\u0EC8-\u0ECD\u0ED0-\u0ED9\u0EDC-\u0EDF\u0F00\u0F18\u0F19\u0F20-\u0F29\u0F35\u0F37\u0F39\u0F3E-\u0F47\u0F49-\u0F6C\u0F71-\u0F84\u0F86-\u0F97\u0F99-\u0FBC\u0FC6\u1000-\u1049\u1050-\u109D\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u135D-\u135F\u1380-\u138F\u13A0-\u13F4\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u170C\u170E-\u1714\u1720-\u1734\u1740-\u1753\u1760-\u176C\u176E-\u1770\u1772\u1773\u1780-\u17D3\u17D7\u17DC\u17DD\u17E0-\u17E9\u180B-\u180D\u1810-\u1819\u1820-\u1877\u1880-\u18AA\u18B0-\u18F5\u1900-\u191E\u1920-\u192B\u1930-\u193B\u1946-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u19D0-\u19D9\u1A00-\u1A1B\u1A20-\u1A5E\u1A60-\u1A7C\u1A7F-\u1A89\u1A90-\u1A99\u1AA7\u1AB0-\u1ABD\u1B00-\u1B4B\u1B50-\u1B59\u1B6B-\u1B73\u1B80-\u1BF3\u1C00-\u1C37\u1C40-\u1C49\u1C4D-\u1C7D\u1CD0-\u1CD2\u1CD4-\u1CF6\u1CF8\u1CF9\u1D00-\u1DF5\u1DFC-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u200C\u200D\u203F\u2040\u2054\u2071\u207F\u2090-\u209C\u20D0-\u20DC\u20E1\u20E5-\u20F0\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D7F-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2DE0-\u2DFF\u2E2F\u3005-\u3007\u3021-\u302F\u3031-\u3035\u3038-\u303C\u3041-\u3096\u3099\u309A\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FCC\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA62B\uA640-\uA66F\uA674-\uA67D\uA67F-\uA69D\uA69F-\uA6F1\uA717-\uA71F\uA722-\uA788\uA78B-\uA78E\uA790-\uA7AD\uA7B0\uA7B1\uA7F7-\uA827\uA840-\uA873\uA880-\uA8C4\uA8D0-\uA8D9\uA8E0-\uA8F7\uA8FB\uA900-\uA92D\uA930-\uA953\uA960-\uA97C\uA980-\uA9C0\uA9CF-\uA9D9\uA9E0-\uA9FE\uAA00-\uAA36\uAA40-\uAA4D\uAA50-\uAA59\uAA60-\uAA76\uAA7A-\uAAC2\uAADB-\uAADD\uAAE0-\uAAEF\uAAF2-\uAAF6\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB5F\uAB64\uAB65\uABC0-\uABEA\uABEC\uABED\uABF0-\uABF9\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE00-\uFE0F\uFE20-\uFE2D\uFE33\uFE34\uFE4D-\uFE4F\uFE70-\uFE74\uFE76-\uFEFC\uFF10-\uFF19\uFF21-\uFF3A\uFF3F\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC])*$/;
- if (keyPath.length >= 1 && validIdentifierRegex.test(keyPath)) {
- return;
- }
- } catch (err) {
- throw new SyntaxError(err.message);
- }
- if (keyPath.indexOf(" ") >= 0) {
- throw new SyntaxError(
- "The keypath argument contains an invalid key path (no spaces allowed).",
- );
- }
+ if (typeof keyPath === "string") {
+ if (keyPath === "" && parent !== "string") {
+ return;
}
-
- if (Array.isArray(keyPath) && keyPath.length > 0) {
- if (parent) {
- // No nested arrays
- throw new SyntaxError(
- "The keypath argument contains an invalid key path (nested arrays).",
- );
- }
- for (const part of keyPath) {
- validateKeyPath(part, "array");
- }
- return;
- } else if (typeof keyPath === "string" && keyPath.indexOf(".") >= 0) {
- keyPath = keyPath.split(".");
- for (const part of keyPath) {
- validateKeyPath(part, "string");
- }
+ try {
+ // https://mathiasbynens.be/demo/javascript-identifier-regex for ECMAScript 5.1 / Unicode v7.0.0, with
+ // reserved words at beginning removed
+ // tslint:disable-next-line max-line-length
+ const validIdentifierRegex = /^(?:[\$A-Z_a-z\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0-\u08B2\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58\u0C59\u0C60\u0C61\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D60\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F4\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19C1-\u19C7\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FCC\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6EF\uA717-\uA71F\uA722-\uA788\uA78B-\uA78E\uA790-\uA7AD\uA7B0\uA7B1\uA7F7-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB5F\uAB64\uAB65\uABC0-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC])(?:[\$0-9A-Z_a-z\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0300-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u0483-\u0487\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u05D0-\u05EA\u05F0-\u05F2\u0610-\u061A\u0620-\u0669\u066E-\u06D3\u06D5-\u06DC\u06DF-\u06E8\u06EA-\u06FC\u06FF\u0710-\u074A\u074D-\u07B1\u07C0-\u07F5\u07FA\u0800-\u082D\u0840-\u085B\u08A0-\u08B2\u08E4-\u0963\u0966-\u096F\u0971-\u0983\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BC-\u09C4\u09C7\u09C8\u09CB-\u09CE\u09D7\u09DC\u09DD\u09DF-\u09E3\u09E6-\u09F1\u0A01-\u0A03\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A59-\u0A5C\u0A5E\u0A66-\u0A75\u0A81-\u0A83\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABC-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AD0\u0AE0-\u0AE3\u0AE6-\u0AEF\u0B01-\u0B03\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3C-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B5C\u0B5D\u0B5F-\u0B63\u0B66-\u0B6F\u0B71\u0B82\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD0\u0BD7\u0BE6-\u0BEF\u0C00-\u0C03\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C58\u0C59\u0C60-\u0C63\u0C66-\u0C6F\u0C81-\u0C83\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBC-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CDE\u0CE0-\u0CE3\u0CE6-\u0CEF\u0CF1\u0CF2\u0D01-\u0D03\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D-\u0D44\u0D46-\u0D48\u0D4A-\u0D4E\u0D57\u0D60-\u0D63\u0D66-\u0D6F\u0D7A-\u0D7F\u0D82\u0D83\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DE6-\u0DEF\u0DF2\u0DF3\u0E01-\u0E3A\u0E40-\u0E4E\u0E50-\u0E59\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB9\u0EBB-\u0EBD\u0EC0-\u0EC4\u0EC6\u0EC8-\u0ECD\u0ED0-\u0ED9\u0EDC-\u0EDF\u0F00\u0F18\u0F19\u0F20-\u0F29\u0F35\u0F37\u0F39\u0F3E-\u0F47\u0F49-\u0F6C\u0F71-\u0F84\u0F86-\u0F97\u0F99-\u0FBC\u0FC6\u1000-\u1049\u1050-\u109D\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u135D-\u135F\u1380-\u138F\u13A0-\u13F4\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u170C\u170E-\u1714\u1720-\u1734\u1740-\u1753\u1760-\u176C\u176E-\u1770\u1772\u1773\u1780-\u17D3\u17D7\u17DC\u17DD\u17E0-\u17E9\u180B-\u180D\u1810-\u1819\u1820-\u1877\u1880-\u18AA\u18B0-\u18F5\u1900-\u191E\u1920-\u192B\u1930-\u193B\u1946-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u19D0-\u19D9\u1A00-\u1A1B\u1A20-\u1A5E\u1A60-\u1A7C\u1A7F-\u1A89\u1A90-\u1A99\u1AA7\u1AB0-\u1ABD\u1B00-\u1B4B\u1B50-\u1B59\u1B6B-\u1B73\u1B80-\u1BF3\u1C00-\u1C37\u1C40-\u1C49\u1C4D-\u1C7D\u1CD0-\u1CD2\u1CD4-\u1CF6\u1CF8\u1CF9\u1D00-\u1DF5\u1DFC-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u200C\u200D\u203F\u2040\u2054\u2071\u207F\u2090-\u209C\u20D0-\u20DC\u20E1\u20E5-\u20F0\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D7F-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2DE0-\u2DFF\u2E2F\u3005-\u3007\u3021-\u302F\u3031-\u3035\u3038-\u303C\u3041-\u3096\u3099\u309A\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FCC\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA62B\uA640-\uA66F\uA674-\uA67D\uA67F-\uA69D\uA69F-\uA6F1\uA717-\uA71F\uA722-\uA788\uA78B-\uA78E\uA790-\uA7AD\uA7B0\uA7B1\uA7F7-\uA827\uA840-\uA873\uA880-\uA8C4\uA8D0-\uA8D9\uA8E0-\uA8F7\uA8FB\uA900-\uA92D\uA930-\uA953\uA960-\uA97C\uA980-\uA9C0\uA9CF-\uA9D9\uA9E0-\uA9FE\uAA00-\uAA36\uAA40-\uAA4D\uAA50-\uAA59\uAA60-\uAA76\uAA7A-\uAAC2\uAADB-\uAADD\uAAE0-\uAAEF\uAAF2-\uAAF6\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB5F\uAB64\uAB65\uABC0-\uABEA\uABEC\uABED\uABF0-\uABF9\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE00-\uFE0F\uFE20-\uFE2D\uFE33\uFE34\uFE4D-\uFE4F\uFE70-\uFE74\uFE76-\uFEFC\uFF10-\uFF19\uFF21-\uFF3A\uFF3F\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC])*$/;
+ if (keyPath.length >= 1 && validIdentifierRegex.test(keyPath)) {
return;
+ }
+ } catch (err) {
+ throw new SyntaxError(err.message);
+ }
+ if (keyPath.indexOf(" ") >= 0) {
+ throw new SyntaxError(
+ "The keypath argument contains an invalid key path (no spaces allowed).",
+ );
+ }
+ }
+
+ if (Array.isArray(keyPath) && keyPath.length > 0) {
+ if (parent) {
+ // No nested arrays
+ throw new SyntaxError(
+ "The keypath argument contains an invalid key path (nested arrays).",
+ );
+ }
+ for (const part of keyPath) {
+ validateKeyPath(part, "array");
+ }
+ return;
+ } else if (typeof keyPath === "string" && keyPath.indexOf(".") >= 0) {
+ keyPath = keyPath.split(".");
+ for (const part of keyPath) {
+ validateKeyPath(part, "string");
}
+ return;
+ }
- throw new SyntaxError();
+ throw new SyntaxError();
};
export default validateKeyPath;
diff --git a/packages/idb-bridge/src/util/valueToKey.ts b/packages/idb-bridge/src/util/valueToKey.ts
index 85c8c409f..5cf5b2b1b 100644
--- a/packages/idb-bridge/src/util/valueToKey.ts
+++ b/packages/idb-bridge/src/util/valueToKey.ts
@@ -62,7 +62,6 @@ function valueToKey(input: any, seen?: Set<object>): Key | Key[] {
}
return keys;
} else {
-
throw new DataError();
}
}
diff --git a/packages/idb-bridge/tsconfig.json b/packages/idb-bridge/tsconfig.json
index 017afdae1..6e41df65d 100644
--- a/packages/idb-bridge/tsconfig.json
+++ b/packages/idb-bridge/tsconfig.json
@@ -1,16 +1,21 @@
{
"compilerOptions": {
+ "composite": true,
"lib": ["es6"],
- "module": "commonjs",
- "target": "es5",
+ "module": "ESNext",
+ "moduleResolution": "node",
+ "target": "ES6",
"noImplicitAny": true,
- "outDir": "build",
+ "outDir": "lib",
"declaration": true,
"noEmitOnError": true,
"strict": true,
"incremental": true,
"sourceMap": true,
- "types": []
+ "rootDir": "./src",
+ "esModuleInterop": true,
+ "importHelpers": true,
+ "typeRoots": ["./node_modules/@types"]
},
"include": ["src/**/*"]
}
diff --git a/packages/idb-bridge/yarn.lock b/packages/idb-bridge/yarn.lock
deleted file mode 100644
index 9a5daad32..000000000
--- a/packages/idb-bridge/yarn.lock
+++ /dev/null
@@ -1,2689 +0,0 @@
-# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
-# yarn lockfile v1
-
-
-"@ava/babel-plugin-throws-helper@^4.0.0":
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/@ava/babel-plugin-throws-helper/-/babel-plugin-throws-helper-4.0.0.tgz#8f5b45b7a0a79c6f4032de2101e0c221847efb62"
- integrity sha512-3diBLIVBPPh3j4+hb5lo0I1D+S/O/VDJPI4Y502apBxmwEqjyXG4gTSPFUlm41sSZeZzMarT/Gzovw9kV7An0w==
-
-"@ava/babel-preset-stage-4@^4.0.0":
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/@ava/babel-preset-stage-4/-/babel-preset-stage-4-4.0.0.tgz#9be5a59ead170062e228bb6ffd2b29f0489424fd"
- integrity sha512-lZEV1ZANzfzSYBU6WHSErsy7jLPbD1iIgAboASPMcKo7woVni5/5IKWeT0RxC8rY802MFktur3OKEw2JY1Tv2w==
- dependencies:
- "@babel/plugin-proposal-async-generator-functions" "^7.2.0"
- "@babel/plugin-proposal-dynamic-import" "^7.5.0"
- "@babel/plugin-proposal-optional-catch-binding" "^7.2.0"
- "@babel/plugin-transform-dotall-regex" "^7.4.4"
- "@babel/plugin-transform-modules-commonjs" "^7.5.0"
-
-"@ava/babel-preset-transform-test-files@^6.0.0":
- version "6.0.0"
- resolved "https://registry.yarnpkg.com/@ava/babel-preset-transform-test-files/-/babel-preset-transform-test-files-6.0.0.tgz#639e8929d2cdc8863c1f16020ce644c525723cd4"
- integrity sha512-8eKhFzZp7Qcq1VLfoC75ggGT8nQs9q8fIxltU47yCB7Wi7Y8Qf6oqY1Bm0z04fIec24vEgr0ENhDHEOUGVDqnA==
- dependencies:
- "@ava/babel-plugin-throws-helper" "^4.0.0"
- babel-plugin-espower "^3.0.1"
-
-"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.5.5":
- version "7.5.5"
- resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.5.5.tgz#bc0782f6d69f7b7d49531219699b988f669a8f9d"
- integrity sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==
- dependencies:
- "@babel/highlight" "^7.0.0"
-
-"@babel/core@^7.5.5":
- version "7.5.5"
- resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.5.5.tgz#17b2686ef0d6bc58f963dddd68ab669755582c30"
- integrity sha512-i4qoSr2KTtce0DmkuuQBV4AuQgGPUcPXMr9L5MyYAtk06z068lQ10a4O009fe5OB/DfNV+h+qqT7ddNV8UnRjg==
- dependencies:
- "@babel/code-frame" "^7.5.5"
- "@babel/generator" "^7.5.5"
- "@babel/helpers" "^7.5.5"
- "@babel/parser" "^7.5.5"
- "@babel/template" "^7.4.4"
- "@babel/traverse" "^7.5.5"
- "@babel/types" "^7.5.5"
- convert-source-map "^1.1.0"
- debug "^4.1.0"
- json5 "^2.1.0"
- lodash "^4.17.13"
- resolve "^1.3.2"
- semver "^5.4.1"
- source-map "^0.5.0"
-
-"@babel/generator@^7.0.0", "@babel/generator@^7.5.5":
- version "7.5.5"
- resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.5.5.tgz#873a7f936a3c89491b43536d12245b626664e3cf"
- integrity sha512-ETI/4vyTSxTzGnU2c49XHv2zhExkv9JHLTwDAFz85kmcwuShvYG2H08FwgIguQf4JC75CBnXAUM5PqeF4fj0nQ==
- dependencies:
- "@babel/types" "^7.5.5"
- jsesc "^2.5.1"
- lodash "^4.17.13"
- source-map "^0.5.0"
- trim-right "^1.0.1"
-
-"@babel/helper-annotate-as-pure@^7.0.0":
- version "7.0.0"
- resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.0.0.tgz#323d39dd0b50e10c7c06ca7d7638e6864d8c5c32"
- integrity sha512-3UYcJUj9kvSLbLbUIfQTqzcy5VX7GRZ/CCDrnOaZorFFM01aXp1+GJwuFGV4NDDoAS+mOUyHcO6UD/RfqOks3Q==
- dependencies:
- "@babel/types" "^7.0.0"
-
-"@babel/helper-function-name@^7.1.0":
- version "7.1.0"
- resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz#a0ceb01685f73355d4360c1247f582bfafc8ff53"
- integrity sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw==
- dependencies:
- "@babel/helper-get-function-arity" "^7.0.0"
- "@babel/template" "^7.1.0"
- "@babel/types" "^7.0.0"
-
-"@babel/helper-get-function-arity@^7.0.0":
- version "7.0.0"
- resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz#83572d4320e2a4657263734113c42868b64e49c3"
- integrity sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ==
- dependencies:
- "@babel/types" "^7.0.0"
-
-"@babel/helper-module-imports@^7.0.0":
- version "7.0.0"
- resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.0.0.tgz#96081b7111e486da4d2cd971ad1a4fe216cc2e3d"
- integrity sha512-aP/hlLq01DWNEiDg4Jn23i+CXxW/owM4WpDLFUbpjxe4NS3BhLVZQ5i7E0ZrxuQ/vwekIeciyamgB1UIYxxM6A==
- dependencies:
- "@babel/types" "^7.0.0"
-
-"@babel/helper-module-transforms@^7.4.4":
- version "7.5.5"
- resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.5.5.tgz#f84ff8a09038dcbca1fd4355661a500937165b4a"
- integrity sha512-jBeCvETKuJqeiaCdyaheF40aXnnU1+wkSiUs/IQg3tB85up1LyL8x77ClY8qJpuRJUcXQo+ZtdNESmZl4j56Pw==
- dependencies:
- "@babel/helper-module-imports" "^7.0.0"
- "@babel/helper-simple-access" "^7.1.0"
- "@babel/helper-split-export-declaration" "^7.4.4"
- "@babel/template" "^7.4.4"
- "@babel/types" "^7.5.5"
- lodash "^4.17.13"
-
-"@babel/helper-plugin-utils@^7.0.0":
- version "7.0.0"
- resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0.tgz#bbb3fbee98661c569034237cc03967ba99b4f250"
- integrity sha512-CYAOUCARwExnEixLdB6sDm2dIJ/YgEAKDM1MOeMeZu9Ld/bDgVo8aiWrXwcY7OBh+1Ea2uUcVRcxKk0GJvW7QA==
-
-"@babel/helper-regex@^7.4.4":
- version "7.5.5"
- resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.5.5.tgz#0aa6824f7100a2e0e89c1527c23936c152cab351"
- integrity sha512-CkCYQLkfkiugbRDO8eZn6lRuR8kzZoGXCg3149iTk5se7g6qykSpy3+hELSwquhu+TgHn8nkLiBwHvNX8Hofcw==
- dependencies:
- lodash "^4.17.13"
-
-"@babel/helper-remap-async-to-generator@^7.1.0":
- version "7.1.0"
- resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.1.0.tgz#361d80821b6f38da75bd3f0785ece20a88c5fe7f"
- integrity sha512-3fOK0L+Fdlg8S5al8u/hWE6vhufGSn0bN09xm2LXMy//REAF8kDCrYoOBKYmA8m5Nom+sV9LyLCwrFynA8/slg==
- dependencies:
- "@babel/helper-annotate-as-pure" "^7.0.0"
- "@babel/helper-wrap-function" "^7.1.0"
- "@babel/template" "^7.1.0"
- "@babel/traverse" "^7.1.0"
- "@babel/types" "^7.0.0"
-
-"@babel/helper-simple-access@^7.1.0":
- version "7.1.0"
- resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.1.0.tgz#65eeb954c8c245beaa4e859da6188f39d71e585c"
- integrity sha512-Vk+78hNjRbsiu49zAPALxTb+JUQCz1aolpd8osOF16BGnLtseD21nbHgLPGUwrXEurZgiCOUmvs3ExTu4F5x6w==
- dependencies:
- "@babel/template" "^7.1.0"
- "@babel/types" "^7.0.0"
-
-"@babel/helper-split-export-declaration@^7.4.4":
- version "7.4.4"
- resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz#ff94894a340be78f53f06af038b205c49d993677"
- integrity sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q==
- dependencies:
- "@babel/types" "^7.4.4"
-
-"@babel/helper-wrap-function@^7.1.0":
- version "7.2.0"
- resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.2.0.tgz#c4e0012445769e2815b55296ead43a958549f6fa"
- integrity sha512-o9fP1BZLLSrYlxYEYyl2aS+Flun5gtjTIG8iln+XuEzQTs0PLagAGSXUcqruJwD5fM48jzIEggCKpIfWTcR7pQ==
- dependencies:
- "@babel/helper-function-name" "^7.1.0"
- "@babel/template" "^7.1.0"
- "@babel/traverse" "^7.1.0"
- "@babel/types" "^7.2.0"
-
-"@babel/helpers@^7.5.5":
- version "7.5.5"
- resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.5.5.tgz#63908d2a73942229d1e6685bc2a0e730dde3b75e"
- integrity sha512-nRq2BUhxZFnfEn/ciJuhklHvFOqjJUD5wpx+1bxUF2axL9C+v4DE/dmp5sT2dKnpOs4orZWzpAZqlCy8QqE/7g==
- dependencies:
- "@babel/template" "^7.4.4"
- "@babel/traverse" "^7.5.5"
- "@babel/types" "^7.5.5"
-
-"@babel/highlight@^7.0.0":
- version "7.5.0"
- resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.5.0.tgz#56d11312bd9248fa619591d02472be6e8cb32540"
- integrity sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ==
- dependencies:
- chalk "^2.0.0"
- esutils "^2.0.2"
- js-tokens "^4.0.0"
-
-"@babel/parser@^7.0.0", "@babel/parser@^7.4.4", "@babel/parser@^7.5.5":
- version "7.5.5"
- resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.5.5.tgz#02f077ac8817d3df4a832ef59de67565e71cca4b"
- integrity sha512-E5BN68cqR7dhKan1SfqgPGhQ178bkVKpXTPEXnFJBrEt8/DKRZlybmy+IgYLTeN7tp1R5Ccmbm2rBk17sHYU3g==
-
-"@babel/plugin-proposal-async-generator-functions@^7.2.0":
- version "7.2.0"
- resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.2.0.tgz#b289b306669dce4ad20b0252889a15768c9d417e"
- integrity sha512-+Dfo/SCQqrwx48ptLVGLdE39YtWRuKc/Y9I5Fy0P1DDBB9lsAHpjcEJQt+4IifuSOSTLBKJObJqMvaO1pIE8LQ==
- dependencies:
- "@babel/helper-plugin-utils" "^7.0.0"
- "@babel/helper-remap-async-to-generator" "^7.1.0"
- "@babel/plugin-syntax-async-generators" "^7.2.0"
-
-"@babel/plugin-proposal-dynamic-import@^7.5.0":
- version "7.5.0"
- resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.5.0.tgz#e532202db4838723691b10a67b8ce509e397c506"
- integrity sha512-x/iMjggsKTFHYC6g11PL7Qy58IK8H5zqfm9e6hu4z1iH2IRyAp9u9dL80zA6R76yFovETFLKz2VJIC2iIPBuFw==
- dependencies:
- "@babel/helper-plugin-utils" "^7.0.0"
- "@babel/plugin-syntax-dynamic-import" "^7.2.0"
-
-"@babel/plugin-proposal-optional-catch-binding@^7.2.0":
- version "7.2.0"
- resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.2.0.tgz#135d81edb68a081e55e56ec48541ece8065c38f5"
- integrity sha512-mgYj3jCcxug6KUcX4OBoOJz3CMrwRfQELPQ5560F70YQUBZB7uac9fqaWamKR1iWUzGiK2t0ygzjTScZnVz75g==
- dependencies:
- "@babel/helper-plugin-utils" "^7.0.0"
- "@babel/plugin-syntax-optional-catch-binding" "^7.2.0"
-
-"@babel/plugin-syntax-async-generators@^7.2.0":
- version "7.2.0"
- resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.2.0.tgz#69e1f0db34c6f5a0cf7e2b3323bf159a76c8cb7f"
- integrity sha512-1ZrIRBv2t0GSlcwVoQ6VgSLpLgiN/FVQUzt9znxo7v2Ov4jJrs8RY8tv0wvDmFN3qIdMKWrmMMW6yZ0G19MfGg==
- dependencies:
- "@babel/helper-plugin-utils" "^7.0.0"
-
-"@babel/plugin-syntax-dynamic-import@^7.2.0":
- version "7.2.0"
- resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.2.0.tgz#69c159ffaf4998122161ad8ebc5e6d1f55df8612"
- integrity sha512-mVxuJ0YroI/h/tbFTPGZR8cv6ai+STMKNBq0f8hFxsxWjl94qqhsb+wXbpNMDPU3cfR1TIsVFzU3nXyZMqyK4w==
- dependencies:
- "@babel/helper-plugin-utils" "^7.0.0"
-
-"@babel/plugin-syntax-optional-catch-binding@^7.2.0":
- version "7.2.0"
- resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.2.0.tgz#a94013d6eda8908dfe6a477e7f9eda85656ecf5c"
- integrity sha512-bDe4xKNhb0LI7IvZHiA13kff0KEfaGX/Hv4lMA9+7TEc63hMNvfKo6ZFpXhKuEp+II/q35Gc4NoMeDZyaUbj9w==
- dependencies:
- "@babel/helper-plugin-utils" "^7.0.0"
-
-"@babel/plugin-transform-dotall-regex@^7.4.4":
- version "7.4.4"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.4.4.tgz#361a148bc951444312c69446d76ed1ea8e4450c3"
- integrity sha512-P05YEhRc2h53lZDjRPk/OektxCVevFzZs2Gfjd545Wde3k+yFDbXORgl2e0xpbq8mLcKJ7Idss4fAg0zORN/zg==
- dependencies:
- "@babel/helper-plugin-utils" "^7.0.0"
- "@babel/helper-regex" "^7.4.4"
- regexpu-core "^4.5.4"
-
-"@babel/plugin-transform-modules-commonjs@^7.5.0":
- version "7.5.0"
- resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.5.0.tgz#425127e6045231360858eeaa47a71d75eded7a74"
- integrity sha512-xmHq0B+ytyrWJvQTc5OWAC4ii6Dhr0s22STOoydokG51JjWhyYo5mRPXoi+ZmtHQhZZwuXNN+GG5jy5UZZJxIQ==
- dependencies:
- "@babel/helper-module-transforms" "^7.4.4"
- "@babel/helper-plugin-utils" "^7.0.0"
- "@babel/helper-simple-access" "^7.1.0"
- babel-plugin-dynamic-import-node "^2.3.0"
-
-"@babel/template@^7.1.0", "@babel/template@^7.4.4":
- version "7.4.4"
- resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.4.4.tgz#f4b88d1225689a08f5bc3a17483545be9e4ed237"
- integrity sha512-CiGzLN9KgAvgZsnivND7rkA+AeJ9JB0ciPOD4U59GKbQP2iQl+olF1l76kJOupqidozfZ32ghwBEJDhnk9MEcw==
- dependencies:
- "@babel/code-frame" "^7.0.0"
- "@babel/parser" "^7.4.4"
- "@babel/types" "^7.4.4"
-
-"@babel/traverse@^7.1.0", "@babel/traverse@^7.5.5":
- version "7.5.5"
- resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.5.5.tgz#f664f8f368ed32988cd648da9f72d5ca70f165bb"
- integrity sha512-MqB0782whsfffYfSjH4TM+LMjrJnhCNEDMDIjeTpl+ASaUvxcjoiVCo/sM1GhS1pHOXYfWVCYneLjMckuUxDaQ==
- dependencies:
- "@babel/code-frame" "^7.5.5"
- "@babel/generator" "^7.5.5"
- "@babel/helper-function-name" "^7.1.0"
- "@babel/helper-split-export-declaration" "^7.4.4"
- "@babel/parser" "^7.5.5"
- "@babel/types" "^7.5.5"
- debug "^4.1.0"
- globals "^11.1.0"
- lodash "^4.17.13"
-
-"@babel/types@^7.0.0", "@babel/types@^7.2.0", "@babel/types@^7.4.4", "@babel/types@^7.5.5":
- version "7.5.5"
- resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.5.5.tgz#97b9f728e182785909aa4ab56264f090a028d18a"
- integrity sha512-s63F9nJioLqOlW3UkyMd+BYhXt44YuaFm/VV0VwuteqjYwRrObkU7ra9pY4wAJR3oXi8hJrMcrcJdO/HH33vtw==
- dependencies:
- esutils "^2.0.2"
- lodash "^4.17.13"
- to-fast-properties "^2.0.0"
-
-"@concordance/react@^2.0.0":
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/@concordance/react/-/react-2.0.0.tgz#aef913f27474c53731f4fd79cc2f54897de90fde"
- integrity sha512-huLSkUuM2/P+U0uy2WwlKuixMsTODD8p4JVQBI4VKeopkiN0C7M3N9XYVawb4M+4spN5RrO/eLhk7KoQX6nsfA==
- dependencies:
- arrify "^1.0.1"
-
-"@nodelib/fs.scandir@2.1.1":
- version "2.1.1"
- resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.1.tgz#7fa8fed654939e1a39753d286b48b4836d00e0eb"
- integrity sha512-NT/skIZjgotDSiXs0WqYhgcuBKhUMgfekCmCGtkUAiLqZdOnrdjmZr9wRl3ll64J9NF79uZ4fk16Dx0yMc/Xbg==
- dependencies:
- "@nodelib/fs.stat" "2.0.1"
- run-parallel "^1.1.9"
-
-"@nodelib/fs.stat@2.0.1", "@nodelib/fs.stat@^2.0.1":
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.1.tgz#814f71b1167390cfcb6a6b3d9cdeb0951a192c14"
- integrity sha512-+RqhBlLn6YRBGOIoVYthsG0J9dfpO79eJyN7BYBkZJtfqrBwf2KK+rD/M/yjZR6WBmIhAgOV7S60eCgaSWtbFw==
-
-"@nodelib/fs.walk@^1.2.1":
- version "1.2.2"
- resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.2.tgz#6a6450c5e17012abd81450eb74949a4d970d2807"
- integrity sha512-J/DR3+W12uCzAJkw7niXDcqcKBg6+5G5Q/ZpThpGNzAUz70eOR6RV4XnnSN01qHZiVl0eavoxJsBypQoKsV2QQ==
- dependencies:
- "@nodelib/fs.scandir" "2.1.1"
- fastq "^1.6.0"
-
-"@sindresorhus/is@^0.14.0":
- version "0.14.0"
- resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea"
- integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==
-
-"@szmarczak/http-timer@^1.1.2":
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421"
- integrity sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==
- dependencies:
- defer-to-connect "^1.0.1"
-
-"@types/events@*":
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7"
- integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==
-
-"@types/glob@^7.1.1":
- version "7.1.1"
- resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575"
- integrity sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==
- dependencies:
- "@types/events" "*"
- "@types/minimatch" "*"
- "@types/node" "*"
-
-"@types/minimatch@*":
- version "3.0.3"
- resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
- integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==
-
-"@types/node@*":
- version "12.7.2"
- resolved "https://registry.yarnpkg.com/@types/node/-/node-12.7.2.tgz#c4e63af5e8823ce9cc3f0b34f7b998c2171f0c44"
- integrity sha512-dyYO+f6ihZEtNPDcWNR1fkoTDf3zAK3lAABDze3mz6POyIercH0lEUawUFXlG8xaQZmm1yEBON/4TsYv/laDYg==
-
-ansi-align@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.0.tgz#b536b371cf687caaef236c18d3e21fe3797467cb"
- integrity sha512-ZpClVKqXN3RGBmKibdfWzqCY4lnjEuoNzU5T0oEFpfd/z5qJHVarukridD4juLO2FXMiwUQxr9WqQtaYa8XRYw==
- dependencies:
- string-width "^3.0.0"
-
-ansi-escapes@^4.2.1:
- version "4.2.1"
- resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.2.1.tgz#4dccdb846c3eee10f6d64dea66273eab90c37228"
- integrity sha512-Cg3ymMAdN10wOk/VYfLV7KCQyv7EDirJ64500sU7n9UlmioEtDuU5Gd+hj73hXSU/ex7tHJSssmyftDdkMLO8Q==
- dependencies:
- type-fest "^0.5.2"
-
-ansi-regex@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998"
- integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=
-
-ansi-regex@^4.1.0:
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997"
- integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==
-
-ansi-styles@^3.2.0, ansi-styles@^3.2.1:
- version "3.2.1"
- resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
- integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
- dependencies:
- color-convert "^1.9.0"
-
-ansi-styles@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.0.0.tgz#f6b84e8fc97ea7add7a53b7530ef28f3fde0e048"
- integrity sha512-8zjUtFJ3db/QoPXuuEMloS2AUf79/yeyttJ7Abr3hteopJu9HK8vsgGviGUMq+zyA6cZZO6gAyZoMTF6TgaEjA==
- dependencies:
- color-convert "^2.0.0"
-
-anymatch@^3.0.1:
- version "3.0.3"
- resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.0.3.tgz#2fb624fe0e84bccab00afee3d0006ed310f22f09"
- integrity sha512-c6IvoeBECQlMVuYUjSwimnhmztImpErfxJzWZhIQinIvQWoGOnB0dLIgifbPHQt5heS6mNlaZG16f06H3C8t1g==
- dependencies:
- normalize-path "^3.0.0"
- picomatch "^2.0.4"
-
-argparse@^1.0.7:
- version "1.0.10"
- resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
- integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==
- dependencies:
- sprintf-js "~1.0.2"
-
-arr-flatten@^1.1.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1"
- integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==
-
-array-find-index@^1.0.1:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1"
- integrity sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=
-
-array-union@^1.0.1:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39"
- integrity sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=
- dependencies:
- array-uniq "^1.0.1"
-
-array-union@^2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
- integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==
-
-array-uniq@^1.0.1:
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6"
- integrity sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=
-
-array-uniq@^2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-2.1.0.tgz#46603d5e28e79bfd02b046fcc1d77c6820bd8e98"
- integrity sha512-bdHxtev7FN6+MXI1YFW0Q8mQ8dTJc2S8AMfju+ZR77pbg2yAdVyDlwkaUI7Har0LyOMRFPHrJ9lYdyjZZswdlQ==
-
-arrify@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
- integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=
-
-arrify@^2.0.1:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa"
- integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==
-
-astral-regex@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9"
- integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==
-
-ava@2.3.0:
- version "2.3.0"
- resolved "https://registry.yarnpkg.com/ava/-/ava-2.3.0.tgz#cac4d7f0a30077a852c6bf5bc7c0bc78c3021e63"
- integrity sha512-4VaaSnl13vpTZmqW3aMqioSolT0/ozRkjQxTLi3p8wtyRONuX/uLKL3uF0j50w2BNRoLsJqztnkX2h8xeVp2lg==
- dependencies:
- "@ava/babel-preset-stage-4" "^4.0.0"
- "@ava/babel-preset-transform-test-files" "^6.0.0"
- "@babel/core" "^7.5.5"
- "@babel/generator" "^7.5.5"
- "@concordance/react" "^2.0.0"
- ansi-escapes "^4.2.1"
- ansi-styles "^4.0.0"
- arr-flatten "^1.1.0"
- array-union "^2.1.0"
- array-uniq "^2.1.0"
- arrify "^2.0.1"
- bluebird "^3.5.5"
- chalk "^2.4.2"
- chokidar "^3.0.2"
- chunkd "^1.0.0"
- ci-parallel-vars "^1.0.0"
- clean-stack "^2.2.0"
- clean-yaml-object "^0.1.0"
- cli-cursor "^3.1.0"
- cli-truncate "^2.0.0"
- code-excerpt "^2.1.1"
- common-path-prefix "^1.0.0"
- concordance "^4.0.0"
- convert-source-map "^1.6.0"
- currently-unhandled "^0.4.1"
- debug "^4.1.1"
- del "^4.1.1"
- dot-prop "^5.1.0"
- emittery "^0.4.1"
- empower-core "^1.2.0"
- equal-length "^1.0.0"
- escape-string-regexp "^2.0.0"
- esm "^3.2.25"
- figures "^3.0.0"
- find-up "^4.1.0"
- get-port "^5.0.0"
- globby "^10.0.1"
- ignore-by-default "^1.0.0"
- import-local "^3.0.2"
- indent-string "^4.0.0"
- is-ci "^2.0.0"
- is-error "^2.2.2"
- is-observable "^2.0.0"
- is-plain-object "^3.0.0"
- is-promise "^2.1.0"
- lodash "^4.17.15"
- loud-rejection "^2.1.0"
- make-dir "^3.0.0"
- matcher "^2.0.0"
- md5-hex "^3.0.1"
- meow "^5.0.0"
- micromatch "^4.0.2"
- ms "^2.1.2"
- observable-to-promise "^1.0.0"
- ora "^3.4.0"
- package-hash "^4.0.0"
- pkg-conf "^3.1.0"
- plur "^3.1.1"
- pretty-ms "^5.0.0"
- require-precompiled "^0.1.0"
- resolve-cwd "^3.0.0"
- slash "^3.0.0"
- source-map-support "^0.5.13"
- stack-utils "^1.0.2"
- strip-ansi "^5.2.0"
- strip-bom-buf "^2.0.0"
- supertap "^1.0.0"
- supports-color "^7.0.0"
- trim-off-newlines "^1.0.1"
- trim-right "^1.0.1"
- unique-temp-dir "^1.0.0"
- update-notifier "^3.0.1"
- write-file-atomic "^3.0.0"
-
-babel-plugin-dynamic-import-node@^2.3.0:
- version "2.3.0"
- resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.0.tgz#f00f507bdaa3c3e3ff6e7e5e98d90a7acab96f7f"
- integrity sha512-o6qFkpeQEBxcqt0XYlWzAVxNCSCZdUgcR8IRlhD/8DylxjjO4foPcvTW0GGKa/cVt3rvxZ7o5ippJ+/0nvLhlQ==
- dependencies:
- object.assign "^4.1.0"
-
-babel-plugin-espower@^3.0.1:
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/babel-plugin-espower/-/babel-plugin-espower-3.0.1.tgz#180db17126f88e754105b8b5216d21e520a6bd4e"
- integrity sha512-Ms49U7VIAtQ/TtcqRbD6UBmJBUCSxiC3+zPc+eGqxKUIFO1lTshyEDRUjhoAbd2rWfwYf3cZ62oXozrd8W6J0A==
- dependencies:
- "@babel/generator" "^7.0.0"
- "@babel/parser" "^7.0.0"
- call-matcher "^1.0.0"
- core-js "^2.0.0"
- espower-location-detector "^1.0.0"
- espurify "^1.6.0"
- estraverse "^4.1.1"
-
-balanced-match@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
- integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
-
-binary-extensions@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c"
- integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==
-
-bluebird@^3.5.5:
- version "3.5.5"
- resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.5.tgz#a8d0afd73251effbbd5fe384a77d73003c17a71f"
- integrity sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w==
-
-blueimp-md5@^2.10.0:
- version "2.11.1"
- resolved "https://registry.yarnpkg.com/blueimp-md5/-/blueimp-md5-2.11.1.tgz#b1f6b6218d13cdedbf5743f32b3023b2afefcbd3"
- integrity sha512-4UiOAmql2XO0Sws07OVzYdCKK0K2Va5g6AVgYXoGhEQiKrdSOefjUCm1frPk6E+xiIOHRqaFg+TUGo7cClKg5g==
-
-boxen@^3.0.0:
- version "3.2.0"
- resolved "https://registry.yarnpkg.com/boxen/-/boxen-3.2.0.tgz#fbdff0de93636ab4450886b6ff45b92d098f45eb"
- integrity sha512-cU4J/+NodM3IHdSL2yN8bqYqnmlBTidDR4RC7nJs61ZmtGz8VZzM3HLQX0zY5mrSmPtR3xWwsq2jOUQqFZN8+A==
- dependencies:
- ansi-align "^3.0.0"
- camelcase "^5.3.1"
- chalk "^2.4.2"
- cli-boxes "^2.2.0"
- string-width "^3.0.0"
- term-size "^1.2.0"
- type-fest "^0.3.0"
- widest-line "^2.0.0"
-
-brace-expansion@^1.1.7:
- version "1.1.11"
- resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
- integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
- dependencies:
- balanced-match "^1.0.0"
- concat-map "0.0.1"
-
-braces@^3.0.1, braces@^3.0.2:
- version "3.0.2"
- resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
- integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
- dependencies:
- fill-range "^7.0.1"
-
-buffer-from@^1.0.0:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
- integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
-
-cacheable-request@^6.0.0:
- version "6.1.0"
- resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-6.1.0.tgz#20ffb8bd162ba4be11e9567d823db651052ca912"
- integrity sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==
- dependencies:
- clone-response "^1.0.2"
- get-stream "^5.1.0"
- http-cache-semantics "^4.0.0"
- keyv "^3.0.0"
- lowercase-keys "^2.0.0"
- normalize-url "^4.1.0"
- responselike "^1.0.2"
-
-call-matcher@^1.0.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/call-matcher/-/call-matcher-1.1.0.tgz#23b2c1bc7a8394c8be28609d77ddbd5786680432"
- integrity sha512-IoQLeNwwf9KTNbtSA7aEBb1yfDbdnzwjCetjkC8io5oGeOmK2CBNdg0xr+tadRYKO0p7uQyZzvon0kXlZbvGrw==
- dependencies:
- core-js "^2.0.0"
- deep-equal "^1.0.0"
- espurify "^1.6.0"
- estraverse "^4.0.0"
-
-call-signature@0.0.2:
- version "0.0.2"
- resolved "https://registry.yarnpkg.com/call-signature/-/call-signature-0.0.2.tgz#a84abc825a55ef4cb2b028bd74e205a65b9a4996"
- integrity sha1-qEq8glpV70yysCi9dOIFpluaSZY=
-
-camelcase-keys@^4.0.0:
- version "4.2.0"
- resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-4.2.0.tgz#a2aa5fb1af688758259c32c141426d78923b9b77"
- integrity sha1-oqpfsa9oh1glnDLBQUJteJI7m3c=
- dependencies:
- camelcase "^4.1.0"
- map-obj "^2.0.0"
- quick-lru "^1.0.0"
-
-camelcase@^4.1.0:
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd"
- integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=
-
-camelcase@^5.3.1:
- version "5.3.1"
- resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
- integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
-
-chalk@^2.0.0, chalk@^2.0.1, chalk@^2.4.2:
- version "2.4.2"
- resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
- integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
- dependencies:
- ansi-styles "^3.2.1"
- escape-string-regexp "^1.0.5"
- supports-color "^5.3.0"
-
-chokidar@^3.0.2:
- version "3.0.2"
- resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.0.2.tgz#0d1cd6d04eb2df0327446188cd13736a3367d681"
- integrity sha512-c4PR2egjNjI1um6bamCQ6bUNPDiyofNQruHvKgHQ4gDUP/ITSVSzNsiI5OWtHOsX323i5ha/kk4YmOZ1Ktg7KA==
- dependencies:
- anymatch "^3.0.1"
- braces "^3.0.2"
- glob-parent "^5.0.0"
- is-binary-path "^2.1.0"
- is-glob "^4.0.1"
- normalize-path "^3.0.0"
- readdirp "^3.1.1"
- optionalDependencies:
- fsevents "^2.0.6"
-
-chunkd@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/chunkd/-/chunkd-1.0.0.tgz#4ead4a3704bcce510c4bb4d4a8be30c557836dd1"
- integrity sha512-xx3Pb5VF9QaqCotolyZ1ywFBgyuJmu6+9dLiqBxgelEse9Xsr3yUlpoX3O4Oh11M00GT2kYMsRByTKIMJW2Lkg==
-
-ci-info@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46"
- integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==
-
-ci-parallel-vars@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/ci-parallel-vars/-/ci-parallel-vars-1.0.0.tgz#af97729ed1c7381911ca37bcea263d62638701b3"
- integrity sha512-u6dx20FBXm+apMi+5x7UVm6EH7BL1gc4XrcnQewjcB7HWRcor/V5qWc3RG2HwpgDJ26gIi2DSEu3B7sXynAw/g==
-
-clean-stack@^2.2.0:
- version "2.2.0"
- resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b"
- integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==
-
-clean-yaml-object@^0.1.0:
- version "0.1.0"
- resolved "https://registry.yarnpkg.com/clean-yaml-object/-/clean-yaml-object-0.1.0.tgz#63fb110dc2ce1a84dc21f6d9334876d010ae8b68"
- integrity sha1-Y/sRDcLOGoTcIfbZM0h20BCui2g=
-
-cli-boxes@^2.2.0:
- version "2.2.0"
- resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.0.tgz#538ecae8f9c6ca508e3c3c95b453fe93cb4c168d"
- integrity sha512-gpaBrMAizVEANOpfZp/EEUixTXDyGt7DFzdK5hU+UbWt/J0lB0w20ncZj59Z9a93xHb9u12zF5BS6i9RKbtg4w==
-
-cli-cursor@^2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5"
- integrity sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=
- dependencies:
- restore-cursor "^2.0.0"
-
-cli-cursor@^3.1.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307"
- integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==
- dependencies:
- restore-cursor "^3.1.0"
-
-cli-spinners@^2.0.0:
- version "2.2.0"
- resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.2.0.tgz#e8b988d9206c692302d8ee834e7a85c0144d8f77"
- integrity sha512-tgU3fKwzYjiLEQgPMD9Jt+JjHVL9kW93FiIMX/l7rivvOD4/LL0Mf7gda3+4U2KJBloybwgj5KEoQgGRioMiKQ==
-
-cli-truncate@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-2.0.0.tgz#68ff6aaa53b203b52ad89b8b1a80f1f61ad1e1d5"
- integrity sha512-C4hp+8GCIFVsUUiXcw+ce+7wexVWImw8rQrgMBFsqerx9LvvcGlwm6sMjQYAEmV/Xb87xc1b5Ttx505MSpZVqg==
- dependencies:
- slice-ansi "^2.1.0"
- string-width "^4.1.0"
-
-clone-response@^1.0.2:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b"
- integrity sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=
- dependencies:
- mimic-response "^1.0.0"
-
-clone@^1.0.2:
- version "1.0.4"
- resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
- integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4=
-
-code-excerpt@^2.1.1:
- version "2.1.1"
- resolved "https://registry.yarnpkg.com/code-excerpt/-/code-excerpt-2.1.1.tgz#5fe3057bfbb71a5f300f659ef2cc0a47651ba77c"
- integrity sha512-tJLhH3EpFm/1x7heIW0hemXJTUU5EWl2V0EIX558jp05Mt1U6DVryCgkp3l37cxqs+DNbNgxG43SkwJXpQ14Jw==
- dependencies:
- convert-to-spaces "^1.0.1"
-
-color-convert@^1.9.0:
- version "1.9.3"
- resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
- integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
- dependencies:
- color-name "1.1.3"
-
-color-convert@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.0.tgz#9851ac61cc0d3898a8a3088650d5bf447bf69d97"
- integrity sha512-hzTicsCJIHdxih9+2aLR1tNGZX5qSJGRHDPVwSY26tVrEf55XNajLOBWz2UuWSIergszA09/bqnOiHyqx9fxQg==
- dependencies:
- color-name "~1.1.4"
-
-color-name@1.1.3:
- version "1.1.3"
- resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
- integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
-
-color-name@~1.1.4:
- version "1.1.4"
- resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
- integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
-
-common-path-prefix@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/common-path-prefix/-/common-path-prefix-1.0.0.tgz#cd52f6f0712e0baab97d6f9732874f22f47752c0"
- integrity sha1-zVL28HEuC6q5fW+XModPIvR3UsA=
-
-concat-map@0.0.1:
- version "0.0.1"
- resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
- integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
-
-concordance@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/concordance/-/concordance-4.0.0.tgz#5932fdee397d129bdbc3a1885fbe69839b1b7e15"
- integrity sha512-l0RFuB8RLfCS0Pt2Id39/oCPykE01pyxgAFypWTlaGRgvLkZrtczZ8atEHpTeEIW+zYWXTBuA9cCSeEOScxReQ==
- dependencies:
- date-time "^2.1.0"
- esutils "^2.0.2"
- fast-diff "^1.1.2"
- js-string-escape "^1.0.1"
- lodash.clonedeep "^4.5.0"
- lodash.flattendeep "^4.4.0"
- lodash.islength "^4.0.1"
- lodash.merge "^4.6.1"
- md5-hex "^2.0.0"
- semver "^5.5.1"
- well-known-symbols "^2.0.0"
-
-configstore@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/configstore/-/configstore-4.0.0.tgz#5933311e95d3687efb592c528b922d9262d227e7"
- integrity sha512-CmquAXFBocrzaSM8mtGPMM/HiWmyIpr4CcJl/rgY2uCObZ/S7cKU0silxslqJejl+t/T9HS8E0PUNQD81JGUEQ==
- dependencies:
- dot-prop "^4.1.0"
- graceful-fs "^4.1.2"
- make-dir "^1.0.0"
- unique-string "^1.0.0"
- write-file-atomic "^2.0.0"
- xdg-basedir "^3.0.0"
-
-convert-source-map@^1.1.0, convert-source-map@^1.6.0:
- version "1.6.0"
- resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.6.0.tgz#51b537a8c43e0f04dec1993bffcdd504e758ac20"
- integrity sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==
- dependencies:
- safe-buffer "~5.1.1"
-
-convert-to-spaces@^1.0.1:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/convert-to-spaces/-/convert-to-spaces-1.0.2.tgz#7e3e48bbe6d997b1417ddca2868204b4d3d85715"
- integrity sha1-fj5Iu+bZl7FBfdyihoIEtNPYVxU=
-
-core-js@^2.0.0:
- version "2.6.9"
- resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.9.tgz#6b4b214620c834152e179323727fc19741b084f2"
- integrity sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A==
-
-cross-spawn@^5.0.1:
- version "5.1.0"
- resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449"
- integrity sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=
- dependencies:
- lru-cache "^4.0.1"
- shebang-command "^1.2.0"
- which "^1.2.9"
-
-crypto-random-string@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e"
- integrity sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=
-
-currently-unhandled@^0.4.1:
- version "0.4.1"
- resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea"
- integrity sha1-mI3zP+qxke95mmE2nddsF635V+o=
- dependencies:
- array-find-index "^1.0.1"
-
-date-time@^2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/date-time/-/date-time-2.1.0.tgz#0286d1b4c769633b3ca13e1e62558d2dbdc2eba2"
- integrity sha512-/9+C44X7lot0IeiyfgJmETtRMhBidBYM2QFFIkGa0U1k+hSyY87Nw7PY3eDqpvCBm7I3WCSfPeZskW/YYq6m4g==
- dependencies:
- time-zone "^1.0.0"
-
-debug@^4.1.0, debug@^4.1.1:
- version "4.1.1"
- resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
- integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==
- dependencies:
- ms "^2.1.1"
-
-decamelize-keys@^1.0.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.0.tgz#d171a87933252807eb3cb61dc1c1445d078df2d9"
- integrity sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=
- dependencies:
- decamelize "^1.1.0"
- map-obj "^1.0.0"
-
-decamelize@^1.1.0:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
- integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
-
-decompress-response@^3.3.0:
- version "3.3.0"
- resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3"
- integrity sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=
- dependencies:
- mimic-response "^1.0.0"
-
-deep-equal@^1.0.0:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
- integrity sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=
-
-deep-extend@^0.6.0:
- version "0.6.0"
- resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
- integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==
-
-defaults@^1.0.3:
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.3.tgz#c656051e9817d9ff08ed881477f3fe4019f3ef7d"
- integrity sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=
- dependencies:
- clone "^1.0.2"
-
-defer-to-connect@^1.0.1:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.0.2.tgz#4bae758a314b034ae33902b5aac25a8dd6a8633e"
- integrity sha512-k09hcQcTDY+cwgiwa6PYKLm3jlagNzQ+RSvhjzESOGOx+MNOuXkxTfEvPrO1IOQ81tArCFYQgi631clB70RpQw==
-
-define-properties@^1.1.2:
- version "1.1.3"
- resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
- integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==
- dependencies:
- object-keys "^1.0.12"
-
-del@^4.1.1:
- version "4.1.1"
- resolved "https://registry.yarnpkg.com/del/-/del-4.1.1.tgz#9e8f117222ea44a31ff3a156c049b99052a9f0b4"
- integrity sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ==
- dependencies:
- "@types/glob" "^7.1.1"
- globby "^6.1.0"
- is-path-cwd "^2.0.0"
- is-path-in-cwd "^2.0.0"
- p-map "^2.0.0"
- pify "^4.0.1"
- rimraf "^2.6.3"
-
-dir-glob@^3.0.1:
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
- integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==
- dependencies:
- path-type "^4.0.0"
-
-dot-prop@^4.1.0:
- version "4.2.0"
- resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.0.tgz#1f19e0c2e1aa0e32797c49799f2837ac6af69c57"
- integrity sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==
- dependencies:
- is-obj "^1.0.0"
-
-dot-prop@^5.1.0:
- version "5.1.0"
- resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.1.0.tgz#bdd8c986a77b83e3fca524e53786df916cabbd8a"
- integrity sha512-n1oC6NBF+KM9oVXtjmen4Yo7HyAVWV2UUl50dCYJdw2924K6dX9bf9TTTWaKtYlRn0FEtxG27KS80ayVLixxJA==
- dependencies:
- is-obj "^2.0.0"
-
-duplexer3@^0.1.4:
- version "0.1.4"
- resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2"
- integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=
-
-emittery@^0.4.1:
- version "0.4.1"
- resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.4.1.tgz#abe9d3297389ba424ac87e53d1c701962ce7433d"
- integrity sha512-r4eRSeStEGf6M5SKdrQhhLK5bOwOBxQhIE3YSTnZE3GpKiLfnnhE+tPtrJE79+eDJgm39BM6LSoI8SCx4HbwlQ==
-
-emoji-regex@^7.0.1:
- version "7.0.3"
- resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156"
- integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==
-
-emoji-regex@^8.0.0:
- version "8.0.0"
- resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
- integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
-
-empower-core@^1.2.0:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/empower-core/-/empower-core-1.2.0.tgz#ce3fb2484d5187fa29c23fba8344b0b2fdf5601c"
- integrity sha512-g6+K6Geyc1o6FdXs9HwrXleCFan7d66G5xSCfSF7x1mJDCes6t0om9lFQG3zOrzh3Bkb/45N0cZ5Gqsf7YrzGQ==
- dependencies:
- call-signature "0.0.2"
- core-js "^2.0.0"
-
-end-of-stream@^1.1.0:
- version "1.4.1"
- resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43"
- integrity sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==
- dependencies:
- once "^1.4.0"
-
-equal-length@^1.0.0:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/equal-length/-/equal-length-1.0.1.tgz#21ca112d48ab24b4e1e7ffc0e5339d31fdfc274c"
- integrity sha1-IcoRLUirJLTh5//A5TOdMf38J0w=
-
-error-ex@^1.3.1:
- version "1.3.2"
- resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf"
- integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==
- dependencies:
- is-arrayish "^0.2.1"
-
-es6-error@^4.0.1:
- version "4.1.1"
- resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d"
- integrity sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==
-
-escape-string-regexp@^1.0.5:
- version "1.0.5"
- resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
- integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
-
-escape-string-regexp@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344"
- integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==
-
-esm@^3.2.25:
- version "3.2.25"
- resolved "https://registry.yarnpkg.com/esm/-/esm-3.2.25.tgz#342c18c29d56157688ba5ce31f8431fbb795cc10"
- integrity sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==
-
-espower-location-detector@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/espower-location-detector/-/espower-location-detector-1.0.0.tgz#a17b7ecc59d30e179e2bef73fb4137704cb331b5"
- integrity sha1-oXt+zFnTDheeK+9z+0E3cEyzMbU=
- dependencies:
- is-url "^1.2.1"
- path-is-absolute "^1.0.0"
- source-map "^0.5.0"
- xtend "^4.0.0"
-
-esprima@^4.0.0:
- version "4.0.1"
- resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
- integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
-
-espurify@^1.6.0:
- version "1.8.1"
- resolved "https://registry.yarnpkg.com/espurify/-/espurify-1.8.1.tgz#5746c6c1ab42d302de10bd1d5bf7f0e8c0515056"
- integrity sha512-ZDko6eY/o+D/gHCWyHTU85mKDgYcS4FJj7S+YD6WIInm7GQ6AnOjmcL4+buFV/JOztVLELi/7MmuGU5NHta0Mg==
- dependencies:
- core-js "^2.0.0"
-
-estraverse@^4.0.0, estraverse@^4.1.1:
- version "4.3.0"
- resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d"
- integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
-
-esutils@^2.0.2:
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
- integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
-
-execa@^0.7.0:
- version "0.7.0"
- resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777"
- integrity sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=
- dependencies:
- cross-spawn "^5.0.1"
- get-stream "^3.0.0"
- is-stream "^1.1.0"
- npm-run-path "^2.0.0"
- p-finally "^1.0.0"
- signal-exit "^3.0.0"
- strip-eof "^1.0.0"
-
-fast-diff@^1.1.2:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03"
- integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==
-
-fast-glob@^3.0.3:
- version "3.0.4"
- resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.0.4.tgz#d484a41005cb6faeb399b951fd1bd70ddaebb602"
- integrity sha512-wkIbV6qg37xTJwqSsdnIphL1e+LaGz4AIQqr00mIubMaEhv1/HEmJ0uuCGZRNRUkZZmOB5mJKO0ZUTVq+SxMQg==
- dependencies:
- "@nodelib/fs.stat" "^2.0.1"
- "@nodelib/fs.walk" "^1.2.1"
- glob-parent "^5.0.0"
- is-glob "^4.0.1"
- merge2 "^1.2.3"
- micromatch "^4.0.2"
-
-fastq@^1.6.0:
- version "1.6.0"
- resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.6.0.tgz#4ec8a38f4ac25f21492673adb7eae9cfef47d1c2"
- integrity sha512-jmxqQ3Z/nXoeyDmWAzF9kH1aGZSis6e/SbfPmJpUnyZ0ogr6iscHQaml4wsEepEWSdtmpy+eVXmCRIMpxaXqOA==
- dependencies:
- reusify "^1.0.0"
-
-figures@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/figures/-/figures-3.0.0.tgz#756275c964646163cc6f9197c7a0295dbfd04de9"
- integrity sha512-HKri+WoWoUgr83pehn/SIgLOMZ9nAWC6dcGj26RY2R4F50u4+RTUz0RCrUlOV3nKRAICW1UGzyb+kcX2qK1S/g==
- dependencies:
- escape-string-regexp "^1.0.5"
-
-fill-range@^7.0.1:
- version "7.0.1"
- resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
- integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
- dependencies:
- to-regex-range "^5.0.1"
-
-find-up@^2.0.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7"
- integrity sha1-RdG35QbHF93UgndaK3eSCjwMV6c=
- dependencies:
- locate-path "^2.0.0"
-
-find-up@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73"
- integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==
- dependencies:
- locate-path "^3.0.0"
-
-find-up@^4.0.0, find-up@^4.1.0:
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
- integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==
- dependencies:
- locate-path "^5.0.0"
- path-exists "^4.0.0"
-
-fs.realpath@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
- integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
-
-fsevents@^2.0.6:
- version "2.0.7"
- resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.0.7.tgz#382c9b443c6cbac4c57187cdda23aa3bf1ccfc2a"
- integrity sha512-a7YT0SV3RB+DjYcppwVDLtn13UQnmg0SWZS7ezZD0UjnLwXmy8Zm21GMVGLaFGimIqcvyMQaOJBrop8MyOp1kQ==
-
-function-bind@^1.1.1:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
- integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
-
-get-port@^5.0.0:
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/get-port/-/get-port-5.0.0.tgz#aa22b6b86fd926dd7884de3e23332c9f70c031a6"
- integrity sha512-imzMU0FjsZqNa6BqOjbbW6w5BivHIuQKopjpPqcnx0AVHJQKCxK1O+Ab3OrVXhrekqfVMjwA9ZYu062R+KcIsQ==
- dependencies:
- type-fest "^0.3.0"
-
-get-stream@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14"
- integrity sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=
-
-get-stream@^4.1.0:
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5"
- integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==
- dependencies:
- pump "^3.0.0"
-
-get-stream@^5.1.0:
- version "5.1.0"
- resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.1.0.tgz#01203cdc92597f9b909067c3e656cc1f4d3c4dc9"
- integrity sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==
- dependencies:
- pump "^3.0.0"
-
-glob-parent@^5.0.0:
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.0.0.tgz#1dc99f0f39b006d3e92c2c284068382f0c20e954"
- integrity sha512-Z2RwiujPRGluePM6j699ktJYxmPpJKCfpGA13jz2hmFZC7gKetzrWvg5KN3+OsIFmydGyZ1AVwERCq1w/ZZwRg==
- dependencies:
- is-glob "^4.0.1"
-
-glob@^7.0.3, glob@^7.1.3:
- version "7.1.4"
- resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255"
- integrity sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==
- dependencies:
- fs.realpath "^1.0.0"
- inflight "^1.0.4"
- inherits "2"
- minimatch "^3.0.4"
- once "^1.3.0"
- path-is-absolute "^1.0.0"
-
-global-dirs@^0.1.0:
- version "0.1.1"
- resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-0.1.1.tgz#b319c0dd4607f353f3be9cca4c72fc148c49f445"
- integrity sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=
- dependencies:
- ini "^1.3.4"
-
-globals@^11.1.0:
- version "11.12.0"
- resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
- integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==
-
-globby@^10.0.1:
- version "10.0.1"
- resolved "https://registry.yarnpkg.com/globby/-/globby-10.0.1.tgz#4782c34cb75dd683351335c5829cc3420e606b22"
- integrity sha512-sSs4inE1FB2YQiymcmTv6NWENryABjUNPeWhOvmn4SjtKybglsyPZxFB3U1/+L1bYi0rNZDqCLlHyLYDl1Pq5A==
- dependencies:
- "@types/glob" "^7.1.1"
- array-union "^2.1.0"
- dir-glob "^3.0.1"
- fast-glob "^3.0.3"
- glob "^7.1.3"
- ignore "^5.1.1"
- merge2 "^1.2.3"
- slash "^3.0.0"
-
-globby@^6.1.0:
- version "6.1.0"
- resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c"
- integrity sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=
- dependencies:
- array-union "^1.0.1"
- glob "^7.0.3"
- object-assign "^4.0.1"
- pify "^2.0.0"
- pinkie-promise "^2.0.0"
-
-got@^9.6.0:
- version "9.6.0"
- resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85"
- integrity sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==
- dependencies:
- "@sindresorhus/is" "^0.14.0"
- "@szmarczak/http-timer" "^1.1.2"
- cacheable-request "^6.0.0"
- decompress-response "^3.3.0"
- duplexer3 "^0.1.4"
- get-stream "^4.1.0"
- lowercase-keys "^1.0.1"
- mimic-response "^1.0.1"
- p-cancelable "^1.0.0"
- to-readable-stream "^1.0.0"
- url-parse-lax "^3.0.0"
-
-graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2:
- version "4.2.2"
- resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.2.tgz#6f0952605d0140c1cfdb138ed005775b92d67b02"
- integrity sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q==
-
-has-flag@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
- integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0=
-
-has-flag@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
- integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
-
-has-symbols@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44"
- integrity sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=
-
-has-yarn@^2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/has-yarn/-/has-yarn-2.1.0.tgz#137e11354a7b5bf11aa5cb649cf0c6f3ff2b2e77"
- integrity sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==
-
-hasha@^5.0.0:
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/hasha/-/hasha-5.0.0.tgz#fdc3785caea03df29535fc8adb512c3d3a709004"
- integrity sha512-PqWdhnQhq6tqD32hZv+l1e5mJHNSudjnaAzgAHfkGiU0ABN6lmbZF8abJIulQHbZ7oiHhP8yL6O910ICMc+5pw==
- dependencies:
- is-stream "^1.1.0"
- type-fest "^0.3.0"
-
-hosted-git-info@^2.1.4:
- version "2.8.4"
- resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.4.tgz#44119abaf4bc64692a16ace34700fed9c03e2546"
- integrity sha512-pzXIvANXEFrc5oFFXRMkbLPQ2rXRoDERwDLyrcUxGhaZhgP54BBSl9Oheh7Vv0T090cszWBxPjkQQ5Sq1PbBRQ==
-
-http-cache-semantics@^4.0.0:
- version "4.0.3"
- resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.0.3.tgz#495704773277eeef6e43f9ab2c2c7d259dda25c5"
- integrity sha512-TcIMG3qeVLgDr1TEd2XvHaTnMPwYQUQMIBLy+5pLSDKYFc7UIqj39w8EGzZkaxoLv/l2K8HaI0t5AVA+YYgUew==
-
-ignore-by-default@^1.0.0:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09"
- integrity sha1-SMptcvbGo68Aqa1K5odr44ieKwk=
-
-ignore@^5.1.1:
- version "5.1.4"
- resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.4.tgz#84b7b3dbe64552b6ef0eca99f6743dbec6d97adf"
- integrity sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A==
-
-import-lazy@^2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43"
- integrity sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=
-
-import-local@^3.0.2:
- version "3.0.2"
- resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.0.2.tgz#a8cfd0431d1de4a2199703d003e3e62364fa6db6"
- integrity sha512-vjL3+w0oulAVZ0hBHnxa/Nm5TAurf9YLQJDhqRZyqb+VKGOB6LU8t9H1Nr5CIo16vh9XfJTOoHwU0B71S557gA==
- dependencies:
- pkg-dir "^4.2.0"
- resolve-cwd "^3.0.0"
-
-imurmurhash@^0.1.4:
- version "0.1.4"
- resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
- integrity sha1-khi5srkoojixPcT7a21XbyMUU+o=
-
-indent-string@^3.0.0, indent-string@^3.2.0:
- version "3.2.0"
- resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-3.2.0.tgz#4a5fd6d27cc332f37e5419a504dbb837105c9289"
- integrity sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=
-
-indent-string@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251"
- integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==
-
-inflight@^1.0.4:
- version "1.0.6"
- resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
- integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=
- dependencies:
- once "^1.3.0"
- wrappy "1"
-
-inherits@2:
- version "2.0.4"
- resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
- integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
-
-ini@^1.3.4, ini@~1.3.0:
- version "1.3.5"
- resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
- integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
-
-irregular-plurals@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/irregular-plurals/-/irregular-plurals-2.0.0.tgz#39d40f05b00f656d0b7fa471230dd3b714af2872"
- integrity sha512-Y75zBYLkh0lJ9qxeHlMjQ7bSbyiSqNW/UOPWDmzC7cXskL1hekSITh1Oc6JV0XCWWZ9DE8VYSB71xocLk3gmGw==
-
-is-arrayish@^0.2.1:
- version "0.2.1"
- resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
- integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=
-
-is-binary-path@^2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
- integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
- dependencies:
- binary-extensions "^2.0.0"
-
-is-ci@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c"
- integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==
- dependencies:
- ci-info "^2.0.0"
-
-is-error@^2.2.2:
- version "2.2.2"
- resolved "https://registry.yarnpkg.com/is-error/-/is-error-2.2.2.tgz#c10ade187b3c93510c5470a5567833ee25649843"
- integrity sha512-IOQqts/aHWbiisY5DuPJQ0gcbvaLFCa7fBa9xoLfxBZvQ+ZI/Zh9xoI7Gk+G64N0FdK4AbibytHht2tWgpJWLg==
-
-is-extglob@^2.1.1:
- version "2.1.1"
- resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
- integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=
-
-is-fullwidth-code-point@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
- integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=
-
-is-fullwidth-code-point@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
- integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
-
-is-glob@^4.0.1:
- version "4.0.1"
- resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc"
- integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==
- dependencies:
- is-extglob "^2.1.1"
-
-is-installed-globally@^0.1.0:
- version "0.1.0"
- resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.1.0.tgz#0dfd98f5a9111716dd535dda6492f67bf3d25a80"
- integrity sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA=
- dependencies:
- global-dirs "^0.1.0"
- is-path-inside "^1.0.0"
-
-is-npm@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-3.0.0.tgz#ec9147bfb629c43f494cf67936a961edec7e8053"
- integrity sha512-wsigDr1Kkschp2opC4G3yA6r9EgVA6NjRpWzIi9axXqeIaAATPRJc4uLujXe3Nd9uO8KoDyA4MD6aZSeXTADhA==
-
-is-number@^7.0.0:
- version "7.0.0"
- resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
- integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
-
-is-obj@^1.0.0:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f"
- integrity sha1-PkcprB9f3gJc19g6iW2rn09n2w8=
-
-is-obj@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982"
- integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==
-
-is-observable@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/is-observable/-/is-observable-2.0.0.tgz#327af1e8cdea9cd717f95911b87c5d34301721a6"
- integrity sha512-fhBZv3eFKUbyHXZ1oHujdo2tZ+CNbdpdzzlENgCGZUC8keoGxUew2jYFLYcUB4qo7LDD03o4KK11m/QYD7kEjg==
-
-is-path-cwd@^2.0.0:
- version "2.2.0"
- resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-2.2.0.tgz#67d43b82664a7b5191fd9119127eb300048a9fdb"
- integrity sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==
-
-is-path-in-cwd@^2.0.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz#bfe2dca26c69f397265a4009963602935a053acb"
- integrity sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==
- dependencies:
- is-path-inside "^2.1.0"
-
-is-path-inside@^1.0.0:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.1.tgz#8ef5b7de50437a3fdca6b4e865ef7aa55cb48036"
- integrity sha1-jvW33lBDej/cprToZe96pVy0gDY=
- dependencies:
- path-is-inside "^1.0.1"
-
-is-path-inside@^2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-2.1.0.tgz#7c9810587d659a40d27bcdb4d5616eab059494b2"
- integrity sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==
- dependencies:
- path-is-inside "^1.0.2"
-
-is-plain-obj@^1.1.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
- integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4=
-
-is-plain-object@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-3.0.0.tgz#47bfc5da1b5d50d64110806c199359482e75a928"
- integrity sha512-tZIpofR+P05k8Aocp7UI/2UTa9lTJSebCXpFFoR9aibpokDj/uXBsJ8luUu0tTVYKkMU6URDUuOfJZ7koewXvg==
- dependencies:
- isobject "^4.0.0"
-
-is-promise@^2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa"
- integrity sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=
-
-is-stream@^1.1.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
- integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ=
-
-is-typedarray@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
- integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
-
-is-url@^1.2.1:
- version "1.2.4"
- resolved "https://registry.yarnpkg.com/is-url/-/is-url-1.2.4.tgz#04a4df46d28c4cff3d73d01ff06abeb318a1aa52"
- integrity sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==
-
-is-utf8@^0.2.1:
- version "0.2.1"
- resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72"
- integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=
-
-is-yarn-global@^0.3.0:
- version "0.3.0"
- resolved "https://registry.yarnpkg.com/is-yarn-global/-/is-yarn-global-0.3.0.tgz#d502d3382590ea3004893746754c89139973e232"
- integrity sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==
-
-isexe@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
- integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
-
-isobject@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/isobject/-/isobject-4.0.0.tgz#3f1c9155e73b192022a80819bacd0343711697b0"
- integrity sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA==
-
-js-string-escape@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/js-string-escape/-/js-string-escape-1.0.1.tgz#e2625badbc0d67c7533e9edc1068c587ae4137ef"
- integrity sha1-4mJbrbwNZ8dTPp7cEGjFh65BN+8=
-
-js-tokens@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
- integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
-
-js-yaml@^3.10.0:
- version "3.13.1"
- resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847"
- integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==
- dependencies:
- argparse "^1.0.7"
- esprima "^4.0.0"
-
-jsesc@^2.5.1:
- version "2.5.2"
- resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4"
- integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==
-
-jsesc@~0.5.0:
- version "0.5.0"
- resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d"
- integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=
-
-json-buffer@3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898"
- integrity sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=
-
-json-parse-better-errors@^1.0.1:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9"
- integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==
-
-json5@^2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.0.tgz#e7a0c62c48285c628d20a10b85c89bb807c32850"
- integrity sha512-8Mh9h6xViijj36g7Dxi+Y4S6hNGV96vcJZr/SrlHh1LR/pEn/8j/+qIBbs44YKl69Lrfctp4QD+AdWLTMqEZAQ==
- dependencies:
- minimist "^1.2.0"
-
-keyv@^3.0.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9"
- integrity sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==
- dependencies:
- json-buffer "3.0.0"
-
-latest-version@^5.0.0:
- version "5.1.0"
- resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-5.1.0.tgz#119dfe908fe38d15dfa43ecd13fa12ec8832face"
- integrity sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==
- dependencies:
- package-json "^6.3.0"
-
-load-json-file@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b"
- integrity sha1-L19Fq5HjMhYjT9U62rZo607AmTs=
- dependencies:
- graceful-fs "^4.1.2"
- parse-json "^4.0.0"
- pify "^3.0.0"
- strip-bom "^3.0.0"
-
-load-json-file@^5.2.0:
- version "5.3.0"
- resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-5.3.0.tgz#4d3c1e01fa1c03ea78a60ac7af932c9ce53403f3"
- integrity sha512-cJGP40Jc/VXUsp8/OrnyKyTZ1y6v/dphm3bioS+RrKXjK2BB6wHUd6JptZEFDGgGahMT+InnZO5i1Ei9mpC8Bw==
- dependencies:
- graceful-fs "^4.1.15"
- parse-json "^4.0.0"
- pify "^4.0.1"
- strip-bom "^3.0.0"
- type-fest "^0.3.0"
-
-locate-path@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e"
- integrity sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=
- dependencies:
- p-locate "^2.0.0"
- path-exists "^3.0.0"
-
-locate-path@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e"
- integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==
- dependencies:
- p-locate "^3.0.0"
- path-exists "^3.0.0"
-
-locate-path@^5.0.0:
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0"
- integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==
- dependencies:
- p-locate "^4.1.0"
-
-lodash.clonedeep@^4.5.0:
- version "4.5.0"
- resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
- integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=
-
-lodash.flattendeep@^4.4.0:
- version "4.4.0"
- resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2"
- integrity sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=
-
-lodash.islength@^4.0.1:
- version "4.0.1"
- resolved "https://registry.yarnpkg.com/lodash.islength/-/lodash.islength-4.0.1.tgz#4e9868d452575d750affd358c979543dc20ed577"
- integrity sha1-Tpho1FJXXXUK/9NYyXlUPcIO1Xc=
-
-lodash.merge@^4.6.1:
- version "4.6.2"
- resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
- integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
-
-lodash@^4.17.13, lodash@^4.17.15:
- version "4.17.15"
- resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
- integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
-
-log-symbols@^2.2.0:
- version "2.2.0"
- resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a"
- integrity sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==
- dependencies:
- chalk "^2.0.1"
-
-loud-rejection@^1.0.0:
- version "1.6.0"
- resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f"
- integrity sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=
- dependencies:
- currently-unhandled "^0.4.1"
- signal-exit "^3.0.0"
-
-loud-rejection@^2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-2.1.0.tgz#4020547ddbc39ed711c8434326df9fc7d2395355"
- integrity sha512-g/6MQxUXYHeVqZ4PGpPL1fS1fOvlXoi7bay0pizmjAd/3JhyXwxzwrnr74yzdmhuerlslbRJ3x7IOXzFz0cE5w==
- dependencies:
- currently-unhandled "^0.4.1"
- signal-exit "^3.0.2"
-
-lowercase-keys@^1.0.0, lowercase-keys@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f"
- integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==
-
-lowercase-keys@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479"
- integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==
-
-lru-cache@^4.0.1:
- version "4.1.5"
- resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd"
- integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==
- dependencies:
- pseudomap "^1.0.2"
- yallist "^2.1.2"
-
-make-dir@^1.0.0:
- version "1.3.0"
- resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c"
- integrity sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==
- dependencies:
- pify "^3.0.0"
-
-make-dir@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.0.0.tgz#1b5f39f6b9270ed33f9f054c5c0f84304989f801"
- integrity sha512-grNJDhb8b1Jm1qeqW5R/O63wUo4UXo2v2HMic6YT9i/HBlF93S8jkMgH7yugvY9ABDShH4VZMn8I+U8+fCNegw==
- dependencies:
- semver "^6.0.0"
-
-map-obj@^1.0.0:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d"
- integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=
-
-map-obj@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-2.0.0.tgz#a65cd29087a92598b8791257a523e021222ac1f9"
- integrity sha1-plzSkIepJZi4eRJXpSPgISIqwfk=
-
-matcher@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/matcher/-/matcher-2.0.0.tgz#85fe38d97670dbd2a46590cf099401e2ffb4755c"
- integrity sha512-nlmfSlgHBFx36j/Pl/KQPbIaqE8Zf0TqmSMjsuddHDg6PMSVgmyW9HpkLs0o0M1n2GIZ/S2BZBLIww/xjhiGng==
- dependencies:
- escape-string-regexp "^2.0.0"
-
-md5-hex@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/md5-hex/-/md5-hex-2.0.0.tgz#d0588e9f1c74954492ecd24ac0ac6ce997d92e33"
- integrity sha1-0FiOnxx0lUSS7NJKwKxs6ZfZLjM=
- dependencies:
- md5-o-matic "^0.1.1"
-
-md5-hex@^3.0.1:
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/md5-hex/-/md5-hex-3.0.1.tgz#be3741b510591434b2784d79e556eefc2c9a8e5c"
- integrity sha512-BUiRtTtV39LIJwinWBjqVsU9xhdnz7/i889V859IBFpuqGAj6LuOvHv5XLbgZ2R7ptJoJaEcxkv88/h25T7Ciw==
- dependencies:
- blueimp-md5 "^2.10.0"
-
-md5-o-matic@^0.1.1:
- version "0.1.1"
- resolved "https://registry.yarnpkg.com/md5-o-matic/-/md5-o-matic-0.1.1.tgz#822bccd65e117c514fab176b25945d54100a03c3"
- integrity sha1-givM1l4RfFFPqxdrJZRdVBAKA8M=
-
-meow@^5.0.0:
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/meow/-/meow-5.0.0.tgz#dfc73d63a9afc714a5e371760eb5c88b91078aa4"
- integrity sha512-CbTqYU17ABaLefO8vCU153ZZlprKYWDljcndKKDCFcYQITzWCXZAVk4QMFZPgvzrnUQ3uItnIE/LoUOwrT15Ig==
- dependencies:
- camelcase-keys "^4.0.0"
- decamelize-keys "^1.0.0"
- loud-rejection "^1.0.0"
- minimist-options "^3.0.1"
- normalize-package-data "^2.3.4"
- read-pkg-up "^3.0.0"
- redent "^2.0.0"
- trim-newlines "^2.0.0"
- yargs-parser "^10.0.0"
-
-merge2@^1.2.3:
- version "1.2.4"
- resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.2.4.tgz#c9269589e6885a60cf80605d9522d4b67ca646e3"
- integrity sha512-FYE8xI+6pjFOhokZu0We3S5NKCirLbCzSh2Usf3qEyr4X8U+0jNg9P8RZ4qz+V2UoECLVwSyzU3LxXBaLGtD3A==
-
-micromatch@^4.0.2:
- version "4.0.2"
- resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259"
- integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==
- dependencies:
- braces "^3.0.1"
- picomatch "^2.0.5"
-
-mimic-fn@^1.0.0:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022"
- integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==
-
-mimic-fn@^2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
- integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
-
-mimic-response@^1.0.0, mimic-response@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b"
- integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==
-
-minimatch@^3.0.4:
- version "3.0.4"
- resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
- integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
- dependencies:
- brace-expansion "^1.1.7"
-
-minimist-options@^3.0.1:
- version "3.0.2"
- resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-3.0.2.tgz#fba4c8191339e13ecf4d61beb03f070103f3d954"
- integrity sha512-FyBrT/d0d4+uiZRbqznPXqw3IpZZG3gl3wKWiX784FycUKVwBt0uLBFkQrtE4tZOrgo78nZp2jnKz3L65T5LdQ==
- dependencies:
- arrify "^1.0.1"
- is-plain-obj "^1.1.0"
-
-minimist@0.0.8:
- version "0.0.8"
- resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
- integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=
-
-minimist@^1.2.0:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
- integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=
-
-mkdirp@^0.5.1:
- version "0.5.1"
- resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
- integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=
- dependencies:
- minimist "0.0.8"
-
-ms@^2.1.1, ms@^2.1.2:
- version "2.1.2"
- resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
- integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
-
-normalize-package-data@^2.3.2, normalize-package-data@^2.3.4:
- version "2.5.0"
- resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"
- integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==
- dependencies:
- hosted-git-info "^2.1.4"
- resolve "^1.10.0"
- semver "2 || 3 || 4 || 5"
- validate-npm-package-license "^3.0.1"
-
-normalize-path@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
- integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
-
-normalize-url@^4.1.0:
- version "4.3.0"
- resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.3.0.tgz#9c49e10fc1876aeb76dba88bf1b2b5d9fa57b2ee"
- integrity sha512-0NLtR71o4k6GLP+mr6Ty34c5GA6CMoEsncKJxvQd8NzPxaHRJNnb5gZE8R1XF4CPIS7QPHLJ74IFszwtNVAHVQ==
-
-npm-run-path@^2.0.0:
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
- integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=
- dependencies:
- path-key "^2.0.0"
-
-object-assign@^4.0.1:
- version "4.1.1"
- resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
- integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
-
-object-keys@^1.0.11, object-keys@^1.0.12:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
- integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==
-
-object.assign@^4.1.0:
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da"
- integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==
- dependencies:
- define-properties "^1.1.2"
- function-bind "^1.1.1"
- has-symbols "^1.0.0"
- object-keys "^1.0.11"
-
-observable-to-promise@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/observable-to-promise/-/observable-to-promise-1.0.0.tgz#37e136f16a15385ac063411ada0e1202bfff58f4"
- integrity sha512-cqnGUrNsE6vdVDTPAX9/WeVzwy/z37vdxupdQXU8vgTXRFH72KCZiZga8aca2ulRPIeem8W3vW9rQHBwfIl2WA==
- dependencies:
- is-observable "^2.0.0"
- symbol-observable "^1.0.4"
-
-once@^1.3.0, once@^1.3.1, once@^1.4.0:
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
- integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
- dependencies:
- wrappy "1"
-
-onetime@^2.0.0:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4"
- integrity sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=
- dependencies:
- mimic-fn "^1.0.0"
-
-onetime@^5.1.0:
- version "5.1.0"
- resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.0.tgz#fff0f3c91617fe62bb50189636e99ac8a6df7be5"
- integrity sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==
- dependencies:
- mimic-fn "^2.1.0"
-
-ora@^3.4.0:
- version "3.4.0"
- resolved "https://registry.yarnpkg.com/ora/-/ora-3.4.0.tgz#bf0752491059a3ef3ed4c85097531de9fdbcd318"
- integrity sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg==
- dependencies:
- chalk "^2.4.2"
- cli-cursor "^2.1.0"
- cli-spinners "^2.0.0"
- log-symbols "^2.2.0"
- strip-ansi "^5.2.0"
- wcwidth "^1.0.1"
-
-os-tmpdir@^1.0.1:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
- integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=
-
-p-cancelable@^1.0.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc"
- integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==
-
-p-finally@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
- integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=
-
-p-limit@^1.1.0:
- version "1.3.0"
- resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8"
- integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==
- dependencies:
- p-try "^1.0.0"
-
-p-limit@^2.0.0, p-limit@^2.2.0:
- version "2.2.0"
- resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.0.tgz#417c9941e6027a9abcba5092dd2904e255b5fbc2"
- integrity sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==
- dependencies:
- p-try "^2.0.0"
-
-p-locate@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43"
- integrity sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=
- dependencies:
- p-limit "^1.1.0"
-
-p-locate@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4"
- integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==
- dependencies:
- p-limit "^2.0.0"
-
-p-locate@^4.1.0:
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07"
- integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==
- dependencies:
- p-limit "^2.2.0"
-
-p-map@^2.0.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175"
- integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==
-
-p-try@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3"
- integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=
-
-p-try@^2.0.0:
- version "2.2.0"
- resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
- integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
-
-package-hash@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/package-hash/-/package-hash-4.0.0.tgz#3537f654665ec3cc38827387fc904c163c54f506"
- integrity sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==
- dependencies:
- graceful-fs "^4.1.15"
- hasha "^5.0.0"
- lodash.flattendeep "^4.4.0"
- release-zalgo "^1.0.0"
-
-package-json@^6.3.0:
- version "6.5.0"
- resolved "https://registry.yarnpkg.com/package-json/-/package-json-6.5.0.tgz#6feedaca35e75725876d0b0e64974697fed145b0"
- integrity sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==
- dependencies:
- got "^9.6.0"
- registry-auth-token "^4.0.0"
- registry-url "^5.0.0"
- semver "^6.2.0"
-
-parse-json@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0"
- integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=
- dependencies:
- error-ex "^1.3.1"
- json-parse-better-errors "^1.0.1"
-
-parse-ms@^2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/parse-ms/-/parse-ms-2.1.0.tgz#348565a753d4391fa524029956b172cb7753097d"
- integrity sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==
-
-path-exists@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
- integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=
-
-path-exists@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
- integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
-
-path-is-absolute@^1.0.0:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
- integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
-
-path-is-inside@^1.0.1, path-is-inside@^1.0.2:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53"
- integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=
-
-path-key@^2.0.0:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
- integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=
-
-path-parse@^1.0.6:
- version "1.0.6"
- resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c"
- integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==
-
-path-type@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f"
- integrity sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==
- dependencies:
- pify "^3.0.0"
-
-path-type@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
- integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
-
-picomatch@^2.0.4, picomatch@^2.0.5:
- version "2.0.7"
- resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.0.7.tgz#514169d8c7cd0bdbeecc8a2609e34a7163de69f6"
- integrity sha512-oLHIdio3tZ0qH76NybpeneBhYVj0QFTfXEFTc/B3zKQspYfYYkWYgFsmzo+4kvId/bQRcNkVeguI3y+CD22BtA==
-
-pify@^2.0.0:
- version "2.3.0"
- resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
- integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw=
-
-pify@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176"
- integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=
-
-pify@^4.0.1:
- version "4.0.1"
- resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231"
- integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==
-
-pinkie-promise@^2.0.0:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa"
- integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o=
- dependencies:
- pinkie "^2.0.0"
-
-pinkie@^2.0.0:
- version "2.0.4"
- resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870"
- integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA=
-
-pkg-conf@^3.1.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/pkg-conf/-/pkg-conf-3.1.0.tgz#d9f9c75ea1bae0e77938cde045b276dac7cc69ae"
- integrity sha512-m0OTbR/5VPNPqO1ph6Fqbj7Hv6QU7gR/tQW40ZqrL1rjgCU85W6C1bJn0BItuJqnR98PWzw7Z8hHeChD1WrgdQ==
- dependencies:
- find-up "^3.0.0"
- load-json-file "^5.2.0"
-
-pkg-dir@^4.2.0:
- version "4.2.0"
- resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3"
- integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==
- dependencies:
- find-up "^4.0.0"
-
-plur@^3.1.1:
- version "3.1.1"
- resolved "https://registry.yarnpkg.com/plur/-/plur-3.1.1.tgz#60267967866a8d811504fe58f2faaba237546a5b"
- integrity sha512-t1Ax8KUvV3FFII8ltczPn2tJdjqbd1sIzu6t4JL7nQ3EyeL/lTrj5PWKb06ic5/6XYDr65rQ4uzQEGN70/6X5w==
- dependencies:
- irregular-plurals "^2.0.0"
-
-prepend-http@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897"
- integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=
-
-pretty-ms@^5.0.0:
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/pretty-ms/-/pretty-ms-5.0.0.tgz#6133a8f55804b208e4728f6aa7bf01085e951e24"
- integrity sha512-94VRYjL9k33RzfKiGokPBPpsmloBYSf5Ri+Pq19zlsEcUKFob+admeXr5eFDRuPjFmEOcjJvPGdillYOJyvZ7Q==
- dependencies:
- parse-ms "^2.1.0"
-
-pseudomap@^1.0.2:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
- integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM=
-
-pump@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
- integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==
- dependencies:
- end-of-stream "^1.1.0"
- once "^1.3.1"
-
-quick-lru@^1.0.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-1.1.0.tgz#4360b17c61136ad38078397ff11416e186dcfbb8"
- integrity sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g=
-
-rc@^1.2.8:
- version "1.2.8"
- resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
- integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==
- dependencies:
- deep-extend "^0.6.0"
- ini "~1.3.0"
- minimist "^1.2.0"
- strip-json-comments "~2.0.1"
-
-read-pkg-up@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-3.0.0.tgz#3ed496685dba0f8fe118d0691dc51f4a1ff96f07"
- integrity sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=
- dependencies:
- find-up "^2.0.0"
- read-pkg "^3.0.0"
-
-read-pkg@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389"
- integrity sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=
- dependencies:
- load-json-file "^4.0.0"
- normalize-package-data "^2.3.2"
- path-type "^3.0.0"
-
-readdirp@^3.1.1:
- version "3.1.2"
- resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.1.2.tgz#fa85d2d14d4289920e4671dead96431add2ee78a"
- integrity sha512-8rhl0xs2cxfVsqzreYCvs8EwBfn/DhVdqtoLmw19uI3SC5avYX9teCurlErfpPXGmYtMHReGaP2RsLnFvz/lnw==
- dependencies:
- picomatch "^2.0.4"
-
-redent@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/redent/-/redent-2.0.0.tgz#c1b2007b42d57eb1389079b3c8333639d5e1ccaa"
- integrity sha1-wbIAe0LVfrE4kHmzyDM2OdXhzKo=
- dependencies:
- indent-string "^3.0.0"
- strip-indent "^2.0.0"
-
-regenerate-unicode-properties@^8.1.0:
- version "8.1.0"
- resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.1.0.tgz#ef51e0f0ea4ad424b77bf7cb41f3e015c70a3f0e"
- integrity sha512-LGZzkgtLY79GeXLm8Dp0BVLdQlWICzBnJz/ipWUgo59qBaZ+BHtq51P2q1uVZlppMuUAT37SDk39qUbjTWB7bA==
- dependencies:
- regenerate "^1.4.0"
-
-regenerate@^1.4.0:
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11"
- integrity sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==
-
-regexpu-core@^4.5.4:
- version "4.5.5"
- resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-4.5.5.tgz#aaffe61c2af58269b3e516b61a73790376326411"
- integrity sha512-FpI67+ky9J+cDizQUJlIlNZFKual/lUkFr1AG6zOCpwZ9cLrg8UUVakyUQJD7fCDIe9Z2nwTQJNPyonatNmDFQ==
- dependencies:
- regenerate "^1.4.0"
- regenerate-unicode-properties "^8.1.0"
- regjsgen "^0.5.0"
- regjsparser "^0.6.0"
- unicode-match-property-ecmascript "^1.0.4"
- unicode-match-property-value-ecmascript "^1.1.0"
-
-registry-auth-token@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-4.0.0.tgz#30e55961eec77379da551ea5c4cf43cbf03522be"
- integrity sha512-lpQkHxd9UL6tb3k/aHAVfnVtn+Bcs9ob5InuFLLEDqSqeq+AljB8GZW9xY0x7F+xYwEcjKe07nyoxzEYz6yvkw==
- dependencies:
- rc "^1.2.8"
- safe-buffer "^5.0.1"
-
-registry-url@^5.0.0:
- version "5.1.0"
- resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-5.1.0.tgz#e98334b50d5434b81136b44ec638d9c2009c5009"
- integrity sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==
- dependencies:
- rc "^1.2.8"
-
-regjsgen@^0.5.0:
- version "0.5.0"
- resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.0.tgz#a7634dc08f89209c2049adda3525711fb97265dd"
- integrity sha512-RnIrLhrXCX5ow/E5/Mh2O4e/oa1/jW0eaBKTSy3LaCj+M3Bqvm97GWDp2yUtzIs4LEn65zR2yiYGFqb2ApnzDA==
-
-regjsparser@^0.6.0:
- version "0.6.0"
- resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.6.0.tgz#f1e6ae8b7da2bae96c99399b868cd6c933a2ba9c"
- integrity sha512-RQ7YyokLiQBomUJuUG8iGVvkgOLxwyZM8k6d3q5SAXpg4r5TZJZigKFvC6PpD+qQ98bCDC5YelPeA3EucDoNeQ==
- dependencies:
- jsesc "~0.5.0"
-
-release-zalgo@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/release-zalgo/-/release-zalgo-1.0.0.tgz#09700b7e5074329739330e535c5a90fb67851730"
- integrity sha1-CXALflB0Mpc5Mw5TXFqQ+2eFFzA=
- dependencies:
- es6-error "^4.0.1"
-
-require-precompiled@^0.1.0:
- version "0.1.0"
- resolved "https://registry.yarnpkg.com/require-precompiled/-/require-precompiled-0.1.0.tgz#5a1b52eb70ebed43eb982e974c85ab59571e56fa"
- integrity sha1-WhtS63Dr7UPrmC6XTIWrWVceVvo=
-
-resolve-cwd@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d"
- integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==
- dependencies:
- resolve-from "^5.0.0"
-
-resolve-from@^5.0.0:
- version "5.0.0"
- resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69"
- integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==
-
-resolve@^1.10.0, resolve@^1.3.2:
- version "1.12.0"
- resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.12.0.tgz#3fc644a35c84a48554609ff26ec52b66fa577df6"
- integrity sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w==
- dependencies:
- path-parse "^1.0.6"
-
-responselike@^1.0.2:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7"
- integrity sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=
- dependencies:
- lowercase-keys "^1.0.0"
-
-restore-cursor@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf"
- integrity sha1-n37ih/gv0ybU/RYpI9YhKe7g368=
- dependencies:
- onetime "^2.0.0"
- signal-exit "^3.0.2"
-
-restore-cursor@^3.1.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e"
- integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==
- dependencies:
- onetime "^5.1.0"
- signal-exit "^3.0.2"
-
-reusify@^1.0.0:
- version "1.0.4"
- resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
- integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
-
-rimraf@^2.6.3:
- version "2.7.1"
- resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
- integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
- dependencies:
- glob "^7.1.3"
-
-run-parallel@^1.1.9:
- version "1.1.9"
- resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.9.tgz#c9dd3a7cf9f4b2c4b6244e173a6ed866e61dd679"
- integrity sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q==
-
-safe-buffer@^5.0.1:
- version "5.2.0"
- resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519"
- integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==
-
-safe-buffer@~5.1.1:
- version "5.1.2"
- resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
- integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
-
-semver-diff@^2.0.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-2.1.0.tgz#4bbb8437c8d37e4b0cf1a68fd726ec6d645d6d36"
- integrity sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY=
- dependencies:
- semver "^5.0.3"
-
-"semver@2 || 3 || 4 || 5", semver@^5.0.3, semver@^5.4.1, semver@^5.5.1:
- version "5.7.1"
- resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
- integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
-
-semver@^6.0.0, semver@^6.2.0:
- version "6.3.0"
- resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
- integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
-
-serialize-error@^2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-2.1.0.tgz#50b679d5635cdf84667bdc8e59af4e5b81d5f60a"
- integrity sha1-ULZ51WNc34Rme9yOWa9OW4HV9go=
-
-shebang-command@^1.2.0:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
- integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=
- dependencies:
- shebang-regex "^1.0.0"
-
-shebang-regex@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
- integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=
-
-signal-exit@^3.0.0, signal-exit@^3.0.2:
- version "3.0.2"
- resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
- integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=
-
-slash@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
- integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
-
-slice-ansi@^2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636"
- integrity sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==
- dependencies:
- ansi-styles "^3.2.0"
- astral-regex "^1.0.0"
- is-fullwidth-code-point "^2.0.0"
-
-source-map-support@^0.5.13:
- version "0.5.13"
- resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932"
- integrity sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==
- dependencies:
- buffer-from "^1.0.0"
- source-map "^0.6.0"
-
-source-map@^0.5.0:
- version "0.5.7"
- resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
- integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
-
-source-map@^0.6.0:
- version "0.6.1"
- resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
- integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
-
-spdx-correct@^3.0.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.0.tgz#fb83e504445268f154b074e218c87c003cd31df4"
- integrity sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==
- dependencies:
- spdx-expression-parse "^3.0.0"
- spdx-license-ids "^3.0.0"
-
-spdx-exceptions@^2.1.0:
- version "2.2.0"
- resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz#2ea450aee74f2a89bfb94519c07fcd6f41322977"
- integrity sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==
-
-spdx-expression-parse@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0"
- integrity sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==
- dependencies:
- spdx-exceptions "^2.1.0"
- spdx-license-ids "^3.0.0"
-
-spdx-license-ids@^3.0.0:
- version "3.0.5"
- resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz#3694b5804567a458d3c8045842a6358632f62654"
- integrity sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==
-
-sprintf-js@~1.0.2:
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
- integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
-
-stack-utils@^1.0.2:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.2.tgz#33eba3897788558bebfc2db059dc158ec36cebb8"
- integrity sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA==
-
-string-width@^2.1.1:
- version "2.1.1"
- resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
- integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==
- dependencies:
- is-fullwidth-code-point "^2.0.0"
- strip-ansi "^4.0.0"
-
-string-width@^3.0.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961"
- integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==
- dependencies:
- emoji-regex "^7.0.1"
- is-fullwidth-code-point "^2.0.0"
- strip-ansi "^5.1.0"
-
-string-width@^4.1.0:
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.1.0.tgz#ba846d1daa97c3c596155308063e075ed1c99aff"
- integrity sha512-NrX+1dVVh+6Y9dnQ19pR0pP4FiEIlUvdTGn8pw6CKTNq5sgib2nIhmUNT5TAmhWmvKr3WcxBcP3E8nWezuipuQ==
- dependencies:
- emoji-regex "^8.0.0"
- is-fullwidth-code-point "^3.0.0"
- strip-ansi "^5.2.0"
-
-strip-ansi@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f"
- integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8=
- dependencies:
- ansi-regex "^3.0.0"
-
-strip-ansi@^5.1.0, strip-ansi@^5.2.0:
- version "5.2.0"
- resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae"
- integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==
- dependencies:
- ansi-regex "^4.1.0"
-
-strip-bom-buf@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/strip-bom-buf/-/strip-bom-buf-2.0.0.tgz#ff9c223937f8e7154b77e9de9bde094186885c15"
- integrity sha512-gLFNHucd6gzb8jMsl5QmZ3QgnUJmp7qn4uUSHNwEXumAp7YizoGYw19ZUVfuq4aBOQUtyn2k8X/CwzWB73W2lQ==
- dependencies:
- is-utf8 "^0.2.1"
-
-strip-bom@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3"
- integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=
-
-strip-eof@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
- integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=
-
-strip-indent@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68"
- integrity sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=
-
-strip-json-comments@~2.0.1:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
- integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
-
-supertap@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/supertap/-/supertap-1.0.0.tgz#bd9751c7fafd68c68cf8222a29892206a119fa9e"
- integrity sha512-HZJ3geIMPgVwKk2VsmO5YHqnnJYl6bV5A9JW2uzqV43WmpgliNEYbuvukfor7URpaqpxuw3CfZ3ONdVbZjCgIA==
- dependencies:
- arrify "^1.0.1"
- indent-string "^3.2.0"
- js-yaml "^3.10.0"
- serialize-error "^2.1.0"
- strip-ansi "^4.0.0"
-
-supports-color@^5.3.0:
- version "5.5.0"
- resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
- integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
- dependencies:
- has-flag "^3.0.0"
-
-supports-color@^7.0.0:
- version "7.0.0"
- resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.0.0.tgz#f2392c50ab35bb3cae7beebf24d254a19f880c06"
- integrity sha512-WRt32iTpYEZWYOpcetGm0NPeSvaebccx7hhS/5M6sAiqnhedtFCHFxkjzZlJvFNCPowiKSFGiZk5USQDFy83vQ==
- dependencies:
- has-flag "^4.0.0"
-
-symbol-observable@^1.0.4:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
- integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==
-
-term-size@^1.2.0:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/term-size/-/term-size-1.2.0.tgz#458b83887f288fc56d6fffbfad262e26638efa69"
- integrity sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk=
- dependencies:
- execa "^0.7.0"
-
-time-zone@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/time-zone/-/time-zone-1.0.0.tgz#99c5bf55958966af6d06d83bdf3800dc82faec5d"
- integrity sha1-mcW/VZWJZq9tBtg73zgA3IL67F0=
-
-to-fast-properties@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
- integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=
-
-to-readable-stream@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/to-readable-stream/-/to-readable-stream-1.0.0.tgz#ce0aa0c2f3df6adf852efb404a783e77c0475771"
- integrity sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==
-
-to-regex-range@^5.0.1:
- version "5.0.1"
- resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
- integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
- dependencies:
- is-number "^7.0.0"
-
-trim-newlines@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-2.0.0.tgz#b403d0b91be50c331dfc4b82eeceb22c3de16d20"
- integrity sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA=
-
-trim-off-newlines@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/trim-off-newlines/-/trim-off-newlines-1.0.1.tgz#9f9ba9d9efa8764c387698bcbfeb2c848f11adb3"
- integrity sha1-n5up2e+odkw4dpi8v+sshI8RrbM=
-
-trim-right@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003"
- integrity sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=
-
-type-fest@^0.3.0:
- version "0.3.1"
- resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.3.1.tgz#63d00d204e059474fe5e1b7c011112bbd1dc29e1"
- integrity sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ==
-
-type-fest@^0.5.2:
- version "0.5.2"
- resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.5.2.tgz#d6ef42a0356c6cd45f49485c3b6281fc148e48a2"
- integrity sha512-DWkS49EQKVX//Tbupb9TFa19c7+MK1XmzkrZUR8TAktmE/DizXoaoJV6TZ/tSIPXipqNiRI6CyAe7x69Jb6RSw==
-
-typedarray-to-buffer@^3.1.5:
- version "3.1.5"
- resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"
- integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==
- dependencies:
- is-typedarray "^1.0.0"
-
-typescript@^3.7.0:
- version "3.7.2"
- resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.2.tgz#27e489b95fa5909445e9fef5ee48d81697ad18fb"
- integrity sha512-ml7V7JfiN2Xwvcer+XAf2csGO1bPBdRbFCkYBczNZggrBZ9c7G3riSUeJmqEU5uOtXNPMhE3n+R4FA/3YOAWOQ==
-
-uid2@0.0.3:
- version "0.0.3"
- resolved "https://registry.yarnpkg.com/uid2/-/uid2-0.0.3.tgz#483126e11774df2f71b8b639dcd799c376162b82"
- integrity sha1-SDEm4Rd03y9xuLY53NeZw3YWK4I=
-
-unicode-canonical-property-names-ecmascript@^1.0.4:
- version "1.0.4"
- resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818"
- integrity sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==
-
-unicode-match-property-ecmascript@^1.0.4:
- version "1.0.4"
- resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz#8ed2a32569961bce9227d09cd3ffbb8fed5f020c"
- integrity sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==
- dependencies:
- unicode-canonical-property-names-ecmascript "^1.0.4"
- unicode-property-aliases-ecmascript "^1.0.4"
-
-unicode-match-property-value-ecmascript@^1.1.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.1.0.tgz#5b4b426e08d13a80365e0d657ac7a6c1ec46a277"
- integrity sha512-hDTHvaBk3RmFzvSl0UVrUmC3PuW9wKVnpoUDYH0JDkSIovzw+J5viQmeYHxVSBptubnr7PbH2e0fnpDRQnQl5g==
-
-unicode-property-aliases-ecmascript@^1.0.4:
- version "1.0.5"
- resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.5.tgz#a9cc6cc7ce63a0a3023fc99e341b94431d405a57"
- integrity sha512-L5RAqCfXqAwR3RriF8pM0lU0w4Ryf/GgzONwi6KnL1taJQa7x1TCxdJnILX59WIGOwR57IVxn7Nej0fz1Ny6fw==
-
-unique-string@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-1.0.0.tgz#9e1057cca851abb93398f8b33ae187b99caec11a"
- integrity sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo=
- dependencies:
- crypto-random-string "^1.0.0"
-
-unique-temp-dir@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/unique-temp-dir/-/unique-temp-dir-1.0.0.tgz#6dce95b2681ca003eebfb304a415f9cbabcc5385"
- integrity sha1-bc6VsmgcoAPuv7MEpBX5y6vMU4U=
- dependencies:
- mkdirp "^0.5.1"
- os-tmpdir "^1.0.1"
- uid2 "0.0.3"
-
-update-notifier@^3.0.1:
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-3.0.1.tgz#78ecb68b915e2fd1be9f767f6e298ce87b736250"
- integrity sha512-grrmrB6Zb8DUiyDIaeRTBCkgISYUgETNe7NglEbVsrLWXeESnlCSP50WfRSj/GmzMPl6Uchj24S/p80nP/ZQrQ==
- dependencies:
- boxen "^3.0.0"
- chalk "^2.0.1"
- configstore "^4.0.0"
- has-yarn "^2.1.0"
- import-lazy "^2.1.0"
- is-ci "^2.0.0"
- is-installed-globally "^0.1.0"
- is-npm "^3.0.0"
- is-yarn-global "^0.3.0"
- latest-version "^5.0.0"
- semver-diff "^2.0.0"
- xdg-basedir "^3.0.0"
-
-url-parse-lax@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-3.0.0.tgz#16b5cafc07dbe3676c1b1999177823d6503acb0c"
- integrity sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=
- dependencies:
- prepend-http "^2.0.0"
-
-validate-npm-package-license@^3.0.1:
- version "3.0.4"
- resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
- integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==
- dependencies:
- spdx-correct "^3.0.0"
- spdx-expression-parse "^3.0.0"
-
-wcwidth@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8"
- integrity sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=
- dependencies:
- defaults "^1.0.3"
-
-well-known-symbols@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/well-known-symbols/-/well-known-symbols-2.0.0.tgz#e9c7c07dbd132b7b84212c8174391ec1f9871ba5"
- integrity sha512-ZMjC3ho+KXo0BfJb7JgtQ5IBuvnShdlACNkKkdsqBmYw3bPAaJfPeYUo6tLUaT5tG/Gkh7xkpBhKRQ9e7pyg9Q==
-
-which@^1.2.9:
- version "1.3.1"
- resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
- integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
- dependencies:
- isexe "^2.0.0"
-
-widest-line@^2.0.0:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-2.0.1.tgz#7438764730ec7ef4381ce4df82fb98a53142a3fc"
- integrity sha512-Ba5m9/Fa4Xt9eb2ELXt77JxVDV8w7qQrH0zS/TWSJdLyAwQjWoOzpzj5lwVftDz6n/EOu3tNACS84v509qwnJA==
- dependencies:
- string-width "^2.1.1"
-
-wrappy@1:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
- integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
-
-write-file-atomic@^2.0.0:
- version "2.4.3"
- resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.4.3.tgz#1fd2e9ae1df3e75b8d8c367443c692d4ca81f481"
- integrity sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==
- dependencies:
- graceful-fs "^4.1.11"
- imurmurhash "^0.1.4"
- signal-exit "^3.0.2"
-
-write-file-atomic@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.0.tgz#1b64dbbf77cb58fd09056963d63e62667ab4fb21"
- integrity sha512-EIgkf60l2oWsffja2Sf2AL384dx328c0B+cIYPTQq5q2rOYuDV00/iPFBOUiDKKwKMOhkymH8AidPaRvzfxY+Q==
- dependencies:
- imurmurhash "^0.1.4"
- is-typedarray "^1.0.0"
- signal-exit "^3.0.2"
- typedarray-to-buffer "^3.1.5"
-
-xdg-basedir@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4"
- integrity sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=
-
-xtend@^4.0.0:
- version "4.0.2"
- resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
- integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
-
-yallist@^2.1.2:
- version "2.1.2"
- resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
- integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=
-
-yargs-parser@^10.0.0:
- version "10.1.0"
- resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-10.1.0.tgz#7202265b89f7e9e9f2e5765e0fe735a905edbaa8"
- integrity sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ==
- dependencies:
- camelcase "^4.1.0"
diff --git a/packages/pogen/package.json b/packages/pogen/package.json
index 25dc6d850..ace5d2d73 100644
--- a/packages/pogen/package.json
+++ b/packages/pogen/package.json
@@ -5,7 +5,7 @@
"author": "Florian Dold",
"license": "GPL-2.0+",
"scripts": {
- "build": "tsc"
+ "compile": "tsc"
},
"devDependencies": {
"typescript": "^3.3.4000"
diff --git a/packages/pogen/tsconfig.json b/packages/pogen/tsconfig.json
index a9e57e9e5..c4a2154ea 100644
--- a/packages/pogen/tsconfig.json
+++ b/packages/pogen/tsconfig.json
@@ -4,7 +4,8 @@
"target": "es5",
"noImplicitAny": false,
"sourceMap": false,
- "outDir": "bin"
+ "outDir": "bin",
+ "incremental": true,
},
"files": [
"pogen.ts"
diff --git a/packages/taler-wallet-android/package.json b/packages/taler-wallet-android/package.json
new file mode 100644
index 000000000..a24dbdbeb
--- /dev/null
+++ b/packages/taler-wallet-android/package.json
@@ -0,0 +1,44 @@
+{
+ "name": "taler-wallet-android",
+ "version": "0.6.12",
+ "description": "",
+ "engines": {
+ "node": ">=0.12.0"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git://git.taler.net/wallet-core.git"
+ },
+ "main": "dist/taler-wallet-android.js",
+ "author": "Florian Dold",
+ "license": "GPL-3.0",
+ "scripts": {
+ "compile": "rollup -c && tsc",
+ "pretty": "prettier --config ../../.prettierrc --write src",
+ "test": "tsc && ava",
+ "coverage": "tsc && nyc ava",
+ "clean": "rimraf lib dist"
+ },
+ "files": [
+ "AUTHORS",
+ "README",
+ "COPYING",
+ "bin/",
+ "dist/node",
+ "src/"
+ ],
+ "devDependencies": {
+ "rimraf": "^3.0.2",
+ "@rollup/plugin-commonjs": "^14.0.0",
+ "@rollup/plugin-json": "^4.1.0",
+ "@rollup/plugin-node-resolve": "^8.4.0",
+ "@rollup/plugin-replace": "^2.3.3",
+ "rollup": "^2.23.0",
+ "rollup-plugin-sourcemaps": "^0.6.2",
+ "rollup-plugin-terser": "^6.1.0"
+ },
+ "dependencies": {
+ "taler-wallet-core": "workspace:*",
+ "tslib": "^2.0.0"
+ }
+}
diff --git a/packages/taler-wallet-android/rollup.config.js b/packages/taler-wallet-android/rollup.config.js
new file mode 100644
index 000000000..7cdca3b98
--- /dev/null
+++ b/packages/taler-wallet-android/rollup.config.js
@@ -0,0 +1,30 @@
+// rollup.config.js
+import commonjs from "@rollup/plugin-commonjs";
+import nodeResolve from "@rollup/plugin-node-resolve";
+import json from "@rollup/plugin-json";
+import builtins from "builtin-modules";
+import pkg from "./package.json";
+
+export default {
+ input: "lib/index.js",
+ output: {
+ file: pkg.main,
+ format: "cjs",
+ },
+ external: builtins,
+ plugins: [
+ nodeResolve({
+ preferBuiltins: true,
+ }),
+
+ commonjs({
+ include: [/node_modules/, /dist/],
+ extensions: [".js", ".ts"],
+ ignoreGlobal: false,
+ sourceMap: false,
+ }),
+
+ json(),
+ ],
+}
+
diff --git a/packages/taler-wallet-android/src/index.d.ts b/packages/taler-wallet-android/src/index.d.ts
new file mode 100644
index 000000000..18e240f35
--- /dev/null
+++ b/packages/taler-wallet-android/src/index.d.ts
@@ -0,0 +1,26 @@
+import {
+ HttpRequestLibrary,
+ HttpResponse,
+ HttpRequestOptions,
+} from "../../taler-wallet-core/src/util/http";
+export {
+ handleWorkerError,
+ handleWorkerMessage,
+} from "../../taler-wallet-core/src/crypto/workers/nodeThreadWorker";
+export declare class AndroidHttpLib implements HttpRequestLibrary {
+ private sendMessage;
+ useNfcTunnel: boolean;
+ private nodeHttpLib;
+ private requestId;
+ private requestMap;
+ constructor(sendMessage: (m: string) => void);
+ get(url: string, opt?: HttpRequestOptions): Promise<HttpResponse>;
+ postJson(
+ url: string,
+ body: any,
+ opt?: HttpRequestOptions,
+ ): Promise<import("../../taler-wallet-core/src/util/http").HttpResponse>;
+ handleTunnelResponse(msg: any): void;
+}
+export declare function installAndroidWalletListener(): void;
+//# sourceMappingURL=index.d.ts.map
diff --git a/packages/taler-wallet-android/src/index.js b/packages/taler-wallet-android/src/index.js
new file mode 100644
index 000000000..ca4b7f971
--- /dev/null
+++ b/packages/taler-wallet-android/src/index.js
@@ -0,0 +1,284 @@
+"use strict";
+/*
+ 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/>
+ */
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.installAndroidWalletListener = exports.AndroidHttpLib = void 0;
+const tslib_1 = require("tslib");
+/**
+ * Imports.
+ */
+const taler_wallet_core_1 = require("taler-wallet-core");
+const promiseUtils_1 = require("../../taler-wallet-core/src/util/promiseUtils");
+const fs_1 = tslib_1.__importDefault(require("fs"));
+const http_1 = require("../../taler-wallet-core/src/util/http");
+const NodeHttpLib_1 = require("../../taler-wallet-core/src/headless/NodeHttpLib");
+const walletCoreApiHandler_1 = require("../../taler-wallet-core/src/walletCoreApiHandler");
+const errors_1 = require("../../taler-wallet-core/src/operations/errors");
+const TalerErrorCode_1 = require("../../taler-wallet-core/src/TalerErrorCode");
+// @ts-ignore: special built-in module
+//import akono = require("akono");
+var nodeThreadWorker_1 = require("../../taler-wallet-core/src/crypto/workers/nodeThreadWorker");
+Object.defineProperty(exports, "handleWorkerError", {
+ enumerable: true,
+ get: function () {
+ return nodeThreadWorker_1.handleWorkerError;
+ },
+});
+Object.defineProperty(exports, "handleWorkerMessage", {
+ enumerable: true,
+ get: function () {
+ return nodeThreadWorker_1.handleWorkerMessage;
+ },
+});
+class AndroidHttpLib {
+ constructor(sendMessage) {
+ this.sendMessage = sendMessage;
+ this.useNfcTunnel = false;
+ this.nodeHttpLib = new NodeHttpLib_1.NodeHttpLib();
+ this.requestId = 1;
+ this.requestMap = {};
+ }
+ get(url, opt) {
+ if (this.useNfcTunnel) {
+ const myId = this.requestId++;
+ const p = promiseUtils_1.openPromise();
+ this.requestMap[myId] = p;
+ const request = {
+ method: "get",
+ url,
+ };
+ this.sendMessage(
+ JSON.stringify({
+ type: "tunnelHttp",
+ request,
+ id: myId,
+ }),
+ );
+ return p.promise;
+ } else {
+ return this.nodeHttpLib.get(url, opt);
+ }
+ }
+ postJson(url, body, opt) {
+ if (this.useNfcTunnel) {
+ const myId = this.requestId++;
+ const p = promiseUtils_1.openPromise();
+ this.requestMap[myId] = p;
+ const request = {
+ method: "postJson",
+ url,
+ body,
+ };
+ this.sendMessage(
+ JSON.stringify({ type: "tunnelHttp", request, id: myId }),
+ );
+ return p.promise;
+ } else {
+ return this.nodeHttpLib.postJson(url, body, opt);
+ }
+ }
+ handleTunnelResponse(msg) {
+ const myId = msg.id;
+ const p = this.requestMap[myId];
+ if (!p) {
+ console.error(
+ `no matching request for tunneled HTTP response, id=${myId}`,
+ );
+ }
+ const headers = new http_1.Headers();
+ if (msg.status != 0) {
+ const resp = {
+ // FIXME: pass through this URL
+ requestUrl: "",
+ headers,
+ status: msg.status,
+ json: () =>
+ tslib_1.__awaiter(this, void 0, void 0, function* () {
+ return JSON.parse(msg.responseText);
+ }),
+ text: () =>
+ tslib_1.__awaiter(this, void 0, void 0, function* () {
+ return msg.responseText;
+ }),
+ };
+ p.resolve(resp);
+ } else {
+ p.reject(new Error(`unexpected HTTP status code ${msg.status}`));
+ }
+ delete this.requestMap[myId];
+ }
+}
+exports.AndroidHttpLib = AndroidHttpLib;
+function sendAkonoMessage(ev) {
+ // @ts-ignore
+ const sendMessage = globalThis.__akono_sendMessage;
+ if (typeof sendMessage !== "function") {
+ const errMsg =
+ "FATAL: cannot install android wallet listener: akono functions missing";
+ console.error(errMsg);
+ throw new Error(errMsg);
+ }
+ const m = JSON.stringify(ev);
+ // @ts-ignore
+ sendMessage(m);
+}
+class AndroidWalletMessageHandler {
+ constructor() {
+ this.wp = promiseUtils_1.openPromise();
+ this.httpLib = new NodeHttpLib_1.NodeHttpLib();
+ }
+ /**
+ * Handle a request from the Android wallet.
+ */
+ handleMessage(operation, id, args) {
+ return tslib_1.__awaiter(this, void 0, void 0, function* () {
+ const wrapResponse = (result) => {
+ return {
+ type: "response",
+ id,
+ operation,
+ result,
+ };
+ };
+ switch (operation) {
+ case "init": {
+ this.walletArgs = {
+ notifyHandler: (notification) =>
+ tslib_1.__awaiter(this, void 0, void 0, function* () {
+ sendAkonoMessage({
+ type: "notification",
+ payload: notification,
+ });
+ }),
+ persistentStoragePath: args.persistentStoragePath,
+ httpLib: this.httpLib,
+ };
+ const w = yield taler_wallet_core_1.getDefaultNodeWallet(
+ this.walletArgs,
+ );
+ this.maybeWallet = w;
+ w.runRetryLoop().catch((e) => {
+ console.error("Error during wallet retry loop", e);
+ });
+ this.wp.resolve(w);
+ return wrapResponse({
+ supported_protocol_versions: {
+ exchange:
+ taler_wallet_core_1.versions.WALLET_EXCHANGE_PROTOCOL_VERSION,
+ merchant:
+ taler_wallet_core_1.versions.WALLET_MERCHANT_PROTOCOL_VERSION,
+ },
+ });
+ }
+ case "getHistory": {
+ return wrapResponse({ history: [] });
+ }
+ case "startTunnel": {
+ // this.httpLib.useNfcTunnel = true;
+ throw Error("not implemented");
+ }
+ case "stopTunnel": {
+ // this.httpLib.useNfcTunnel = false;
+ throw Error("not implemented");
+ }
+ case "tunnelResponse": {
+ // httpLib.handleTunnelResponse(msg.args);
+ throw Error("not implemented");
+ }
+ case "reset": {
+ const oldArgs = this.walletArgs;
+ this.walletArgs = Object.assign({}, oldArgs);
+ if (oldArgs && oldArgs.persistentStoragePath) {
+ try {
+ fs_1.default.unlinkSync(oldArgs.persistentStoragePath);
+ } catch (e) {
+ console.error("Error while deleting the wallet db:", e);
+ }
+ // Prevent further storage!
+ this.walletArgs.persistentStoragePath = undefined;
+ }
+ const wallet = yield this.wp.promise;
+ wallet.stop();
+ this.wp = promiseUtils_1.openPromise();
+ this.maybeWallet = undefined;
+ const w = yield taler_wallet_core_1.getDefaultNodeWallet(
+ this.walletArgs,
+ );
+ this.maybeWallet = w;
+ w.runRetryLoop().catch((e) => {
+ console.error("Error during wallet retry loop", e);
+ });
+ this.wp.resolve(w);
+ return wrapResponse({});
+ }
+ default: {
+ const wallet = yield this.wp.promise;
+ return yield walletCoreApiHandler_1.handleCoreApiRequest(
+ wallet,
+ operation,
+ id,
+ args,
+ );
+ }
+ }
+ });
+ }
+}
+function installAndroidWalletListener() {
+ const handler = new AndroidWalletMessageHandler();
+ const onMessage = (msgStr) =>
+ tslib_1.__awaiter(this, void 0, void 0, function* () {
+ if (typeof msgStr !== "string") {
+ console.error("expected string as message");
+ return;
+ }
+ const msg = JSON.parse(msgStr);
+ const operation = msg.operation;
+ if (typeof operation !== "string") {
+ console.error(
+ "message to android wallet helper must contain operation of type string",
+ );
+ return;
+ }
+ const id = msg.id;
+ console.log(`android listener: got request for ${operation} (${id})`);
+ try {
+ const respMsg = yield handler.handleMessage(operation, id, msg.args);
+ console.log(
+ `android listener: sending success response for ${operation} (${id})`,
+ );
+ sendAkonoMessage(respMsg);
+ } catch (e) {
+ const respMsg = {
+ type: "error",
+ id,
+ operation,
+ error: errors_1.makeErrorDetails(
+ TalerErrorCode_1.TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
+ "unexpected exception",
+ {},
+ ),
+ };
+ sendAkonoMessage(respMsg);
+ return;
+ }
+ });
+ // @ts-ignore
+ globalThis.__akono_onMessage = onMessage;
+ console.log("android wallet listener installed");
+}
+exports.installAndroidWalletListener = installAndroidWalletListener;
+//# sourceMappingURL=index.js.map
diff --git a/packages/taler-wallet-android/src/index.js.map b/packages/taler-wallet-android/src/index.js.map
new file mode 100644
index 000000000..2c6d50b9f
--- /dev/null
+++ b/packages/taler-wallet-android/src/index.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"index.js","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"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"} \ No newline at end of file
diff --git a/packages/taler-wallet-android/src/index.ts b/packages/taler-wallet-android/src/index.ts
new file mode 100644
index 000000000..d0001e991
--- /dev/null
+++ b/packages/taler-wallet-android/src/index.ts
@@ -0,0 +1,285 @@
+/*
+ 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/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ Wallet,
+ getDefaultNodeWallet,
+ DefaultNodeWalletArgs,
+ versions,
+ httpLib,
+ nodeThreadWorker,
+ promiseUtil,
+ NodeHttpLib,
+ walletCoreApi,
+ walletNotifications,
+ TalerErrorCode,
+ makeErrorDetails,
+} from "taler-wallet-core";
+
+import fs from "fs";
+
+export const handleWorkerError = nodeThreadWorker.handleWorkerError;
+export const handleWorkerMessage = nodeThreadWorker.handleWorkerMessage;
+
+export class AndroidHttpLib implements httpLib.HttpRequestLibrary {
+ useNfcTunnel = false;
+
+ private nodeHttpLib: httpLib.HttpRequestLibrary = new NodeHttpLib();
+
+ private requestId = 1;
+
+ private requestMap: {
+ [id: number]: promiseUtil.OpenedPromise<httpLib.HttpResponse>;
+ } = {};
+
+ constructor(private sendMessage: (m: string) => void) {}
+
+ get(
+ url: string,
+ opt?: httpLib.HttpRequestOptions,
+ ): Promise<httpLib.HttpResponse> {
+ if (this.useNfcTunnel) {
+ const myId = this.requestId++;
+ const p = promiseUtil.openPromise<httpLib.HttpResponse>();
+ this.requestMap[myId] = p;
+ const request = {
+ method: "get",
+ url,
+ };
+ this.sendMessage(
+ JSON.stringify({
+ type: "tunnelHttp",
+ request,
+ id: myId,
+ }),
+ );
+ return p.promise;
+ } else {
+ return this.nodeHttpLib.get(url, opt);
+ }
+ }
+
+ postJson(
+ url: string,
+ body: any,
+ opt?: httpLib.HttpRequestOptions,
+ ): Promise<httpLib.HttpResponse> {
+ if (this.useNfcTunnel) {
+ const myId = this.requestId++;
+ const p = promiseUtil.openPromise<httpLib.HttpResponse>();
+ this.requestMap[myId] = p;
+ const request = {
+ method: "postJson",
+ url,
+ body,
+ };
+ this.sendMessage(
+ JSON.stringify({ type: "tunnelHttp", request, id: myId }),
+ );
+ return p.promise;
+ } else {
+ return this.nodeHttpLib.postJson(url, body, opt);
+ }
+ }
+
+ handleTunnelResponse(msg: any): void {
+ const myId = msg.id;
+ const p = this.requestMap[myId];
+ if (!p) {
+ console.error(
+ `no matching request for tunneled HTTP response, id=${myId}`,
+ );
+ }
+ const headers = new httpLib.Headers();
+ if (msg.status != 0) {
+ const resp: httpLib.HttpResponse = {
+ // FIXME: pass through this URL
+ requestUrl: "",
+ headers,
+ status: msg.status,
+ json: async () => JSON.parse(msg.responseText),
+ text: async () => msg.responseText,
+ };
+ p.resolve(resp);
+ } else {
+ p.reject(new Error(`unexpected HTTP status code ${msg.status}`));
+ }
+ delete this.requestMap[myId];
+ }
+}
+
+function sendAkonoMessage(ev: walletCoreApi.CoreApiEnvelope): void {
+ // @ts-ignore
+ const sendMessage = globalThis.__akono_sendMessage;
+ if (typeof sendMessage !== "function") {
+ const errMsg =
+ "FATAL: cannot install android wallet listener: akono functions missing";
+ console.error(errMsg);
+ throw new Error(errMsg);
+ }
+ const m = JSON.stringify(ev);
+ // @ts-ignore
+ sendMessage(m);
+}
+
+class AndroidWalletMessageHandler {
+ walletArgs: DefaultNodeWalletArgs | undefined;
+ maybeWallet: Wallet | undefined;
+ wp = promiseUtil.openPromise<Wallet>();
+ httpLib = new NodeHttpLib();
+
+ /**
+ * Handle a request from the Android wallet.
+ */
+ async handleMessage(
+ operation: string,
+ id: string,
+ args: any,
+ ): Promise<walletCoreApi.CoreApiResponse> {
+ const wrapResponse = (
+ result: unknown,
+ ): walletCoreApi.CoreApiResponseSuccess => {
+ return {
+ type: "response",
+ id,
+ operation,
+ result,
+ };
+ };
+ switch (operation) {
+ case "init": {
+ this.walletArgs = {
+ notifyHandler: async (
+ notification: walletNotifications.WalletNotification,
+ ) => {
+ sendAkonoMessage({ type: "notification", payload: notification });
+ },
+ persistentStoragePath: args.persistentStoragePath,
+ httpLib: this.httpLib,
+ };
+ const w = await getDefaultNodeWallet(this.walletArgs);
+ this.maybeWallet = w;
+ w.runRetryLoop().catch((e) => {
+ console.error("Error during wallet retry loop", e);
+ });
+ this.wp.resolve(w);
+ return wrapResponse({
+ supported_protocol_versions: {
+ exchange: versions.WALLET_EXCHANGE_PROTOCOL_VERSION,
+ merchant: versions.WALLET_MERCHANT_PROTOCOL_VERSION,
+ },
+ });
+ }
+ case "getHistory": {
+ return wrapResponse({ history: [] });
+ }
+ case "startTunnel": {
+ // this.httpLib.useNfcTunnel = true;
+ throw Error("not implemented");
+ }
+ case "stopTunnel": {
+ // this.httpLib.useNfcTunnel = false;
+ throw Error("not implemented");
+ }
+ case "tunnelResponse": {
+ // httpLib.handleTunnelResponse(msg.args);
+ throw Error("not implemented");
+ }
+ case "reset": {
+ const oldArgs = this.walletArgs;
+ this.walletArgs = { ...oldArgs };
+ if (oldArgs && oldArgs.persistentStoragePath) {
+ try {
+ fs.unlinkSync(oldArgs.persistentStoragePath);
+ } catch (e) {
+ console.error("Error while deleting the wallet db:", e);
+ }
+ // Prevent further storage!
+ this.walletArgs.persistentStoragePath = undefined;
+ }
+ const wallet = await this.wp.promise;
+ wallet.stop();
+ this.wp = promiseUtil.openPromise<Wallet>();
+ this.maybeWallet = undefined;
+ const w = await getDefaultNodeWallet(this.walletArgs);
+ this.maybeWallet = w;
+ w.runRetryLoop().catch((e) => {
+ console.error("Error during wallet retry loop", e);
+ });
+ this.wp.resolve(w);
+ return wrapResponse({});
+ }
+ default: {
+ const wallet = await this.wp.promise;
+ return await walletCoreApi.handleCoreApiRequest(
+ wallet,
+ operation,
+ id,
+ args,
+ );
+ }
+ }
+ }
+}
+
+export function installAndroidWalletListener(): void {
+ const handler = new AndroidWalletMessageHandler();
+ const onMessage = async (msgStr: any): Promise<void> => {
+ if (typeof msgStr !== "string") {
+ console.error("expected string as message");
+ return;
+ }
+ const msg = JSON.parse(msgStr);
+ const operation = msg.operation;
+ if (typeof operation !== "string") {
+ console.error(
+ "message to android wallet helper must contain operation of type string",
+ );
+ return;
+ }
+ const id = msg.id;
+ console.log(`android listener: got request for ${operation} (${id})`);
+
+ try {
+ const respMsg = await handler.handleMessage(operation, id, msg.args);
+ console.log(
+ `android listener: sending success response for ${operation} (${id})`,
+ );
+ sendAkonoMessage(respMsg);
+ } catch (e) {
+ const respMsg: walletCoreApi.CoreApiResponse = {
+ type: "error",
+ id,
+ operation,
+ error: makeErrorDetails(
+ TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
+ "unexpected exception",
+ {},
+ ),
+ };
+ sendAkonoMessage(respMsg);
+ return;
+ }
+ };
+
+ // @ts-ignore
+ globalThis.__akono_onMessage = onMessage;
+
+ console.log("android wallet listener installed");
+}
diff --git a/packages/taler-wallet-android/tsconfig.json b/packages/taler-wallet-android/tsconfig.json
new file mode 100644
index 000000000..abb21b4db
--- /dev/null
+++ b/packages/taler-wallet-android/tsconfig.json
@@ -0,0 +1,32 @@
+{
+ "compileOnSave": true,
+ "compilerOptions": {
+ "composite": true,
+ "declaration": true,
+ "declarationMap": true,
+ "target": "ES6",
+ "jsx": "react",
+ "reactNamespace": "React",
+ "module": "commonjs",
+ "moduleResolution": "node",
+ "sourceMap": true,
+ "lib": [
+ "es6",
+ "dom"
+ ],
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "strict": true,
+ "strictPropertyInitialization": false,
+ "outDir": "lib",
+ "noImplicitAny": true,
+ "noImplicitThis": true,
+ "allowJs": true,
+ "checkJs": true,
+ "incremental": true,
+ "esModuleInterop": true,
+ "importHelpers": true,
+ "rootDir": "./src"
+ },
+ "include": ["src/**/*"]
+}
diff --git a/packages/taler-wallet-cli/bin/taler-wallet-cli b/packages/taler-wallet-cli/bin/taler-wallet-cli
new file mode 100755
index 000000000..871514024
--- /dev/null
+++ b/packages/taler-wallet-cli/bin/taler-wallet-cli
@@ -0,0 +1,7 @@
+#!/usr/bin/env node
+try {
+ require('source-map-support').install();
+} catch (e) {
+ // Do nothing.
+}
+require('../dist/taler-wallet-cli.js')
diff --git a/packages/taler-wallet-cli/package.json b/packages/taler-wallet-cli/package.json
new file mode 100644
index 000000000..1d4460021
--- /dev/null
+++ b/packages/taler-wallet-cli/package.json
@@ -0,0 +1,48 @@
+{
+ "name": "taler-wallet-cli",
+ "version": "0.6.12",
+ "description": "",
+ "engines": {
+ "node": ">=0.12.0"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git://git.taler.net/wallet-core.git"
+ },
+ "author": "Florian Dold",
+ "license": "GPL-3.0",
+ "main": "dist/taler-wallet-cli.js",
+ "bin": {
+ "taler-wallet-cli": "./bin/taler-wallet-cli"
+ },
+ "scripts": {
+ "compile": "tsc && rollup -c",
+ "clean": "rimraf lib dist",
+ "pretty": "prettier --config ../../.prettierrc --write src"
+ },
+ "files": [
+ "AUTHORS",
+ "README",
+ "COPYING",
+ "bin/",
+ "dist/node",
+ "src/"
+ ],
+ "devDependencies": {
+ "@rollup/plugin-commonjs": "^14.0.0",
+ "@rollup/plugin-json": "^4.1.0",
+ "@rollup/plugin-node-resolve": "^8.4.0",
+ "@rollup/plugin-replace": "^2.3.3",
+ "rimraf": "^3.0.2",
+ "rollup": "^2.23.0",
+ "rollup-plugin-sourcemaps": "^0.6.2",
+ "rollup-plugin-terser": "^6.1.0",
+ "typedoc": "^0.17.8",
+ "typescript": "^3.9.7"
+ },
+ "dependencies": {
+ "source-map-support": "^0.5.19",
+ "taler-wallet-core": "workspace:*",
+ "tslib": "^2.0.0"
+ }
+}
diff --git a/packages/taler-wallet-cli/rollup.config.js b/packages/taler-wallet-cli/rollup.config.js
new file mode 100644
index 000000000..7cdca3b98
--- /dev/null
+++ b/packages/taler-wallet-cli/rollup.config.js
@@ -0,0 +1,30 @@
+// rollup.config.js
+import commonjs from "@rollup/plugin-commonjs";
+import nodeResolve from "@rollup/plugin-node-resolve";
+import json from "@rollup/plugin-json";
+import builtins from "builtin-modules";
+import pkg from "./package.json";
+
+export default {
+ input: "lib/index.js",
+ output: {
+ file: pkg.main,
+ format: "cjs",
+ },
+ external: builtins,
+ plugins: [
+ nodeResolve({
+ preferBuiltins: true,
+ }),
+
+ commonjs({
+ include: [/node_modules/, /dist/],
+ extensions: [".js", ".ts"],
+ ignoreGlobal: false,
+ sourceMap: false,
+ }),
+
+ json(),
+ ],
+}
+
diff --git a/packages/taler-wallet-cli/src/clk.ts b/packages/taler-wallet-cli/src/clk.ts
new file mode 100644
index 000000000..a905464bd
--- /dev/null
+++ b/packages/taler-wallet-cli/src/clk.ts
@@ -0,0 +1,614 @@
+/*
+ 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/>
+ */
+
+/**
+ * Imports.
+ */
+import process from "process";
+import path from "path";
+import readline from "readline";
+
+class Converter<T> {}
+
+export const INT = new Converter<number>();
+export const STRING: Converter<string> = new Converter<string>();
+
+export interface OptionArgs<T> {
+ help?: string;
+ default?: T;
+ onPresentHandler?: (v: T) => void;
+}
+
+export interface ArgumentArgs<T> {
+ metavar?: string;
+ help?: string;
+ default?: T;
+}
+
+export interface SubcommandArgs {
+ help?: string;
+}
+
+export interface FlagArgs {
+ help?: string;
+}
+
+export interface ProgramArgs {
+ help?: string;
+}
+
+interface ArgumentDef {
+ name: string;
+ conv: Converter<any>;
+ args: ArgumentArgs<any>;
+ required: boolean;
+}
+
+interface SubcommandDef {
+ commandGroup: CommandGroup<any, any>;
+ name: string;
+ args: SubcommandArgs;
+}
+
+type ActionFn<TG> = (x: TG) => void;
+
+type SubRecord<S extends keyof any, N extends keyof any, V> = {
+ [Y in S]: { [X in N]: V };
+};
+
+interface OptionDef {
+ name: string;
+ flagspec: string[];
+ /**
+ * Converter, only present for options, not for flags.
+ */
+ conv?: Converter<any>;
+ args: OptionArgs<any>;
+ isFlag: boolean;
+ required: boolean;
+}
+
+function splitOpt(opt: string): { key: string; value?: string } {
+ const idx = opt.indexOf("=");
+ if (idx == -1) {
+ return { key: opt };
+ }
+ return { key: opt.substring(0, idx), value: opt.substring(idx + 1) };
+}
+
+function formatListing(key: string, value?: string): string {
+ const res = " " + key;
+ if (!value) {
+ return res;
+ }
+ if (res.length >= 25) {
+ return res + "\n" + " " + value;
+ } else {
+ return res.padEnd(24) + " " + value;
+ }
+}
+
+export class CommandGroup<GN extends keyof any, TG> {
+ private shortOptions: { [name: string]: OptionDef } = {};
+ private longOptions: { [name: string]: OptionDef } = {};
+ private subcommandMap: { [name: string]: SubcommandDef } = {};
+ private subcommands: SubcommandDef[] = [];
+ private options: OptionDef[] = [];
+ private arguments: ArgumentDef[] = [];
+
+ private myAction?: ActionFn<TG>;
+
+ constructor(
+ private argKey: string,
+ private name: string | null,
+ private scArgs: SubcommandArgs,
+ ) {}
+
+ action(f: ActionFn<TG>): void {
+ if (this.myAction) {
+ throw Error("only one action supported per command");
+ }
+ this.myAction = f;
+ }
+
+ requiredOption<N extends keyof any, V>(
+ name: N,
+ flagspec: string[],
+ conv: Converter<V>,
+ args: OptionArgs<V> = {},
+ ): CommandGroup<GN, TG & SubRecord<GN, N, V>> {
+ const def: OptionDef = {
+ args: args,
+ conv: conv,
+ flagspec: flagspec,
+ isFlag: false,
+ required: true,
+ name: name as string,
+ };
+ this.options.push(def);
+ for (const flag of flagspec) {
+ if (flag.startsWith("--")) {
+ const flagname = flag.substring(2);
+ this.longOptions[flagname] = def;
+ } else if (flag.startsWith("-")) {
+ const flagname = flag.substring(1);
+ this.shortOptions[flagname] = def;
+ } else {
+ throw Error("option must start with '-' or '--'");
+ }
+ }
+ return this as any;
+ }
+
+ maybeOption<N extends keyof any, V>(
+ name: N,
+ flagspec: string[],
+ conv: Converter<V>,
+ args: OptionArgs<V> = {},
+ ): CommandGroup<GN, TG & SubRecord<GN, N, V | undefined>> {
+ const def: OptionDef = {
+ args: args,
+ conv: conv,
+ flagspec: flagspec,
+ isFlag: false,
+ required: false,
+ name: name as string,
+ };
+ this.options.push(def);
+ for (const flag of flagspec) {
+ if (flag.startsWith("--")) {
+ const flagname = flag.substring(2);
+ this.longOptions[flagname] = def;
+ } else if (flag.startsWith("-")) {
+ const flagname = flag.substring(1);
+ this.shortOptions[flagname] = def;
+ } else {
+ throw Error("option must start with '-' or '--'");
+ }
+ }
+ return this as any;
+ }
+
+ requiredArgument<N extends keyof any, V>(
+ name: N,
+ conv: Converter<V>,
+ args: ArgumentArgs<V> = {},
+ ): CommandGroup<GN, TG & SubRecord<GN, N, V>> {
+ const argDef: ArgumentDef = {
+ args: args,
+ conv: conv,
+ name: name as string,
+ required: true,
+ };
+ this.arguments.push(argDef);
+ return this as any;
+ }
+
+ maybeArgument<N extends keyof any, V>(
+ name: N,
+ conv: Converter<V>,
+ args: ArgumentArgs<V> = {},
+ ): CommandGroup<GN, TG & SubRecord<GN, N, V | undefined>> {
+ const argDef: ArgumentDef = {
+ args: args,
+ conv: conv,
+ name: name as string,
+ required: false,
+ };
+ this.arguments.push(argDef);
+ return this as any;
+ }
+
+ flag<N extends string, V>(
+ name: N,
+ flagspec: string[],
+ args: OptionArgs<V> = {},
+ ): CommandGroup<GN, TG & SubRecord<GN, N, boolean>> {
+ const def: OptionDef = {
+ args: args,
+ flagspec: flagspec,
+ isFlag: true,
+ required: false,
+ name: name as string,
+ };
+ this.options.push(def);
+ for (const flag of flagspec) {
+ if (flag.startsWith("--")) {
+ const flagname = flag.substring(2);
+ this.longOptions[flagname] = def;
+ } else if (flag.startsWith("-")) {
+ const flagname = flag.substring(1);
+ this.shortOptions[flagname] = def;
+ } else {
+ throw Error("option must start with '-' or '--'");
+ }
+ }
+ return this as any;
+ }
+
+ subcommand<GN extends keyof any>(
+ argKey: GN,
+ name: string,
+ args: SubcommandArgs = {},
+ ): CommandGroup<GN, TG> {
+ const cg = new CommandGroup<GN, {}>(argKey as string, name, args);
+ const def: SubcommandDef = {
+ commandGroup: cg,
+ name: name as string,
+ args: args,
+ };
+ cg.flag("help", ["-h", "--help"], {
+ help: "Show this message and exit.",
+ });
+ this.subcommandMap[name as string] = def;
+ this.subcommands.push(def);
+ this.subcommands = this.subcommands.sort((x1, x2) => {
+ const a = x1.name;
+ const b = x2.name;
+ if (a === b) {
+ return 0;
+ } else if (a < b) {
+ return -1;
+ } else {
+ return 1;
+ }
+ });
+ return cg as any;
+ }
+
+ printHelp(progName: string, parents: CommandGroup<any, any>[]): void {
+ let usageSpec = "";
+ for (const p of parents) {
+ usageSpec += (p.name ?? progName) + " ";
+ if (p.arguments.length >= 1) {
+ usageSpec += "<ARGS...> ";
+ }
+ }
+ usageSpec += (this.name ?? progName) + " ";
+ if (this.subcommands.length != 0) {
+ usageSpec += "COMMAND ";
+ }
+ for (const a of this.arguments) {
+ const argName = a.args.metavar ?? a.name;
+ usageSpec += `<${argName}> `;
+ }
+ usageSpec = usageSpec.trimRight();
+ console.log(`Usage: ${usageSpec}`);
+ if (this.scArgs.help) {
+ console.log();
+ console.log(this.scArgs.help);
+ }
+ if (this.options.length != 0) {
+ console.log();
+ console.log("Options:");
+ for (const opt of this.options) {
+ let optSpec = opt.flagspec.join(", ");
+ if (!opt.isFlag) {
+ optSpec = optSpec + "=VALUE";
+ }
+ console.log(formatListing(optSpec, opt.args.help));
+ }
+ }
+
+ if (this.subcommands.length != 0) {
+ console.log();
+ console.log("Commands:");
+ for (const subcmd of this.subcommands) {
+ console.log(formatListing(subcmd.name, subcmd.args.help));
+ }
+ }
+ }
+
+ /**
+ * Run the (sub-)command with the given command line parameters.
+ */
+ run(
+ progname: string,
+ parents: CommandGroup<any, any>[],
+ unparsedArgs: string[],
+ parsedArgs: any,
+ ): void {
+ let posArgIndex = 0;
+ let argsTerminated = false;
+ let i;
+ let foundSubcommand: CommandGroup<any, any> | undefined = undefined;
+ const myArgs: any = (parsedArgs[this.argKey] = {});
+ const foundOptions: { [name: string]: boolean } = {};
+ const currentName = this.name ?? progname;
+ for (i = 0; i < unparsedArgs.length; i++) {
+ const argVal = unparsedArgs[i];
+ if (argsTerminated == false) {
+ if (argVal === "--") {
+ argsTerminated = true;
+ continue;
+ }
+ if (argVal.startsWith("--")) {
+ const opt = argVal.substring(2);
+ const r = splitOpt(opt);
+ const d = this.longOptions[r.key];
+ if (!d) {
+ console.error(
+ `error: unknown option '--${r.key}' for ${currentName}`,
+ );
+ process.exit(-1);
+ throw Error("not reached");
+ }
+ if (d.isFlag) {
+ if (r.value !== undefined) {
+ console.error(`error: flag '--${r.key}' does not take a value`);
+ process.exit(-1);
+ throw Error("not reached");
+ }
+ foundOptions[d.name] = true;
+ myArgs[d.name] = true;
+ } else {
+ if (r.value === undefined) {
+ if (i === unparsedArgs.length - 1) {
+ console.error(`error: option '--${r.key}' needs an argument`);
+ process.exit(-1);
+ throw Error("not reached");
+ }
+ myArgs[d.name] = unparsedArgs[i + 1];
+ i++;
+ } else {
+ myArgs[d.name] = r.value;
+ }
+ foundOptions[d.name] = true;
+ }
+ continue;
+ }
+ if (argVal.startsWith("-") && argVal != "-") {
+ const optShort = argVal.substring(1);
+ for (let si = 0; si < optShort.length; si++) {
+ const chr = optShort[si];
+ const opt = this.shortOptions[chr];
+ if (!opt) {
+ console.error(`error: option '-${chr}' not known`);
+ process.exit(-1);
+ }
+ if (opt.isFlag) {
+ myArgs[opt.name] = true;
+ foundOptions[opt.name] = true;
+ } else {
+ if (si == optShort.length - 1) {
+ if (i === unparsedArgs.length - 1) {
+ console.error(`error: option '-${chr}' needs an argument`);
+ process.exit(-1);
+ throw Error("not reached");
+ } else {
+ myArgs[opt.name] = unparsedArgs[i + 1];
+ i++;
+ }
+ } else {
+ myArgs[opt.name] = optShort.substring(si + 1);
+ }
+ foundOptions[opt.name] = true;
+ break;
+ }
+ }
+ continue;
+ }
+ }
+ if (this.subcommands.length != 0) {
+ const subcmd = this.subcommandMap[argVal];
+ if (!subcmd) {
+ console.error(`error: unknown command '${argVal}'`);
+ process.exit(-1);
+ throw Error("not reached");
+ }
+ foundSubcommand = subcmd.commandGroup;
+ break;
+ } else {
+ const d = this.arguments[posArgIndex];
+ if (!d) {
+ console.error(`error: too many arguments for ${currentName}`);
+ process.exit(-1);
+ throw Error("not reached");
+ }
+ myArgs[d.name] = unparsedArgs[i];
+ posArgIndex++;
+ }
+ }
+
+ if (parsedArgs[this.argKey].help) {
+ this.printHelp(progname, parents);
+ process.exit(0);
+ throw Error("not reached");
+ }
+
+ for (let i = posArgIndex; i < this.arguments.length; i++) {
+ const d = this.arguments[i];
+ if (d.required) {
+ if (d.args.default !== undefined) {
+ myArgs[d.name] = d.args.default;
+ } else {
+ console.error(
+ `error: missing positional argument '${d.name}' for ${currentName}`,
+ );
+ process.exit(-1);
+ throw Error("not reached");
+ }
+ }
+ }
+
+ for (const option of this.options) {
+ if (option.isFlag == false && option.required == true) {
+ if (!foundOptions[option.name]) {
+ if (option.args.default !== undefined) {
+ myArgs[option.name] = option.args.default;
+ } else {
+ const name = option.flagspec.join(",");
+ console.error(`error: missing option '${name}'`);
+ process.exit(-1);
+ throw Error("not reached");
+ }
+ }
+ }
+ }
+
+ for (const option of this.options) {
+ const ph = option.args.onPresentHandler;
+ if (ph && foundOptions[option.name]) {
+ ph(myArgs[option.name]);
+ }
+ }
+
+ if (foundSubcommand) {
+ foundSubcommand.run(
+ progname,
+ Array.prototype.concat(parents, [this]),
+ unparsedArgs.slice(i + 1),
+ parsedArgs,
+ );
+ } else if (this.myAction) {
+ let r;
+ try {
+ r = this.myAction(parsedArgs);
+ } catch (e) {
+ console.error(`An error occured while running ${currentName}`);
+ console.error(e);
+ process.exit(1);
+ }
+ Promise.resolve(r).catch((e) => {
+ console.error(`An error occured while running ${currentName}`);
+ console.error(e);
+ process.exit(1);
+ });
+ } else {
+ this.printHelp(progname, parents);
+ process.exit(-1);
+ throw Error("not reached");
+ }
+ }
+}
+
+export class Program<PN extends keyof any, T> {
+ private mainCommand: CommandGroup<any, any>;
+
+ constructor(argKey: string, args: ProgramArgs = {}) {
+ this.mainCommand = new CommandGroup<any, any>(argKey, null, {
+ help: args.help,
+ });
+ this.mainCommand.flag("help", ["-h", "--help"], {
+ help: "Show this message and exit.",
+ });
+ }
+
+ run(): void {
+ const args = process.argv;
+ if (args.length < 2) {
+ console.error(
+ "Error while parsing command line arguments: not enough arguments",
+ );
+ process.exit(-1);
+ }
+ const progname = path.basename(args[1]);
+ const rest = args.slice(2);
+
+ this.mainCommand.run(progname, [], rest, {});
+ }
+
+ subcommand<GN extends keyof any>(
+ argKey: GN,
+ name: string,
+ args: SubcommandArgs = {},
+ ): CommandGroup<GN, T> {
+ const cmd = this.mainCommand.subcommand(argKey, name as string, args);
+ return cmd as any;
+ }
+
+ requiredOption<N extends keyof any, V>(
+ name: N,
+ flagspec: string[],
+ conv: Converter<V>,
+ args: OptionArgs<V> = {},
+ ): Program<PN, T & SubRecord<PN, N, V>> {
+ this.mainCommand.requiredOption(name, flagspec, conv, args);
+ return this as any;
+ }
+
+ maybeOption<N extends keyof any, V>(
+ name: N,
+ flagspec: string[],
+ conv: Converter<V>,
+ args: OptionArgs<V> = {},
+ ): Program<PN, T & SubRecord<PN, N, V | undefined>> {
+ this.mainCommand.maybeOption(name, flagspec, conv, args);
+ return this as any;
+ }
+
+ /**
+ * Add a flag (option without value) to the program.
+ */
+ flag<N extends string>(
+ name: N,
+ flagspec: string[],
+ args: OptionArgs<boolean> = {},
+ ): Program<PN, T & SubRecord<PN, N, boolean>> {
+ this.mainCommand.flag(name, flagspec, args);
+ return this as any;
+ }
+
+ /**
+ * Add a required positional argument to the program.
+ */
+ requiredArgument<N extends keyof any, V>(
+ name: N,
+ conv: Converter<V>,
+ args: ArgumentArgs<V> = {},
+ ): Program<N, T & SubRecord<PN, N, V>> {
+ this.mainCommand.requiredArgument(name, conv, args);
+ return this as any;
+ }
+
+ /**
+ * Add an optional argument to the program.
+ */
+ maybeArgument<N extends keyof any, V>(
+ name: N,
+ conv: Converter<V>,
+ args: ArgumentArgs<V> = {},
+ ): Program<N, T & SubRecord<PN, N, V | undefined>> {
+ this.mainCommand.maybeArgument(name, conv, args);
+ return this as any;
+ }
+}
+
+export type GetArgType<T> = T extends Program<any, infer AT>
+ ? AT
+ : T extends CommandGroup<any, infer AT>
+ ? AT
+ : any;
+
+export function program<PN extends keyof any>(
+ argKey: PN,
+ args: ProgramArgs = {},
+): Program<PN, {}> {
+ return new Program(argKey as string, args);
+}
+
+export function prompt(question: string): Promise<string> {
+ const stdinReadline = readline.createInterface({
+ input: process.stdin,
+ output: process.stdout,
+ });
+ return new Promise<string>((resolve, reject) => {
+ stdinReadline.question(question, (res) => {
+ resolve(res);
+ stdinReadline.close();
+ });
+ });
+}
diff --git a/packages/taler-wallet-cli/src/index.ts b/packages/taler-wallet-cli/src/index.ts
new file mode 100644
index 000000000..c8e517e53
--- /dev/null
+++ b/packages/taler-wallet-cli/src/index.ts
@@ -0,0 +1,640 @@
+/*
+ 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/>
+ */
+
+import os from "os";
+import fs from "fs";
+import {
+ getDefaultNodeWallet,
+ Logger,
+ Amounts,
+ Wallet,
+ OperationFailedAndReportedError,
+ OperationFailedError,
+ time,
+ taleruri,
+ walletTypes,
+ talerCrypto,
+ payto,
+ codec,
+ testvectors,
+ walletCoreApi,
+ NodeHttpLib,
+} from "taler-wallet-core";
+import * as clk from "./clk";
+
+const logger = new Logger("taler-wallet-cli.ts");
+
+const defaultWalletDbPath = os.homedir + "/" + ".talerwalletdb.json";
+
+function assertUnreachable(x: never): never {
+ throw new Error("Didn't expect to get here");
+}
+
+async function doPay(
+ wallet: Wallet,
+ payUrl: string,
+ options: { alwaysYes: boolean } = { alwaysYes: true },
+): Promise<void> {
+ const result = await wallet.preparePayForUri(payUrl);
+ if (result.status === walletTypes.PreparePayResultType.InsufficientBalance) {
+ console.log("contract", result.contractTerms);
+ console.error("insufficient balance");
+ process.exit(1);
+ return;
+ }
+ if (result.status === walletTypes.PreparePayResultType.AlreadyConfirmed) {
+ if (result.paid) {
+ console.log("already paid!");
+ } else {
+ console.log("payment already in progress");
+ }
+
+ process.exit(0);
+ return;
+ }
+ if (result.status === "payment-possible") {
+ console.log("paying ...");
+ } else {
+ throw Error("not reached");
+ }
+ console.log("contract", result.contractTerms);
+ console.log("raw amount:", result.amountRaw);
+ console.log("effective amount:", result.amountEffective);
+ let pay;
+ if (options.alwaysYes) {
+ pay = true;
+ } else {
+ while (true) {
+ const yesNoResp = (await clk.prompt("Pay? [Y/n]")).toLowerCase();
+ if (yesNoResp === "" || yesNoResp === "y" || yesNoResp === "yes") {
+ pay = true;
+ break;
+ } else if (yesNoResp === "n" || yesNoResp === "no") {
+ pay = false;
+ break;
+ } else {
+ console.log("please answer y/n");
+ }
+ }
+ }
+
+ if (pay) {
+ await wallet.confirmPay(result.proposalId, undefined);
+ } else {
+ console.log("not paying");
+ }
+}
+
+function applyVerbose(verbose: boolean): void {
+ // TODO
+}
+
+function printVersion(): void {
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
+ const info = require("../../../package.json");
+ console.log(`${info.version}`);
+ process.exit(0);
+}
+
+const walletCli = clk
+ .program("wallet", {
+ help: "Command line interface for the GNU Taler wallet.",
+ })
+ .maybeOption("walletDbFile", ["--wallet-db"], clk.STRING, {
+ help: "location of the wallet database file",
+ })
+ .maybeOption("timetravel", ["--timetravel"], clk.INT, {
+ help: "modify system time by given offset in microseconds",
+ onPresentHandler: (x) => {
+ // Convert microseconds to milliseconds and do timetravel
+ logger.info(`timetravelling ${x} microseconds`);
+ time.setDangerousTimetravel(x / 1000);
+ },
+ })
+ .maybeOption("inhibit", ["--inhibit"], clk.STRING, {
+ help:
+ "Inhibit running certain operations, useful for debugging and testing.",
+ })
+ .flag("noThrottle", ["--no-throttle"], {
+ help: "Don't do any request throttling.",
+ })
+ .flag("version", ["-v", "--version"], {
+ onPresentHandler: printVersion,
+ })
+ .flag("verbose", ["-V", "--verbose"], {
+ help: "Enable verbose output.",
+ });
+
+type WalletCliArgsType = clk.GetArgType<typeof walletCli>;
+
+async function withWallet<T>(
+ walletCliArgs: WalletCliArgsType,
+ f: (w: Wallet) => Promise<T>,
+): Promise<T> {
+ const dbPath = walletCliArgs.wallet.walletDbFile ?? defaultWalletDbPath;
+ const myHttpLib = new NodeHttpLib();
+ if (walletCliArgs.wallet.noThrottle) {
+ myHttpLib.setThrottling(false);
+ }
+ const wallet = await getDefaultNodeWallet({
+ persistentStoragePath: dbPath,
+ httpLib: myHttpLib,
+ });
+ applyVerbose(walletCliArgs.wallet.verbose);
+ try {
+ await wallet.fillDefaults();
+ const ret = await f(wallet);
+ return ret;
+ } catch (e) {
+ if (
+ e instanceof OperationFailedAndReportedError ||
+ e instanceof OperationFailedError
+ ) {
+ console.error("Operation failed: " + e.message);
+ console.error(
+ "Error details:",
+ JSON.stringify(e.operationError, undefined, 2),
+ );
+ } else {
+ console.error("caught unhandled exception (bug?):", e);
+ }
+ process.exit(1);
+ } finally {
+ wallet.stop();
+ }
+}
+
+walletCli
+ .subcommand("balance", "balance", { help: "Show wallet balance." })
+ .flag("json", ["--json"], {
+ help: "Show raw JSON.",
+ })
+ .action(async (args) => {
+ await withWallet(args, async (wallet) => {
+ const balance = await wallet.getBalances();
+ console.log(JSON.stringify(balance, undefined, 2));
+ });
+ });
+
+walletCli
+ .subcommand("api", "api", { help: "Call the wallet-core API directly." })
+ .requiredArgument("operation", clk.STRING)
+ .requiredArgument("request", clk.STRING)
+ .action(async (args) => {
+ await withWallet(args, async (wallet) => {
+ let requestJson;
+ try {
+ requestJson = JSON.parse(args.api.request);
+ } catch (e) {
+ console.error("Invalid JSON");
+ process.exit(1);
+ }
+ const resp = await walletCoreApi.handleCoreApiRequest(
+ wallet,
+ args.api.operation,
+ "reqid-1",
+ requestJson,
+ );
+ console.log(JSON.stringify(resp, undefined, 2));
+ });
+ });
+
+walletCli
+ .subcommand("", "pending", { help: "Show pending operations." })
+ .action(async (args) => {
+ await withWallet(args, async (wallet) => {
+ const pending = await wallet.getPendingOperations();
+ console.log(JSON.stringify(pending, undefined, 2));
+ });
+ });
+
+walletCli
+ .subcommand("transactions", "transactions", { help: "Show transactions." })
+ .maybeOption("currency", ["--currency"], clk.STRING)
+ .maybeOption("search", ["--search"], clk.STRING)
+ .action(async (args) => {
+ await withWallet(args, async (wallet) => {
+ const pending = await wallet.getTransactions({
+ currency: args.transactions.currency,
+ search: args.transactions.search,
+ });
+ console.log(JSON.stringify(pending, undefined, 2));
+ });
+ });
+
+async function asyncSleep(milliSeconds: number): Promise<void> {
+ return new Promise<void>((resolve, reject) => {
+ setTimeout(() => resolve(), milliSeconds);
+ });
+}
+
+walletCli
+ .subcommand("runPendingOpt", "run-pending", {
+ help: "Run pending operations.",
+ })
+ .flag("forceNow", ["-f", "--force-now"])
+ .action(async (args) => {
+ await withWallet(args, async (wallet) => {
+ await wallet.runPending(args.runPendingOpt.forceNow);
+ });
+ });
+
+walletCli
+ .subcommand("finishPendingOpt", "run-until-done", {
+ help: "Run until no more work is left.",
+ })
+ .action(async (args) => {
+ await withWallet(args, async (wallet) => {
+ await wallet.runUntilDoneAndStop();
+ });
+ });
+
+walletCli
+ .subcommand("handleUri", "handle-uri", {
+ help: "Handle a taler:// URI.",
+ })
+ .requiredArgument("uri", clk.STRING)
+ .flag("autoYes", ["-y", "--yes"])
+ .action(async (args) => {
+ await withWallet(args, async (wallet) => {
+ const uri: string = args.handleUri.uri;
+ const uriType = taleruri.classifyTalerUri(uri);
+ switch (uriType) {
+ case taleruri.TalerUriType.TalerPay:
+ await doPay(wallet, uri, { alwaysYes: args.handleUri.autoYes });
+ break;
+ case taleruri.TalerUriType.TalerTip:
+ {
+ const res = await wallet.getTipStatus(uri);
+ console.log("tip status", res);
+ await wallet.acceptTip(res.tipId);
+ }
+ break;
+ case taleruri.TalerUriType.TalerRefund:
+ await wallet.applyRefund(uri);
+ break;
+ case taleruri.TalerUriType.TalerWithdraw:
+ {
+ const withdrawInfo = await wallet.getWithdrawalDetailsForUri(uri);
+ const selectedExchange = withdrawInfo.defaultExchangeBaseUrl;
+ if (!selectedExchange) {
+ console.error("no suggested exchange!");
+ process.exit(1);
+ return;
+ }
+ const res = await wallet.acceptWithdrawal(uri, selectedExchange);
+ await wallet.processReserve(res.reservePub);
+ }
+ break;
+ default:
+ console.log(`URI type (${uriType}) not handled`);
+ break;
+ }
+ return;
+ });
+ });
+
+const exchangesCli = walletCli.subcommand("exchangesCmd", "exchanges", {
+ help: "Manage exchanges.",
+});
+
+exchangesCli
+ .subcommand("exchangesListCmd", "list", {
+ help: "List known exchanges.",
+ })
+ .action(async (args) => {
+ console.log("Listing exchanges ...");
+ await withWallet(args, async (wallet) => {
+ const exchanges = await wallet.getExchanges();
+ console.log(JSON.stringify(exchanges, undefined, 2));
+ });
+ });
+
+exchangesCli
+ .subcommand("exchangesUpdateCmd", "update", {
+ help: "Update or add an exchange by base URL.",
+ })
+ .requiredArgument("url", clk.STRING, {
+ help: "Base URL of the exchange.",
+ })
+ .flag("force", ["-f", "--force"])
+ .action(async (args) => {
+ await withWallet(args, async (wallet) => {
+ await wallet.updateExchangeFromUrl(
+ args.exchangesUpdateCmd.url,
+ args.exchangesUpdateCmd.force,
+ );
+ });
+ });
+
+exchangesCli
+ .subcommand("exchangesAddCmd", "add", {
+ help: "Add an exchange by base URL.",
+ })
+ .requiredArgument("url", clk.STRING, {
+ help: "Base URL of the exchange.",
+ })
+ .action(async (args) => {
+ await withWallet(args, async (wallet) => {
+ await wallet.updateExchangeFromUrl(args.exchangesAddCmd.url);
+ });
+ });
+
+exchangesCli
+ .subcommand("exchangesAcceptTosCmd", "accept-tos", {
+ help: "Accept terms of service.",
+ })
+ .requiredArgument("url", clk.STRING, {
+ help: "Base URL of the exchange.",
+ })
+ .requiredArgument("etag", clk.STRING, {
+ help: "ToS version tag to accept",
+ })
+ .action(async (args) => {
+ await withWallet(args, async (wallet) => {
+ await wallet.acceptExchangeTermsOfService(
+ args.exchangesAcceptTosCmd.url,
+ args.exchangesAcceptTosCmd.etag,
+ );
+ });
+ });
+
+exchangesCli
+ .subcommand("exchangesTosCmd", "tos", {
+ help: "Show terms of service.",
+ })
+ .requiredArgument("url", clk.STRING, {
+ help: "Base URL of the exchange.",
+ })
+ .action(async (args) => {
+ await withWallet(args, async (wallet) => {
+ const tosResult = await wallet.getExchangeTos(args.exchangesTosCmd.url);
+ console.log(JSON.stringify(tosResult, undefined, 2));
+ });
+ });
+
+const advancedCli = walletCli.subcommand("advancedArgs", "advanced", {
+ help:
+ "Subcommands for advanced operations (only use if you know what you're doing!).",
+});
+
+advancedCli
+ .subcommand("manualWithdrawalDetails", "manual-withdrawal-details", {
+ help: "Query withdrawal fees.",
+ })
+ .requiredArgument("exchange", clk.STRING)
+ .requiredArgument("amount", clk.STRING)
+ .action(async (args) => {
+ await withWallet(args, async (wallet) => {
+ const details = await wallet.getWithdrawalDetailsForAmount(
+ args.manualWithdrawalDetails.exchange,
+ Amounts.parseOrThrow(args.manualWithdrawalDetails.amount),
+ );
+ console.log(JSON.stringify(details, undefined, 2));
+ });
+ });
+
+advancedCli
+ .subcommand("decode", "decode", {
+ help: "Decode base32-crockford.",
+ })
+ .action((args) => {
+ const enc = fs.readFileSync(0, "utf8");
+ fs.writeFileSync(1, talerCrypto.decodeCrock(enc.trim()));
+ });
+
+advancedCli
+ .subcommand("withdrawManually", "withdraw-manually", {
+ help: "Withdraw manually from an exchange.",
+ })
+ .requiredOption("exchange", ["--exchange"], clk.STRING, {
+ help: "Base URL of the exchange.",
+ })
+ .requiredOption("amount", ["--amount"], clk.STRING, {
+ help: "Amount to withdraw",
+ })
+ .action(async (args) => {
+ await withWallet(args, async (wallet) => {
+ const exchange = await wallet.updateExchangeFromUrl(
+ args.withdrawManually.exchange,
+ );
+ const acct = exchange.wireInfo?.accounts[0];
+ if (!acct) {
+ console.log("exchange has no accounts");
+ return;
+ }
+ const reserve = await wallet.acceptManualWithdrawal(
+ exchange.baseUrl,
+ Amounts.parseOrThrow(args.withdrawManually.amount),
+ );
+ const completePaytoUri = payto.addPaytoQueryParams(acct.payto_uri, {
+ amount: args.withdrawManually.amount,
+ message: `Taler top-up ${reserve.reservePub}`,
+ });
+ console.log("Created reserve", reserve.reservePub);
+ console.log("Payto URI", completePaytoUri);
+ });
+ });
+
+const reservesCli = advancedCli.subcommand("reserves", "reserves", {
+ help: "Manage reserves.",
+});
+
+reservesCli
+ .subcommand("list", "list", {
+ help: "List reserves.",
+ })
+ .action(async (args) => {
+ await withWallet(args, async (wallet) => {
+ const reserves = await wallet.getReserves();
+ console.log(JSON.stringify(reserves, undefined, 2));
+ });
+ });
+
+reservesCli
+ .subcommand("update", "update", {
+ help: "Update reserve status via exchange.",
+ })
+ .requiredArgument("reservePub", clk.STRING)
+ .action(async (args) => {
+ await withWallet(args, async (wallet) => {
+ await wallet.updateReserve(args.update.reservePub);
+ });
+ });
+
+advancedCli
+ .subcommand("payPrepare", "pay-prepare", {
+ help: "Claim an order but don't pay yet.",
+ })
+ .requiredArgument("url", clk.STRING)
+ .action(async (args) => {
+ await withWallet(args, async (wallet) => {
+ const res = await wallet.preparePayForUri(args.payPrepare.url);
+ switch (res.status) {
+ case walletTypes.PreparePayResultType.InsufficientBalance:
+ console.log("insufficient balance");
+ break;
+ case walletTypes.PreparePayResultType.AlreadyConfirmed:
+ if (res.paid) {
+ console.log("already paid!");
+ } else {
+ console.log("payment in progress");
+ }
+ break;
+ case walletTypes.PreparePayResultType.PaymentPossible:
+ console.log("payment possible");
+ break;
+ default:
+ assertUnreachable(res);
+ }
+ });
+ });
+
+advancedCli
+ .subcommand("payConfirm", "pay-confirm", {
+ help: "Confirm payment proposed by a merchant.",
+ })
+ .requiredArgument("proposalId", clk.STRING)
+ .maybeOption("sessionIdOverride", ["--session-id"], clk.STRING)
+ .action(async (args) => {
+ await withWallet(args, async (wallet) => {
+ wallet.confirmPay(
+ args.payConfirm.proposalId,
+ args.payConfirm.sessionIdOverride,
+ );
+ });
+ });
+
+advancedCli
+ .subcommand("refresh", "force-refresh", {
+ help: "Force a refresh on a coin.",
+ })
+ .requiredArgument("coinPub", clk.STRING)
+ .action(async (args) => {
+ await withWallet(args, async (wallet) => {
+ await wallet.refresh(args.refresh.coinPub);
+ });
+ });
+
+advancedCli
+ .subcommand("dumpCoins", "dump-coins", {
+ help: "Dump coins in an easy-to-process format.",
+ })
+ .action(async (args) => {
+ await withWallet(args, async (wallet) => {
+ const coinDump = await wallet.dumpCoins();
+ console.log(JSON.stringify(coinDump, undefined, 2));
+ });
+ });
+
+const coinPubListCodec = codec.makeCodecForList(codec.codecForString);
+
+advancedCli
+ .subcommand("suspendCoins", "suspend-coins", {
+ help: "Mark a coin as suspended, will not be used for payments.",
+ })
+ .requiredArgument("coinPubSpec", clk.STRING)
+ .action(async (args) => {
+ await withWallet(args, async (wallet) => {
+ let coinPubList: string[];
+ try {
+ coinPubList = coinPubListCodec.decode(
+ JSON.parse(args.suspendCoins.coinPubSpec),
+ );
+ } catch (e) {
+ console.log("could not parse coin list:", e.message);
+ process.exit(1);
+ }
+ for (const c of coinPubList) {
+ await wallet.setCoinSuspended(c, true);
+ }
+ });
+ });
+
+advancedCli
+ .subcommand("unsuspendCoins", "unsuspend-coins", {
+ help: "Mark a coin as suspended, will not be used for payments.",
+ })
+ .requiredArgument("coinPubSpec", clk.STRING)
+ .action(async (args) => {
+ await withWallet(args, async (wallet) => {
+ let coinPubList: string[];
+ try {
+ coinPubList = coinPubListCodec.decode(
+ JSON.parse(args.unsuspendCoins.coinPubSpec),
+ );
+ } catch (e) {
+ console.log("could not parse coin list:", e.message);
+ process.exit(1);
+ }
+ for (const c of coinPubList) {
+ await wallet.setCoinSuspended(c, false);
+ }
+ });
+ });
+
+advancedCli
+ .subcommand("coins", "list-coins", {
+ help: "List coins.",
+ })
+ .action(async (args) => {
+ await withWallet(args, async (wallet) => {
+ const coins = await wallet.getCoins();
+ for (const coin of coins) {
+ console.log(`coin ${coin.coinPub}`);
+ console.log(` status ${coin.status}`);
+ console.log(` exchange ${coin.exchangeBaseUrl}`);
+ console.log(` denomPubHash ${coin.denomPubHash}`);
+ console.log(
+ ` remaining amount ${Amounts.stringify(coin.currentAmount)}`,
+ );
+ }
+ });
+ });
+
+advancedCli
+ .subcommand("updateReserve", "update-reserve", {
+ help: "Update reserve status.",
+ })
+ .requiredArgument("reservePub", clk.STRING)
+ .action(async (args) => {
+ await withWallet(args, async (wallet) => {
+ const r = await wallet.updateReserve(args.updateReserve.reservePub);
+ console.log("updated reserve:", JSON.stringify(r, undefined, 2));
+ });
+ });
+
+advancedCli
+ .subcommand("updateReserve", "show-reserve", {
+ help: "Show the current reserve status.",
+ })
+ .requiredArgument("reservePub", clk.STRING)
+ .action(async (args) => {
+ await withWallet(args, async (wallet) => {
+ const r = await wallet.getReserve(args.updateReserve.reservePub);
+ console.log("updated reserve:", JSON.stringify(r, undefined, 2));
+ });
+ });
+
+const testCli = walletCli.subcommand("testingArgs", "testing", {
+ help: "Subcommands for testing GNU Taler deployments.",
+});
+
+testCli.subcommand("vectors", "vectors").action(async (args) => {
+ testvectors.printTestVectors();
+});
+
+walletCli.run();
diff --git a/packages/taler-wallet-cli/tsconfig.json b/packages/taler-wallet-cli/tsconfig.json
new file mode 100644
index 000000000..34767d1e0
--- /dev/null
+++ b/packages/taler-wallet-cli/tsconfig.json
@@ -0,0 +1,30 @@
+{
+ "compileOnSave": true,
+ "compilerOptions": {
+ "composite": true,
+ "target": "ES6",
+ "module": "ESNext",
+ "moduleResolution": "node",
+ "sourceMap": true,
+ "lib": ["es6"],
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "strict": true,
+ "strictPropertyInitialization": false,
+ "outDir": "lib",
+ "noImplicitAny": true,
+ "noImplicitThis": true,
+ "incremental": true,
+ "esModuleInterop": true,
+ "importHelpers": true,
+ "rootDir": "src",
+ "baseUrl": "./src",
+ "types": ["node"]
+ },
+ "include": ["src/**/*"],
+ "references": [
+ {
+ "path": "../taler-wallet-core/"
+ }
+ ]
+}
diff --git a/packages/taler-wallet-core/.gitignore b/packages/taler-wallet-core/.gitignore
new file mode 100644
index 000000000..502167fa0
--- /dev/null
+++ b/packages/taler-wallet-core/.gitignore
@@ -0,0 +1 @@
+/lib
diff --git a/packages/taler-wallet-core/package.json b/packages/taler-wallet-core/package.json
new file mode 100644
index 000000000..20240bab4
--- /dev/null
+++ b/packages/taler-wallet-core/package.json
@@ -0,0 +1,81 @@
+{
+ "name": "taler-wallet-core",
+ "version": "0.6.12",
+ "description": "",
+ "engines": {
+ "node": ">=0.12.0"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git://git.taler.net/wallet-core.git"
+ },
+ "author": "Florian Dold",
+ "license": "GPL-3.0",
+ "scripts": {
+ "compile": "tsc && rollup -c",
+ "pretty": "prettier --config ../../.prettierrc --write src",
+ "test": "tsc && ava",
+ "coverage": "tsc && nyc ava",
+ "clean": "rimraf dist lib"
+ },
+ "files": [
+ "AUTHORS",
+ "README",
+ "COPYING",
+ "bin/",
+ "dist/node",
+ "src/"
+ ],
+ "main": "./dist/taler-wallet-core.js",
+ "module": "./lib/index.js",
+ "types": "./lib/index.d.ts",
+ "devDependencies": {
+ "@ava/typescript": "^1.1.1",
+ "@typescript-eslint/eslint-plugin": "^3.6.1",
+ "@typescript-eslint/parser": "^3.6.1",
+ "ava": "^3.10.1",
+ "eslint": "^7.4.0",
+ "eslint-config-airbnb-typescript": "^8.0.2",
+ "eslint-plugin-import": "^2.22.0",
+ "eslint-plugin-jsx-a11y": "^6.3.1",
+ "eslint-plugin-react": "^7.20.3",
+ "eslint-plugin-react-hooks": "^4.0.8",
+ "jed": "^1.1.1",
+ "moment": "^2.27.0",
+ "nyc": "^15.1.0",
+ "po2json": "^0.4.5",
+ "pogen": "workspace:*",
+ "prettier": "^2.0.5",
+ "source-map-resolve": "^0.6.0",
+ "structured-clone": "^0.2.2",
+ "typedoc": "^0.17.8",
+ "typescript": "^3.9.7",
+ "rollup": "^2.23.0",
+ "esm": "^3.2.25",
+ "rimraf": "^3.0.2"
+ },
+ "dependencies": {
+ "@types/node": "^14.0.27",
+ "axios": "^0.19.2",
+ "big-integer": "^1.6.48",
+ "idb-bridge": "workspace:*",
+ "source-map-support": "^0.5.19",
+ "tslib": "^2.0.0"
+ },
+ "ava": {
+ "require": ["esm"],
+ "files": [
+ "src/**/*-test.*"
+ ],
+ "typescript": {
+ "extensions": [
+ "js",
+ "ts",
+ "tsx"
+ ],
+ "rewritePaths": {
+ "src/": "lib/"
+ }
+ }
+ }
+}
diff --git a/packages/taler-wallet-core/rollup.config.js b/packages/taler-wallet-core/rollup.config.js
new file mode 100644
index 000000000..2f0a86b2a
--- /dev/null
+++ b/packages/taler-wallet-core/rollup.config.js
@@ -0,0 +1,31 @@
+// rollup.config.js
+import commonjs from "@rollup/plugin-commonjs";
+import nodeResolve from "@rollup/plugin-node-resolve";
+import json from "@rollup/plugin-json";
+import builtins from "builtin-modules";
+import pkg from "./package.json";
+
+export default {
+ input: "lib/index.js",
+ output: {
+ file: pkg.main,
+ format: "cjs",
+ sourcemap: false,
+ },
+ external: builtins,
+ plugins: [
+ nodeResolve({
+ preferBuiltins: true,
+ }),
+
+ commonjs({
+ include: [/node_modules/, /dist/],
+ extensions: [".js"],
+ ignoreGlobal: false,
+ sourceMap: false,
+ }),
+
+ json(),
+ ],
+}
+
diff --git a/packages/taler-wallet-core/src/TalerErrorCode.d.ts.map b/packages/taler-wallet-core/src/TalerErrorCode.d.ts.map
new file mode 100644
index 000000000..87926875d
--- /dev/null
+++ b/packages/taler-wallet-core/src/TalerErrorCode.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"TalerErrorCode.d.ts","sourceRoot":"","sources":["TalerErrorCode.ts"],"names":[],"mappings":"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"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/TalerErrorCode.ts b/packages/taler-wallet-core/src/TalerErrorCode.ts
new file mode 100644
index 000000000..d45b1064f
--- /dev/null
+++ b/packages/taler-wallet-core/src/TalerErrorCode.ts
@@ -0,0 +1,3097 @@
+/*
+ This file is part of GNU Taler
+ Copyright (C) 2012-2020 Taler Systems SA
+
+ GNU Taler is free software: you can redistribute it and/or modify it
+ under the terms of the GNU Lesser General Public License as published
+ by the Free Software Foundation, either version 3 of the License,
+ or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but
+ WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+ SPDX-License-Identifier: LGPL3.0-or-later
+
+ Note: the LGPL does not apply to all components of GNU Taler,
+ but it does apply to this file.
+ */
+
+export enum TalerErrorCode {
+ /**
+ * Special code to indicate no error (or no "code" present).
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ NONE = 0,
+
+ /**
+ * Special code to indicate that a non-integer error code was returned in the JSON response.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ INVALID = 1,
+
+ /**
+ * The response we got from the server was not even in JSON format.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ INVALID_RESPONSE = 2,
+
+ /**
+ * Generic implementation error: this function was not yet implemented.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ NOT_IMPLEMENTED = 3,
+
+ /**
+ * Exchange is badly configured and thus cannot operate.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ EXCHANGE_BAD_CONFIGURATION = 4,
+
+ /**
+ * Internal assertion error.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ INTERNAL_INVARIANT_FAILURE = 5,
+
+ /**
+ * Operation timed out.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TIMEOUT = 6,
+
+ /**
+ * Exchange failed to allocate memory for building JSON reply.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ JSON_ALLOCATION_FAILURE = 7,
+
+ /**
+ * HTTP method invalid for this URL.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ METHOD_INVALID = 8,
+
+ /**
+ * Operation specified invalid for this URL (resulting in a "NOT FOUND" for the overall response).
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ OPERATION_INVALID = 9,
+
+ /**
+ * There is no endpoint defined for the URL provided by the client.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ENDPOINT_UNKNOWN = 10,
+
+ /**
+ * The URI is longer than the longest URI the HTTP server is willing to parse.
+ * Returned with an HTTP status code of #MHD_HTTP_URI_TOO_LONG (414).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ URI_TOO_LONG = 11,
+
+ /**
+ * The number of segments included in the URI does not match the number of segments expected by the endpoint.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WRONG_NUMBER_OF_SEGMENTS = 12,
+
+ /**
+ * The start and end-times in the wire fee structure leave a hole. This is not allowed. Generated as an error on the client-side.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ HOLE_IN_WIRE_FEE_STRUCTURE = 13,
+
+ /**
+ * The version string given does not follow the expected CURRENT:REVISION:AGE Format. Generated as an error on the client side.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ VERSION_MALFORMED = 14,
+
+ /**
+ * The client-side experienced an internal failure. Generated as an error on the client side.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ CLIENT_INTERNAL_FAILURE = 15,
+
+ /**
+ * The body is too large to be permissible for the endpoint.
+ * Returned with an HTTP status code of #MHD_HTTP_PAYLOAD_TOO_LARGE (413).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ UPLOAD_EXCEEDS_LIMIT = 16,
+
+ /**
+ * The payto:// URI we got is malformed.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PAYTO_MALFORMED = 17,
+
+ /**
+ * The exchange failed to even just initialize its connection to the database.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DB_SETUP_FAILED = 1001,
+
+ /**
+ * The exchange encountered an error event to just start the database transaction.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DB_START_FAILED = 1002,
+
+ /**
+ * The exchange encountered an error event to commit the database transaction (hard, unrecoverable error).
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DB_COMMIT_FAILED_HARD = 1003,
+
+ /**
+ * The exchange encountered an error event to commit the database transaction, even after repeatedly retrying it there was always a conflicting transaction. (This indicates a repeated serialization error; should only happen if some client maliciously tries to create conflicting concurrent transactions.)
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DB_COMMIT_FAILED_ON_RETRY = 1004,
+
+ /**
+ * The exchange had insufficient memory to parse the request.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PARSER_OUT_OF_MEMORY = 1005,
+
+ /**
+ * The JSON in the client's request to the exchange was malformed. (Generic parse error).
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ JSON_INVALID = 1006,
+
+ /**
+ * The JSON in the client's request to the exchange was malformed. Details about the location of the parse error are provided.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ JSON_INVALID_WITH_DETAILS = 1007,
+
+ /**
+ * A required parameter in the request to the exchange was missing.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PARAMETER_MISSING = 1008,
+
+ /**
+ * A parameter in the request to the exchange was malformed.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PARAMETER_MALFORMED = 1009,
+
+ /**
+ * The exchange failed to obtain the transaction history of the given coin from the database while generating an insufficient funds errors. This can happen during /deposit or /recoup requests.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ COIN_HISTORY_DB_ERROR_INSUFFICIENT_FUNDS = 1010,
+
+ /**
+ * Internal logic error. Some server-side function failed that really should not.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ INTERNAL_LOGIC_ERROR = 1011,
+
+ /**
+ * The method specified in a payto:// URI is not one we expected.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PAYTO_WRONG_METHOD = 1012,
+
+ /**
+ * The same coin was already used with a different denomination previously.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ COIN_CONFLICTING_DENOMINATION_KEY = 1013,
+
+ /**
+ * We failed to update the database of known coins.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DB_COIN_HISTORY_STORE_ERROR = 1014,
+
+ /**
+ * The public key of given to a /coins/ handler was malformed.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ COINS_INVALID_COIN_PUB = 1050,
+
+ /**
+ * The reserve key of given to a /reserves/ handler was malformed.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ RESERVES_INVALID_RESERVE_PUB = 1051,
+
+ /**
+ * The public key of given to a /transfers/ handler was malformed.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TRANSFERS_INVALID_WTID = 1052,
+
+ /**
+ * The wire hash of given to a /deposits/ handler was malformed.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DEPOSITS_INVALID_H_WIRE = 1053,
+
+ /**
+ * The merchant key of given to a /deposits/ handler was malformed.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DEPOSITS_INVALID_MERCHANT_PUB = 1054,
+
+ /**
+ * The hash of the contract terms given to a /deposits/ handler was malformed.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DEPOSITS_INVALID_H_CONTRACT_TERMS = 1055,
+
+ /**
+ * The coin public key of given to a /deposits/ handler was malformed.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DEPOSITS_INVALID_COIN_PUB = 1056,
+
+ /**
+ * The body returned by the exchange for a /deposits/ request was malformed. Error created client-side.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DEPOSITS_INVALID_BODY_BY_EXCHANGE = 1057,
+
+ /**
+ * The signature returned by the exchange in a /deposits/ request was malformed. Error created client-side.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DEPOSITS_INVALID_SIGNATURE_BY_EXCHANGE = 1058,
+
+ /**
+ * The given reserve does not have sufficient funds to admit the requested withdraw operation at this time. The response includes the current "balance" of the reserve as well as the transaction "history" that lead to this balance.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WITHDRAW_INSUFFICIENT_FUNDS = 1100,
+
+ /**
+ * The exchange has no information about the "reserve_pub" that was given.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WITHDRAW_RESERVE_UNKNOWN = 1101,
+
+ /**
+ * The amount to withdraw together with the fee exceeds the numeric range for Taler amounts. This is not a client failure, as the coin value and fees come from the exchange's configuration.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WITHDRAW_AMOUNT_FEE_OVERFLOW = 1102,
+
+ /**
+ * All of the deposited amounts into this reserve total up to a value that is too big for the numeric range for Taler amounts. This is not a client failure, as the transaction history comes from the exchange's configuration.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ AMOUNT_DEPOSITS_OVERFLOW = 1103,
+
+ /**
+ * For one of the historic withdrawals from this reserve, the exchange could not find the denomination key. This is not a client failure, as the transaction history comes from the exchange's configuration.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WITHDRAW_HISTORIC_DENOMINATION_KEY_NOT_FOUND = 1104,
+
+ /**
+ * All of the withdrawals from reserve total up to a value that is too big for the numeric range for Taler amounts. This is not a client failure, as the transaction history comes from the exchange's configuration.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WITHDRAW_AMOUNT_WITHDRAWALS_OVERFLOW = 1105,
+
+ /**
+ * The exchange somehow knows about this reserve, but there seem to have been no wire transfers made. This is not a client failure, as this is a database consistency issue of the exchange.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WITHDRAW_RESERVE_WITHOUT_WIRE_TRANSFER = 1106,
+
+ /**
+ * The exchange failed to create the signature using the denomination key.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WITHDRAW_SIGNATURE_FAILED = 1107,
+
+ /**
+ * The exchange failed to store the withdraw operation in its database.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WITHDRAW_DB_STORE_ERROR = 1108,
+
+ /**
+ * The exchange failed to check against historic withdraw data from database (as part of ensuring the idempotency of the operation).
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WITHDRAW_DB_FETCH_ERROR = 1109,
+
+ /**
+ * The exchange is not aware of the denomination key the wallet requested for the withdrawal.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WITHDRAW_DENOMINATION_KEY_NOT_FOUND = 1110,
+
+ /**
+ * The signature of the reserve is not valid.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WITHDRAW_RESERVE_SIGNATURE_INVALID = 1111,
+
+ /**
+ * When computing the reserve history, we ended up with a negative overall balance, which should be impossible.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WITHDRAW_HISTORY_DB_ERROR_INSUFFICIENT_FUNDS = 1112,
+
+ /**
+ * When computing the reserve history, we ended up with a negative overall balance, which should be impossible.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WITHDRAW_RESERVE_HISTORY_IMPOSSIBLE = 1113,
+
+ /**
+ * Validity period of the coin to be withdrawn is in the future.
+ * Returned with an HTTP status code of #MHD_HTTP_PRECONDITION_FAILED (412).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WITHDRAW_VALIDITY_IN_FUTURE = 1114,
+
+ /**
+ * Withdraw period of the coin to be withdrawn is in the past.
+ * Returned with an HTTP status code of #MHD_HTTP_GONE (410).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WITHDRAW_VALIDITY_IN_PAST = 1115,
+
+ /**
+ * Withdraw period of the coin to be withdrawn is in the past.
+ * Returned with an HTTP status code of #MHD_HTTP_GONE (410).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DENOMINATION_KEY_LOST = 1116,
+
+ /**
+ * The exchange's database entry with the reserve balance summary is inconsistent with its own history of the reserve.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WITHDRAW_RESERVE_BALANCE_CORRUPT = 1117,
+
+ /**
+ * The exchange responded with a reply that did not satsify the protocol. This error is not used in the protocol but created client-side.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WITHDRAW_REPLY_MALFORMED = 1118,
+
+ /**
+ * The client failed to unblind the blind signature. This error is not used in the protocol but created client-side.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WITHDRAW_UNBLIND_FAILURE = 1119,
+
+ /**
+ * The exchange failed to obtain the transaction history of the given reserve from the database.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ RESERVE_STATUS_DB_ERROR = 1150,
+
+ /**
+ * The reserve status was requested using a unknown key, to be returned with 404 Not Found.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ RESERVE_STATUS_UNKNOWN = 1151,
+
+ /**
+ * The exchange responded with a reply that did not satsify the protocol. This error is not used in the protocol but created client-side.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ RESERVE_STATUS_REPLY_MALFORMED = 1152,
+
+ /**
+ * The respective coin did not have sufficient residual value for the /deposit operation (i.e. due to double spending). The "history" in the response provides the transaction history of the coin proving this fact.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DEPOSIT_INSUFFICIENT_FUNDS = 1200,
+
+ /**
+ * The exchange failed to obtain the transaction history of the given coin from the database (this does not happen merely because the coin is seen by the exchange for the first time).
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DEPOSIT_HISTORY_DB_ERROR = 1201,
+
+ /**
+ * The exchange failed to store the /depost information in the database.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DEPOSIT_STORE_DB_ERROR = 1202,
+
+ /**
+ * The exchange database is unaware of the denomination key that signed the coin (however, the exchange process is; this is not supposed to happen; it can happen if someone decides to purge the DB behind the back of the exchange process). Hence the deposit is being refused.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DEPOSIT_DB_DENOMINATION_KEY_UNKNOWN = 1203,
+
+ /**
+ * The exchange was trying to lookup the denomination key for the purpose of a DEPOSIT operation. However, the denomination key is unavailable for that purpose. This can be because it is entirely unknown to the exchange or not in the validity period for the deposit operation. Hence the deposit is being refused.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DEPOSIT_DENOMINATION_KEY_UNKNOWN = 1204,
+
+ /**
+ * The signature made by the coin over the deposit permission is not valid.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DEPOSIT_COIN_SIGNATURE_INVALID = 1205,
+
+ /**
+ * The signature of the denomination key over the coin is not valid.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DEPOSIT_DENOMINATION_SIGNATURE_INVALID = 1206,
+
+ /**
+ * The stated value of the coin after the deposit fee is subtracted would be negative.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DEPOSIT_NEGATIVE_VALUE_AFTER_FEE = 1207,
+
+ /**
+ * The stated refund deadline is after the wire deadline.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DEPOSIT_REFUND_DEADLINE_AFTER_WIRE_DEADLINE = 1208,
+
+ /**
+ * The exchange does not recognize the validity of or support the given wire format type.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DEPOSIT_INVALID_WIRE_FORMAT_TYPE = 1209,
+
+ /**
+ * The exchange failed to canonicalize and hash the given wire format. For example, the merchant failed to provide the "salt" or a valid payto:// URI in the wire details. Note that while the exchange will do some basic sanity checking on the wire details, it cannot warrant that the banking system will ultimately be able to route to the specified address, even if this check passed.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DEPOSIT_INVALID_WIRE_FORMAT_JSON = 1210,
+
+ /**
+ * The hash of the given wire address does not match the wire hash specified in the proposal data.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DEPOSIT_INVALID_WIRE_FORMAT_CONTRACT_HASH_CONFLICT = 1211,
+
+ /**
+ * The exchange detected that the given account number is invalid for the selected wire format type.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DEPOSIT_INVALID_WIRE_FORMAT_ACCOUNT_NUMBER = 1213,
+
+ /**
+ * Timestamp included in deposit permission is intolerably far off with respect to the clock of the exchange.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DEPOSIT_INVALID_TIMESTAMP = 1218,
+
+ /**
+ * Validity period of the denomination key is in the future.
+ * Returned with an HTTP status code of #MHD_HTTP_PRECONDITION_FAILED (412).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DEPOSIT_DENOMINATION_VALIDITY_IN_FUTURE = 1219,
+
+ /**
+ * Denomination key of the coin is past the deposit deadline.
+ * Returned with an HTTP status code of #MHD_HTTP_GONE (410).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DEPOSIT_DENOMINATION_EXPIRED = 1220,
+
+ /**
+ * The signature provided by the exchange is not valid. Error created client-side.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DEPOSIT_INVALID_SIGNATURE_BY_EXCHANGE = 1221,
+
+ /**
+ * The currency specified for the deposit is different from the currency of the coin.
+ * Returned with an HTTP status code of #MHD_HTTP_PRECONDITION_FAILED (412).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DEPOSIT_CURRENCY_MISMATCH = 1222,
+
+ /**
+ * The respective coin did not have sufficient residual value for the /refresh/melt operation. The "history" in this response provdes the "residual_value" of the coin, which may be less than its "original_value".
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MELT_INSUFFICIENT_FUNDS = 1300,
+
+ /**
+ * The respective coin did not have sufficient residual value for the /refresh/melt operation. The "history" in this response provdes the "residual_value" of the coin, which may be less than its "original_value".
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MELT_DENOMINATION_KEY_NOT_FOUND = 1301,
+
+ /**
+ * The exchange had an internal error reconstructing the transaction history of the coin that was being melted.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MELT_COIN_HISTORY_COMPUTATION_FAILED = 1302,
+
+ /**
+ * The exchange failed to check against historic melt data from database (as part of ensuring the idempotency of the operation).
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MELT_DB_FETCH_ERROR = 1303,
+
+ /**
+ * The exchange failed to store session data in the database.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MELT_DB_STORE_SESSION_ERROR = 1304,
+
+ /**
+ * The exchange encountered melt fees exceeding the melted coin's contribution.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MELT_FEES_EXCEED_CONTRIBUTION = 1305,
+
+ /**
+ * The denomination key signature on the melted coin is invalid.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MELT_DENOMINATION_SIGNATURE_INVALID = 1306,
+
+ /**
+ * The signature made with the coin to be melted is invalid.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MELT_COIN_SIGNATURE_INVALID = 1307,
+
+ /**
+ * The exchange failed to obtain the transaction history of the given coin from the database while generating an insufficient funds errors.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MELT_HISTORY_DB_ERROR_INSUFFICIENT_FUNDS = 1308,
+
+ /**
+ * The denomination of the given coin has past its expiration date and it is also not a valid zombie (that is, was not refreshed with the fresh coin being subjected to recoup).
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MELT_COIN_EXPIRED_NO_ZOMBIE = 1309,
+
+ /**
+ * The signature returned by the exchange in a melt request was malformed. Error created client-side.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MELT_INVALID_SIGNATURE_BY_EXCHANGE = 1310,
+
+ /**
+ * The currency specified for the melt amount is different from the currency of the coin.
+ * Returned with an HTTP status code of #MHD_HTTP_PRECONDITION_FAILED (412).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MELT_CURRENCY_MISMATCH = 1311,
+
+ /**
+ * The exchange is unaware of the denomination key that was used to sign the melted zombie coin.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ REFRESH_RECOUP_DENOMINATION_KEY_NOT_FOUND = 1351,
+
+ /**
+ * Validity period of the denomination key is in the future.
+ * Returned with an HTTP status code of #MHD_HTTP_PRECONDITION_FAILED (412).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ REFRESH_RECOUP_DENOMINATION_VALIDITY_IN_FUTURE = 1352,
+
+ /**
+ * Denomination key of the coin is past the deposit deadline.
+ * Returned with an HTTP status code of #MHD_HTTP_GONE (410).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ REFRESH_RECOUP_DENOMINATION_EXPIRED = 1353,
+
+ /**
+ * Denomination key of the coin is past the deposit deadline.
+ * Returned with an HTTP status code of #MHD_HTTP_GONE (410).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ REFRESH_ZOMBIE_DENOMINATION_EXPIRED = 1354,
+
+ /**
+ * The provided transfer keys do not match up with the original commitment. Information about the original commitment is included in the response.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ REVEAL_COMMITMENT_VIOLATION = 1370,
+
+ /**
+ * Failed to produce the blinded signatures over the coins to be returned.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ REVEAL_SIGNING_ERROR = 1371,
+
+ /**
+ * The exchange is unaware of the refresh session specified in the request.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ REVEAL_SESSION_UNKNOWN = 1372,
+
+ /**
+ * The exchange failed to retrieve valid session data from the database.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ REVEAL_DB_FETCH_SESSION_ERROR = 1373,
+
+ /**
+ * The exchange failed to retrieve previously revealed data from the database.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ REVEAL_DB_FETCH_REVEAL_ERROR = 1374,
+
+ /**
+ * The exchange failed to retrieve commitment data from the database.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ REVEAL_DB_COMMIT_ERROR = 1375,
+
+ /**
+ * The size of the cut-and-choose dimension of the private transfer keys request does not match #TALER_CNC_KAPPA - 1.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ REVEAL_CNC_TRANSFER_ARRAY_SIZE_INVALID = 1376,
+
+ /**
+ * The number of coins to be created in refresh exceeds the limits of the exchange. private transfer keys request does not match #TALER_CNC_KAPPA - 1.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ REVEAL_NEW_DENOMS_ARRAY_SIZE_EXCESSIVE = 1377,
+
+ /**
+ * The number of envelopes given does not match the number of denomination keys given.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ REVEAL_NEW_DENOMS_ARRAY_SIZE_MISMATCH = 1378,
+
+ /**
+ * The exchange encountered a numeric overflow totaling up the cost for the refresh operation.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ REVEAL_COST_CALCULATION_OVERFLOW = 1379,
+
+ /**
+ * The exchange's cost calculation shows that the melt amount is below the costs of the transaction.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ REVEAL_AMOUNT_INSUFFICIENT = 1380,
+
+ /**
+ * The exchange is unaware of the denomination key that was requested for one of the fresh coins.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ REVEAL_FRESH_DENOMINATION_KEY_NOT_FOUND = 1381,
+
+ /**
+ * The signature made with the coin over the link data is invalid.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ REVEAL_LINK_SIGNATURE_INVALID = 1382,
+
+ /**
+ * The exchange failed to generate the signature as it could not find the signing key for the denomination.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ REVEAL_KEYS_MISSING = 1383,
+
+ /**
+ * The refresh session hash given to a /refreshes/ handler was malformed.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ REVEAL_INVALID_RCH = 1384,
+
+ /**
+ * The exchange responded with a reply that did not satsify the protocol. This error is not used in the protocol but created client-side.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ REVEAL_REPLY_MALFORMED = 1385,
+
+ /**
+ * The coin specified in the link request is unknown to the exchange.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ LINK_COIN_UNKNOWN = 1400,
+
+ /**
+ * The exchange responded with a reply that did not satsify the protocol. This error is not used in the protocol but created client-side.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ LINK_REPLY_MALFORMED = 1401,
+
+ /**
+ * The exchange knows literally nothing about the coin we were asked to refund. But without a transaction history, we cannot issue a refund. This is kind-of OK, the owner should just refresh it directly without executing the refund.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ REFUND_COIN_NOT_FOUND = 1500,
+
+ /**
+ * We could not process the refund request as the coin's transaction history does not permit the requested refund at this time. The "history" in the response proves this.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ REFUND_CONFLICT = 1501,
+
+ /**
+ * The exchange knows about the coin we were asked to refund, but not about the specific /deposit operation. Hence, we cannot issue a refund (as we do not know if this merchant public key is authorized to do a refund).
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ REFUND_DEPOSIT_NOT_FOUND = 1503,
+
+ /**
+ * The currency specified for the refund is different from the currency of the coin.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ REFUND_CURRENCY_MISMATCH = 1504,
+
+ /**
+ * When we tried to check if we already paid out the coin, the exchange's database suddenly disagreed with data it previously provided (internal inconsistency).
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ REFUND_DB_INCONSISTENT = 1505,
+
+ /**
+ * The exchange can no longer refund the customer/coin as the money was already transferred (paid out) to the merchant. (It should be past the refund deadline.)
+ * Returned with an HTTP status code of #MHD_HTTP_GONE (410).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ REFUND_MERCHANT_ALREADY_PAID = 1506,
+
+ /**
+ * The amount the exchange was asked to refund exceeds (with fees) the total amount of the deposit (including fees).
+ * Returned with an HTTP status code of #MHD_HTTP_PRECONDITION_FAILED (412).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ REFUND_INSUFFICIENT_FUNDS = 1507,
+
+ /**
+ * The exchange failed to recover information about the denomination key of the refunded coin (even though it recognizes the key). Hence it could not check the fee structure.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ REFUND_DENOMINATION_KEY_NOT_FOUND = 1508,
+
+ /**
+ * The refund fee specified for the request is lower than the refund fee charged by the exchange for the given denomination key of the refunded coin.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ REFUND_FEE_TOO_LOW = 1509,
+
+ /**
+ * The exchange failed to store the refund information to its database.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ REFUND_STORE_DB_ERROR = 1510,
+
+ /**
+ * The refund fee is specified in a different currency than the refund amount.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ REFUND_FEE_CURRENCY_MISMATCH = 1511,
+
+ /**
+ * The refunded amount is smaller than the refund fee, which would result in a negative refund.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ REFUND_FEE_ABOVE_AMOUNT = 1512,
+
+ /**
+ * The signature of the merchant is invalid.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ REFUND_MERCHANT_SIGNATURE_INVALID = 1513,
+
+ /**
+ * Merchant backend failed to create the refund confirmation signature.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ REFUND_MERCHANT_SIGNING_FAILED = 1514,
+
+ /**
+ * The signature returned by the exchange in a refund request was malformed. Error created client-side.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ REFUND_INVALID_SIGNATURE_BY_EXCHANGE = 1515,
+
+ /**
+ * The wire format specified in the "sender_account_details" is not understood or not supported by this exchange.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ADMIN_ADD_INCOMING_WIREFORMAT_UNSUPPORTED = 1600,
+
+ /**
+ * The currency specified in the "amount" parameter is not supported by this exhange.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ADMIN_ADD_INCOMING_CURRENCY_UNSUPPORTED = 1601,
+
+ /**
+ * The exchange failed to store information about the incoming transfer in its database.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ADMIN_ADD_INCOMING_DB_STORE = 1602,
+
+ /**
+ * The exchange encountered an error (that is not about not finding the wire transfer) trying to lookup a wire transfer identifier in the database.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TRANSFERS_GET_DB_FETCH_FAILED = 1700,
+
+ /**
+ * The exchange found internally inconsistent data when resolving a wire transfer identifier in the database.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TRANSFERS_GET_DB_INCONSISTENT = 1701,
+
+ /**
+ * The exchange did not find information about the specified wire transfer identifier in the database.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TRANSFERS_GET_WTID_NOT_FOUND = 1702,
+
+ /**
+ * The exchange did not find information about the wire transfer fees it charged.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TRANSFERS_GET_WIRE_FEE_NOT_FOUND = 1703,
+
+ /**
+ * The exchange found a wire fee that was above the total transfer value (and thus could not have been charged).
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TRANSFERS_GET_WIRE_FEE_INCONSISTENT = 1704,
+
+ /**
+ * The exchange responded with a reply that did not satsify the protocol. This error is not used in the protocol but created client-side.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TRANSFERS_GET_REPLY_MALFORMED = 1705,
+
+ /**
+ * The exchange found internally inconsistent fee data when resolving a transaction in the database.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DEPOSITS_GET_DB_FEE_INCONSISTENT = 1800,
+
+ /**
+ * The exchange encountered an error (that is not about not finding the transaction) trying to lookup a transaction in the database.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DEPOSITS_GET_DB_FETCH_FAILED = 1801,
+
+ /**
+ * The exchange did not find information about the specified transaction in the database.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DEPOSITS_GET_NOT_FOUND = 1802,
+
+ /**
+ * The exchange failed to identify the wire transfer of the transaction (or information about the plan that it was supposed to still happen in the future).
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DEPOSITS_GET_WTID_RESOLUTION_ERROR = 1803,
+
+ /**
+ * The signature of the merchant is invalid.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DEPOSITS_GET_MERCHANT_SIGNATURE_INVALID = 1804,
+
+ /**
+ * The given denomination key is not in the "recoup" set of the exchange right now.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ RECOUP_DENOMINATION_KEY_UNKNOWN = 1850,
+
+ /**
+ * The given coin signature is invalid for the request.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ RECOUP_SIGNATURE_INVALID = 1851,
+
+ /**
+ * The signature of the denomination key over the coin is not valid.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ RECOUP_DENOMINATION_SIGNATURE_INVALID = 1852,
+
+ /**
+ * The exchange failed to access its own database about reserves.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ RECOUP_DB_FETCH_FAILED = 1853,
+
+ /**
+ * The exchange could not find the corresponding withdraw operation. The request is denied.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ RECOUP_WITHDRAW_NOT_FOUND = 1854,
+
+ /**
+ * The exchange obtained an internally inconsistent transaction history for the given coin.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ RECOUP_HISTORY_DB_ERROR = 1855,
+
+ /**
+ * The exchange failed to store information about the recoup to be performed in the database.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ RECOUP_DB_PUT_FAILED = 1856,
+
+ /**
+ * The coin's remaining balance is zero. The request is denied.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ RECOUP_COIN_BALANCE_ZERO = 1857,
+
+ /**
+ * The exchange failed to reproduce the coin's blinding.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ RECOUP_BLINDING_FAILED = 1858,
+
+ /**
+ * The coin's remaining balance is zero. The request is denied.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ RECOUP_COIN_BALANCE_NEGATIVE = 1859,
+
+ /**
+ * Validity period of the denomination key is in the future.
+ * Returned with an HTTP status code of #MHD_HTTP_PRECONDITION_FAILED (412).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ RECOUP_DENOMINATION_VALIDITY_IN_FUTURE = 1860,
+
+ /**
+ * The exchange responded with a reply that did not satsify the protocol. This error is not used in the protocol but created client-side.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ RECOUP_REPLY_MALFORMED = 1861,
+
+ /**
+ * The "have" parameter was not a natural number.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ KEYS_HAVE_NOT_NUMERIC = 1900,
+
+ /**
+ * We currently cannot find any keys.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ KEYS_MISSING = 1901,
+
+ /**
+ * This exchange does not allow clients to request /keys for times other than the current (exchange) time.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ KEYS_TIMETRAVEL_FORBIDDEN = 1902,
+
+ /**
+ * The keys response was malformed. This error is generated client-side.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ KEYS_INVALID = 1903,
+
+ /**
+ * The backend could not find the merchant instance specified in the request.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ INSTANCE_UNKNOWN = 2000,
+
+ /**
+ * The backend lacks a wire transfer method configuration option for the given instance.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PROPOSAL_INSTANCE_CONFIGURATION_LACKS_WIRE = 2002,
+
+ /**
+ * The merchant failed to provide a meaningful response to a /pay request. This error is created client-side.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PAY_MERCHANT_INVALID_RESPONSE = 2100,
+
+ /**
+ * The exchange responded saying that funds were insufficient (for example, due to double-spending).
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PAY_INSUFFICIENT_FUNDS = 2101,
+
+ /**
+ * The merchant failed to commit the exchanges' response to a /deposit request to its database.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PAY_DB_STORE_PAY_ERROR = 2102,
+
+ /**
+ * The specified exchange is not supported/trusted by this merchant.
+ * Returned with an HTTP status code of #MHD_HTTP_PRECONDITION_FAILED (412).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PAY_EXCHANGE_REJECTED = 2103,
+
+ /**
+ * The denomination key used for payment is not listed among the denomination keys of the exchange.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PAY_DENOMINATION_KEY_NOT_FOUND = 2104,
+
+ /**
+ * The denomination key used for payment is not audited by an auditor approved by the merchant.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PAY_DENOMINATION_KEY_AUDITOR_FAILURE = 2105,
+
+ /**
+ * There was an integer overflow totaling up the amounts or deposit fees in the payment.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PAY_AMOUNT_OVERFLOW = 2106,
+
+ /**
+ * The deposit fees exceed the total value of the payment.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PAY_FEES_EXCEED_PAYMENT = 2107,
+
+ /**
+ * After considering deposit and wire fees, the payment is insufficient to satisfy the required amount for the contract. The client should revisit the logic used to calculate fees it must cover.
+ * Returned with an HTTP status code of #MHD_HTTP_ACCEPTED (202).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PAY_PAYMENT_INSUFFICIENT_DUE_TO_FEES = 2108,
+
+ /**
+ * Even if we do not consider deposit and wire fees, the payment is insufficient to satisfy the required amount for the contract.
+ * Returned with an HTTP status code of #MHD_HTTP_ACCEPTED (202).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PAY_PAYMENT_INSUFFICIENT = 2109,
+
+ /**
+ * The signature over the contract of one of the coins was invalid.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PAY_COIN_SIGNATURE_INVALID = 2110,
+
+ /**
+ * We failed to contact the exchange for the /pay request.
+ * Returned with an HTTP status code of #MHD_HTTP_REQUEST_TIMEOUT (408).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PAY_EXCHANGE_TIMEOUT = 2111,
+
+ /**
+ * When we tried to find information about the exchange to issue the deposit, we failed. This usually only happens if the merchant backend is somehow unable to get its own HTTP client logic to work.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PAY_EXCHANGE_LOOKUP_FAILED = 2112,
+
+ /**
+ * The refund deadline in the contract is after the transfer deadline.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PAY_REFUND_DEADLINE_PAST_WIRE_TRANSFER_DEADLINE = 2114,
+
+ /**
+ * The request fails to provide coins for the payment.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PAY_COINS_ARRAY_EMPTY = 2115,
+
+ /**
+ * The merchant failed to fetch the contract terms from the merchant's database.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PAY_DB_FETCH_PAY_ERROR = 2116,
+
+ /**
+ * The merchant failed to fetch the merchant's previous state with respect to transactions from its database.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PAY_DB_FETCH_TRANSACTION_ERROR = 2117,
+
+ /**
+ * The merchant failed to store the merchant's state with respect to the transaction in its database.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PAY_DB_STORE_TRANSACTION_ERROR = 2119,
+
+ /**
+ * The exchange failed to provide a valid response to the merchant's /keys request.
+ * Returned with an HTTP status code of #MHD_HTTP_FAILED_DEPENDENCY (424).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PAY_EXCHANGE_KEYS_FAILURE = 2120,
+
+ /**
+ * The payment is too late, the offer has expired.
+ * Returned with an HTTP status code of #MHD_HTTP_GONE (410).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PAY_OFFER_EXPIRED = 2121,
+
+ /**
+ * The "merchant" field is missing in the proposal data.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PAY_MERCHANT_FIELD_MISSING = 2122,
+
+ /**
+ * Failed computing a hash code (likely server out-of-memory).
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PAY_FAILED_COMPUTE_PROPOSAL_HASH = 2123,
+
+ /**
+ * Failed to locate merchant's account information matching the wire hash given in the proposal.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PAY_WIRE_HASH_UNKNOWN = 2124,
+
+ /**
+ * We got different currencies for the wire fee and the maximum wire fee.
+ * Returned with an HTTP status code of #MHD_HTTP_PRECONDITION_FAILED (412).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PAY_WIRE_FEE_CURRENCY_MISMATCH = 2125,
+
+ /**
+ * The exchange had a failure when trying to process the request, returning a malformed response.
+ * Returned with an HTTP status code of #MHD_HTTP_FAILED_DEPENDENCY (424).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PAY_EXCHANGE_REPLY_MALFORMED = 2126,
+
+ /**
+ * A unknown merchant public key was included in the payment. That happens typically when the wallet sends the payment to the wrong merchant instance.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PAY_WRONG_INSTANCE = 2127,
+
+ /**
+ * The exchange failed to give us a response when we asked for /keys.
+ * Returned with an HTTP status code of #MHD_HTTP_FAILED_DEPENDENCY (424).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PAY_EXCHANGE_HAS_NO_KEYS = 2128,
+
+ /**
+ * The deposit time for the denomination has expired.
+ * Returned with an HTTP status code of #MHD_HTTP_GONE (410).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PAY_DENOMINATION_DEPOSIT_EXPIRED = 2129,
+
+ /**
+ * The proposal is not known to the backend.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PAY_PROPOSAL_NOT_FOUND = 2130,
+
+ /**
+ * The exchange of the deposited coin charges a wire fee that could not be added to the total (total amount too high).
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PAY_EXCHANGE_WIRE_FEE_ADDITION_FAILED = 2131,
+
+ /**
+ * The contract was not fully paid because of refunds. Note that clients MAY treat this as paid if, for example, contracts must be executed despite of refunds.
+ * Returned with an HTTP status code of #MHD_HTTP_PAYMENT_REQUIRED (402).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PAY_REFUNDED = 2132,
+
+ /**
+ * According to our database, we have refunded more than we were paid (which should not be possible).
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PAY_REFUNDS_EXCEED_PAYMENTS = 2133,
+
+ /**
+ * Legacy stuff. Remove me with protocol v1.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PAY_ABORT_REFUND_REFUSED_PAYMENT_COMPLETE = 2134,
+
+ /**
+ * The payment failed at the exchange.
+ * Returned with an HTTP status code of #MHD_HTTP_FAILED_DEPENDENCY (424).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PAY_EXCHANGE_FAILED = 2135,
+
+ /**
+ * The merchant backend couldn't verify the order payment because of a database failure.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PAID_DB_ERROR = 2146,
+
+ /**
+ * The order is not known.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PAID_ORDER_UNKNOWN = 2147,
+
+ /**
+ * The contract hash does not match the given order ID.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PAID_CONTRACT_HASH_MISMATCH = 2148,
+
+ /**
+ * The signature of the merchant is not valid for the given contract hash.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PAID_COIN_SIGNATURE_INVALID = 2149,
+
+ /**
+ * The merchant failed to contact the exchange.
+ * Returned with an HTTP status code of #MHD_HTTP_FAILED_DEPENDENCY (424).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ABORT_EXCHANGE_KEYS_FAILURE = 2150,
+
+ /**
+ * The merchant failed to send the exchange the refund request.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ABORT_EXCHANGE_REFUND_FAILED = 2151,
+
+ /**
+ * The merchant failed to find the exchange to process the lookup.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ABORT_EXCHANGE_LOOKUP_FAILED = 2152,
+
+ /**
+ * The merchant failed to store the abort request in its database.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ABORT_DB_STORE_ABORT_ERROR = 2153,
+
+ /**
+ * The merchant failed to repeatedly serialize the transaction.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ABORT_DB_STORE_TRANSACTION_ERROR = 2154,
+
+ /**
+ * The merchant failed in the lookup part of the transaction.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ABORT_DB_FETCH_TRANSACTION_ERROR = 2155,
+
+ /**
+ * The merchant could not find the contract.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ABORT_CONTRACT_NOT_FOUND = 2156,
+
+ /**
+ * The payment was already completed and thus cannot be aborted anymore.
+ * Returned with an HTTP status code of #MHD_HTTP_PRECONDITION_FAILED (412).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ABORT_REFUND_REFUSED_PAYMENT_COMPLETE = 2157,
+
+ /**
+ * The hash provided by the wallet does not match the order.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ABORT_CONTRACT_HASH_MISSMATCH = 2158,
+
+ /**
+ * The array of coins cannot be empty.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ABORT_COINS_ARRAY_EMPTY = 2159,
+
+ /**
+ * The merchant experienced a timeout processing the request.
+ * Returned with an HTTP status code of #MHD_HTTP_REQUEST_TIMEOUT (408).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ABORT_EXCHANGE_TIMEOUT = 2160,
+
+ /**
+ * The merchant could not find the order.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ FORGET_ORDER_NOT_FOUND = 2180,
+
+ /**
+ * One of the paths to forget is malformed.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ FORGET_PATH_SYNTAX_INCORRECT = 2181,
+
+ /**
+ * One of the paths to forget was not marked as forgettable.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ FORGET_PATH_NOT_FORGETTABLE = 2182,
+
+ /**
+ * Integer overflow with specified timestamp argument detected.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ HISTORY_TIMESTAMP_OVERFLOW = 2200,
+
+ /**
+ * Failed to retrieve history from merchant database.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ HISTORY_DB_FETCH_ERROR = 2201,
+
+ /**
+ * The backend could not find the contract specified in the request.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ POLL_PAYMENT_CONTRACT_NOT_FOUND = 2250,
+
+ /**
+ * The response provided by the merchant backend was malformed. This error is created client-side.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ POLL_PAYMENT_REPLY_MALFORMED = 2251,
+
+ /**
+ * We failed to contact the exchange for the /track/transaction request.
+ * Returned with an HTTP status code of #MHD_HTTP_SERVICE_UNAVAILABLE (503).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TRACK_TRANSACTION_EXCHANGE_TIMEOUT = 2300,
+
+ /**
+ * We failed to get a valid /keys response from the exchange for the /track/transaction request.
+ * Returned with an HTTP status code of #MHD_HTTP_FAILED_DEPENDENCY (424).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TRACK_TRANSACTION_EXCHANGE_KEYS_FAILURE = 2301,
+
+ /**
+ * The backend could not find the transaction specified in the request.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TRACK_TRANSACTION_TRANSACTION_UNKNOWN = 2302,
+
+ /**
+ * The backend had a database access error trying to retrieve transaction data from its database.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TRACK_TRANSACTION_DB_FETCH_TRANSACTION_ERROR = 2303,
+
+ /**
+ * The backend had a database access error trying to retrieve payment data from its database.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TRACK_TRANSACTION_DB_FETCH_PAYMENT_ERROR = 2304,
+
+ /**
+ * The backend found no applicable deposits in the database. This is odd, as we know about the transaction, but not about deposits we made for the transaction.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TRACK_TRANSACTION_DB_NO_DEPOSITS_ERROR = 2305,
+
+ /**
+ * We failed to obtain a wire transfer identifier for one of the coins in the transaction.
+ * Returned with an HTTP status code of #MHD_HTTP_FAILED_DEPENDENCY (424).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TRACK_TRANSACTION_COIN_TRACE_ERROR = 2306,
+
+ /**
+ * We failed to obtain the full wire transfer identifier for the transfer one of the coins was aggregated into.
+ * Returned with an HTTP status code of #MHD_HTTP_FAILED_DEPENDENCY (424).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TRACK_TRANSACTION_WIRE_TRANSFER_TRACE_ERROR = 2307,
+
+ /**
+ * We got conflicting reports from the exhange with respect to which transfers are included in which aggregate.
+ * Returned with an HTTP status code of #MHD_HTTP_FAILED_DEPENDENCY (424).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TRACK_TRANSACTION_CONFLICTING_REPORTS = 2308,
+
+ /**
+ * We did failed to retrieve information from our database.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GET_TRANSFERS_DB_FETCH_ERROR = 2350,
+
+ /**
+ * We failed to contact the exchange for the /track/transfer request.
+ * Returned with an HTTP status code of #MHD_HTTP_SERVICE_UNAVAILABLE (503).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ POST_TRANSFERS_EXCHANGE_TIMEOUT = 2400,
+
+ /**
+ * We failed to obtain an acceptable /keys response from the exchange for the /track/transfer request.
+ * Returned with an HTTP status code of #MHD_HTTP_FAILED_DEPENDENCY (424).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ POST_TRANSFERS_EXCHANGE_KEYS_FAILURE = 2401,
+
+ /**
+ * We failed to persist coin wire transfer information in our merchant database.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ POST_TRANSFERS_DB_STORE_COIN_ERROR = 2402,
+
+ /**
+ * We internally failed to execute the /track/transfer request.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ POST_TRANSFERS_REQUEST_ERROR = 2403,
+
+ /**
+ * We failed to persist wire transfer information in our merchant database.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ POST_TRANSFERS_DB_STORE_TRANSFER_ERROR = 2404,
+
+ /**
+ * The exchange returned an error from /track/transfer.
+ * Returned with an HTTP status code of #MHD_HTTP_FAILED_DEPENDENCY (424).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ POST_TRANSFERS_EXCHANGE_ERROR = 2405,
+
+ /**
+ * We failed to fetch deposit information from our merchant database.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ POST_TRANSFERS_DB_FETCH_DEPOSIT_ERROR = 2406,
+
+ /**
+ * We encountered an internal logic error.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ POST_TRANSFERS_DB_INTERNAL_LOGIC_ERROR = 2407,
+
+ /**
+ * The exchange gave conflicting information about a coin which has been wire transferred.
+ * Returned with an HTTP status code of #MHD_HTTP_FAILED_DEPENDENCY (424).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ POST_TRANSFERS_CONFLICTING_REPORTS = 2408,
+
+ /**
+ * The merchant backend had problems in creating the JSON response.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ POST_TRANSFERS_JSON_RESPONSE_ERROR = 2409,
+
+ /**
+ * The exchange charged a different wire fee than what it originally advertised, and it is higher.
+ * Returned with an HTTP status code of #MHD_HTTP_FAILED_DEPENDENCY (424).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ POST_TRANSFERS_JSON_BAD_WIRE_FEE = 2410,
+
+ /**
+ * We did not find the account that the transfer was made to.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ POST_TRANSFERS_ACCOUNT_NOT_FOUND = 2411,
+
+ /**
+ * We did failed to store information in our database.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ POST_TRANSFERS_DB_STORE_ERROR = 2412,
+
+ /**
+ * We did failed to retrieve information from our database.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ POST_TRANSFERS_DB_LOOKUP_ERROR = 2413,
+
+ /**
+ * The merchant backend cannot create an instance under the given identifier as one already exists. Use PATCH to modify the existing entry.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ POST_INSTANCES_ALREADY_EXISTS = 2450,
+
+ /**
+ * The merchant backend cannot create an instance because the specified bank accounts are somehow invalid.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ POST_INSTANCES_BAD_PAYTO_URIS = 2451,
+
+ /**
+ * The merchant backend cannot create an instance because it failed to start the database transaction.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ POST_INSTANCES_DB_START_ERROR = 2452,
+
+ /**
+ * The merchant backend cannot create an instance because it failed to commit the database transaction.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ POST_INSTANCES_DB_COMMIT_ERROR = 2453,
+
+ /**
+ * The merchant backend cannot delete an instance because it failed to commit the database transaction.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DELETE_INSTANCES_ID_DB_HARD_FAILURE = 2454,
+
+ /**
+ * The merchant backend cannot delete the data because it already does not exist.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DELETE_INSTANCES_ID_NO_SUCH_INSTANCE = 2455,
+
+ /**
+ * The merchant backend cannot update an instance because the specified bank accounts are somehow invalid.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PATCH_INSTANCES_BAD_PAYTO_URIS = 2456,
+
+ /**
+ * The merchant backend cannot patch an instance because it failed to start the database transaction.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PATCH_INSTANCES_DB_START_ERROR = 2457,
+
+ /**
+ * The merchant backend cannot patch an instance because it failed to commit the database transaction.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PATCH_INSTANCES_DB_COMMIT_ERROR = 2458,
+
+ /**
+ * The hash provided in the request of /map/in does not match the contract sent alongside in the same request.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MAP_IN_UNMATCHED_HASH = 2500,
+
+ /**
+ * The backend encountered an error while trying to store the h_contract_terms into the database.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PROPOSAL_STORE_DB_ERROR = 2501,
+
+ /**
+ * The backend encountered an error while trying to retrieve the proposal data from database. Likely to be an internal error.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PROPOSAL_LOOKUP_DB_ERROR = 2502,
+
+ /**
+ * The proposal being looked up is not found on this merchant.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PROPOSAL_LOOKUP_NOT_FOUND = 2503,
+
+ /**
+ * The proposal had no timestamp and the backend failed to obtain the local time. Likely to be an internal error.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PROPOSAL_NO_LOCALTIME = 2504,
+
+ /**
+ * The order provided to the backend could not be parsed, some required fields were missing or ill-formed.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PROPOSAL_ORDER_PARSE_ERROR = 2505,
+
+ /**
+ * The backend encountered an error while trying to find the existing proposal in the database.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PROPOSAL_STORE_DB_ERROR_HARD = 2506,
+
+ /**
+ * The backend encountered an error while trying to find the existing proposal in the database.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PROPOSAL_STORE_DB_ERROR_SOFT = 2507,
+
+ /**
+ * The backend encountered an error: the proposal already exists.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PROPOSAL_STORE_DB_ERROR_ALREADY_EXISTS = 2508,
+
+ /**
+ * The order provided to the backend uses an amount in a currency that does not match the backend's configuration.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PROPOSAL_ORDER_BAD_CURRENCY = 2509,
+
+ /**
+ * The response provided by the merchant backend was malformed. This error is created client-side.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PROPOSAL_REPLY_MALFORMED = 2510,
+
+ /**
+ * The order provided to the backend could not be deleted, it is not known.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ORDERS_DELETE_NO_SUCH_ORDER = 2511,
+
+ /**
+ * The order provided to the backend could not be deleted, our offer is still valid and awaiting payment.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ORDERS_DELETE_AWAITING_PAYMENT = 2512,
+
+ /**
+ * The order provided to the backend could not be deleted, due to a database error.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ORDERS_DELETE_DB_HARD_FAILURE = 2513,
+
+ /**
+ * The order provided to the backend could not be completed, due to a database error trying to fetch product inventory data.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ORDERS_LOOKUP_PRODUCT_DB_HARD_FAILURE = 2514,
+
+ /**
+ * The order provided to the backend could not be completed, due to a database serialization error (which should be impossible) trying to fetch product inventory data.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ORDERS_LOOKUP_PRODUCT_DB_SOFT_FAILURE = 2515,
+
+ /**
+ * The order provided to the backend could not be completed, because a product to be completed via inventory data is not actually in our inventory.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ORDERS_LOOKUP_PRODUCT_NOT_FOUND = 2516,
+
+ /**
+ * We could not obtain a list of all orders because of a database failure.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ORDERS_GET_DB_LOOKUP_ERROR = 2517,
+
+ /**
+ * We could not claim the order because of a database failure.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ORDERS_CLAIM_HARD_DB_ERROR = 2518,
+
+ /**
+ * We could not claim the order because of a database serialization failure.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ORDERS_CLAIM_SOFT_DB_ERROR = 2519,
+
+ /**
+ * We could not claim the order because the backend is unaware of it.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ORDERS_CLAIM_NOT_FOUND = 2520,
+
+ /**
+ * We could not claim the order because someone else claimed it first.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ ORDERS_ALREADY_CLAIMED = 2521,
+
+ /**
+ * The merchant backend failed to lookup the products.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GET_PRODUCTS_DB_LOOKUP_ERROR = 2550,
+
+ /**
+ * The merchant backend failed to start the transaction.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PRODUCTS_POST_DB_START_ERROR = 2551,
+
+ /**
+ * The product ID exists.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PRODUCTS_POST_CONFLICT_PRODUCT_EXISTS = 2552,
+
+ /**
+ * The merchant backend failed to serialize the transaction.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PRODUCTS_POST_DB_COMMIT_SOFT_ERROR = 2553,
+
+ /**
+ * The merchant backend failed to commit the transaction.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PRODUCTS_POST_DB_COMMIT_HARD_ERROR = 2554,
+
+ /**
+ * The merchant backend failed to commit the transaction.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PRODUCTS_PATCH_DB_COMMIT_HARD_ERROR = 2555,
+
+ /**
+ * The merchant backend did not find the product to be updated.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PRODUCTS_PATCH_UNKNOWN_PRODUCT = 2556,
+
+ /**
+ * The update would have reduced the total amount of product lost, which is not allowed.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PRODUCTS_PATCH_TOTAL_LOST_REDUCED = 2557,
+
+ /**
+ * The update would have mean that more stocks were lost than what remains from total inventory after sales, which is not allowed.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PRODUCTS_PATCH_TOTAL_LOST_EXCEEDS_STOCKS = 2558,
+
+ /**
+ * The update would have reduced the total amount of product in stock, which is not allowed.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PRODUCTS_PATCH_TOTAL_STOCKED_REDUCED = 2559,
+
+ /**
+ * The lock request is for more products than we have left (unlocked) in stock.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PRODUCTS_LOCK_INSUFFICIENT_STOCKS = 2560,
+
+ /**
+ * The lock request is for an unknown product.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PRODUCTS_LOCK_UNKNOWN_PRODUCT = 2561,
+
+ /**
+ * The deletion request resulted in a hard database error.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PRODUCTS_DELETE_DB_HARD_FAILURE = 2562,
+
+ /**
+ * The deletion request was for a product unknown to the backend.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PRODUCTS_DELETE_NO_SUCH_PRODUCT = 2563,
+
+ /**
+ * The deletion request is for a product that is locked.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PRODUCTS_DELETE_CONFLICTING_LOCK = 2564,
+
+ /**
+ * The merchant returned a malformed response. Error created client-side.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ REFUND_LOOKUP_INVALID_RESPONSE = 2600,
+
+ /**
+ * The frontend gave an unknown order id to issue the refund to.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ REFUND_ORDER_ID_UNKNOWN = 2601,
+
+ /**
+ * The amount to be refunded is inconsistent: either is lower than the previous amount being awarded, or it is too big to be paid back. In this second case, the fault stays on the business dept. side.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ REFUND_INCONSISTENT_AMOUNT = 2602,
+
+ /**
+ * The backend encountered an error while trying to retrieve the payment data from database. Likely to be an internal error.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ REFUND_LOOKUP_DB_ERROR = 2603,
+
+ /**
+ * The backend encountered an error while trying to retrieve the payment data from database. Likely to be an internal error.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ REFUND_MERCHANT_DB_COMMIT_ERROR = 2604,
+
+ /**
+ * Payments are stored in a single db transaction; this error indicates that one db operation within that transaction failed. This might involve storing of coins or other related db operations, like starting/committing the db transaction or marking a contract as paid.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PAY_DB_STORE_PAYMENTS_ERROR = 2605,
+
+ /**
+ * The backend failed to sign the refund request.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ PAY_REFUND_SIGNATURE_FAILED = 2606,
+
+ /**
+ * The merchant backend is not available of any applicable refund(s) for this order.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ REFUND_LOOKUP_NO_REFUND = 2607,
+
+ /**
+ * The frontend gave an unpaid order id to issue the refund to.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ REFUND_ORDER_ID_UNPAID = 2608,
+
+ /**
+ * The requested wire method is not supported by the exchange.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ RESERVES_POST_UNSUPPORTED_WIRE_METHOD = 2650,
+
+ /**
+ * The backend failed to commit the result to the database.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ RESERVES_POST_DB_COMMIT_HARD_ERROR = 2651,
+
+ /**
+ * The backend failed to fetch the requested information from the database.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GET_RESERVES_DB_LOOKUP_ERROR = 2652,
+
+ /**
+ * The backend knows the instance that was supposed to support the tip, but it was not configured for tipping (i.e. has no exchange associated with it). Likely to be a configuration error.
+ * Returned with an HTTP status code of #MHD_HTTP_PRECONDITION_FAILED (412).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TIP_AUTHORIZE_INSTANCE_DOES_NOT_TIP = 2701,
+
+ /**
+ * The reserve that was used to fund the tips has expired.
+ * Returned with an HTTP status code of #MHD_HTTP_GONE (410).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TIP_AUTHORIZE_RESERVE_EXPIRED = 2702,
+
+ /**
+ * The reserve that was used to fund the tips was not found in the DB.
+ * Returned with an HTTP status code of #MHD_HTTP_SERVICE_UNAVAILABLE (503).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TIP_AUTHORIZE_RESERVE_UNKNOWN = 2703,
+
+ /**
+ * The backend knows the instance that was supposed to support the tip, and it was configured for tipping. However, the funds remaining are insufficient to cover the tip, and the merchant should top up the reserve.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TIP_AUTHORIZE_INSUFFICIENT_FUNDS = 2704,
+
+ /**
+ * The backend had trouble accessing the database to persist information about the tip authorization. Returned with an HTTP status code of internal error.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TIP_AUTHORIZE_DB_HARD_ERROR = 2705,
+
+ /**
+ * The backend had trouble accessing the database to persist information about the tip authorization. The problem might be fixable by repeating the transaction.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TIP_AUTHORIZE_DB_SOFT_ERROR = 2706,
+
+ /**
+ * The backend failed to obtain a reserve status from the exchange.
+ * Returned with an HTTP status code of #MHD_HTTP_FAILED_DEPENDENCY (424).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TIP_QUERY_RESERVE_STATUS_FAILED_EXCHANGE_DOWN = 2707,
+
+ /**
+ * The backend got an empty (!) reserve history from the exchange.
+ * Returned with an HTTP status code of #MHD_HTTP_FAILED_DEPENDENCY (424).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TIP_QUERY_RESERVE_HISTORY_FAILED_EMPTY = 2708,
+
+ /**
+ * The backend got an invalid reserve history (fails to start with a deposit) from the exchange.
+ * Returned with an HTTP status code of #MHD_HTTP_FAILED_DEPENDENCY (424).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TIP_QUERY_RESERVE_HISTORY_INVALID_NO_DEPOSIT = 2709,
+
+ /**
+ * The backend got an 404 response from the exchange when it inquired about the reserve history.
+ * Returned with an HTTP status code of #MHD_HTTP_SERVICE_UNAVAILABLE (503).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TIP_QUERY_RESERVE_UNKNOWN_TO_EXCHANGE = 2710,
+
+ /**
+ * The backend got a reserve with a currency that does not match the backend's currency.
+ * Returned with an HTTP status code of #MHD_HTTP_SERVICE_UNAVAILABLE (503).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TIP_QUERY_RESERVE_CURRENCY_MISMATCH = 2711,
+
+ /**
+ * The backend got a reserve history with amounts it cannot process (addition failure in deposits).
+ * Returned with an HTTP status code of #MHD_HTTP_FAILED_DEPENDENCY (424).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TIP_QUERY_RESERVE_HISTORY_ARITHMETIC_ISSUE_DEPOSIT = 2712,
+
+ /**
+ * The backend got a reserve history with amounts it cannot process (addition failure in withdraw amounts).
+ * Returned with an HTTP status code of #MHD_HTTP_FAILED_DEPENDENCY (424).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TIP_QUERY_RESERVE_HISTORY_ARITHMETIC_ISSUE_WITHDRAW = 2713,
+
+ /**
+ * The backend got a reserve history with amounts it cannot process (addition failure in closing amounts).
+ * Returned with an HTTP status code of #MHD_HTTP_FAILED_DEPENDENCY (424).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TIP_QUERY_RESERVE_HISTORY_ARITHMETIC_ISSUE_CLOSED = 2714,
+
+ /**
+ * The backend got a reserve history with inconsistent amounts.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TIP_QUERY_RESERVE_HISTORY_ARITHMETIC_ISSUE_INCONSISTENT = 2715,
+
+ /**
+ * The backend encountered a database error querying tipping reserves.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TIP_QUERY_DB_ERROR = 2716,
+
+ /**
+ * The backend got an unexpected resever history reply from the exchange.
+ * Returned with an HTTP status code of #MHD_HTTP_FAILED_DEPENDENCY (424).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TIP_QUERY_RESERVE_HISTORY_FAILED = 2717,
+
+ /**
+ * The backend got a reserve history with amounts it cannot process (addition failure in withdraw amounts).
+ * Returned with an HTTP status code of #MHD_HTTP_FAILED_DEPENDENCY (424).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TIP_QUERY_RESERVE_HISTORY_ARITHMETIC_ISSUE_RECOUP = 2718,
+
+ /**
+ * The backend knows the instance that was supposed to support the tip, but it was not configured for tipping (i.e. has no exchange associated with it). Likely to be a configuration error.
+ * Returned with an HTTP status code of #MHD_HTTP_PRECONDITION_FAILED (412).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TIP_QUERY_INSTANCE_DOES_NOT_TIP = 2719,
+
+ /**
+ * The tip id is unknown. This could happen if the tip id is wrong or the tip authorization expired.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TIP_QUERY_TIP_ID_UNKNOWN = 2720,
+
+ /**
+ * The reserve could not be deleted due to a database failure.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ RESERVES_DELETE_DB_HARD_FAILURE = 2721,
+
+ /**
+ * The reserve could not be deleted because it is unknown.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ RESERVES_DELETE_NO_SUCH_RESERVE = 2722,
+
+ /**
+ * The backend got an unexpected error trying to lookup reserve details from the backend.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TIP_LOOKUP_RESERVE_DB_FAILURE = 2723,
+
+ /**
+ * The backend repeatedly failed to serialize the transaction to authorize the tip.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TIP_AUTHORIZE_DB_SERIALIZATION_FAILURE = 2724,
+
+ /**
+ * The backend failed to start the transaction to authorize the tip.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TIP_AUTHORIZE_DB_START_FAILURE = 2725,
+
+ /**
+ * The backend failed looking up the reserve needed to authorize the tip.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TIP_AUTHORIZE_DB_LOOKUP_RESERVE_FAILURE = 2726,
+
+ /**
+ * The backend failed to find a reserve needed to authorize the tip.
+ * Returned with an HTTP status code of #MHD_HTTP_SERVICE_UNAVAILABLE (503).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TIP_AUTHORIZE_DB_RESERVE_NOT_FOUND = 2727,
+
+ /**
+ * The backend encountered an internal invariant violation.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TIP_AUTHORIZE_DB_RESERVE_INVARIANT_FAILURE = 2728,
+
+ /**
+ * The selected exchange expired.
+ * Returned with an HTTP status code of #MHD_HTTP_GONE (410).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TIP_AUTHORIZE_DB_RESERVE_EXPIRED = 2729,
+
+ /**
+ * The backend failed updating the reserve needed to authorize the tip.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TIP_AUTHORIZE_DB_UPDATE_RESERVE_FAILURE = 2730,
+
+ /**
+ * The backend had trouble accessing the database to persist information about enabling tips. Returned with an HTTP status code of internal error.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TIP_ENABLE_DB_TRANSACTION_ERROR = 2750,
+
+ /**
+ * The tip ID is unknown. This could happen if the tip has expired.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TIP_PICKUP_TIP_ID_UNKNOWN = 2800,
+
+ /**
+ * The amount requested exceeds the remaining tipping balance for this tip ID. Returned with an HTTP status code of "Conflict" (as it conflicts with a previous pickup operation).
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TIP_PICKUP_NO_FUNDS = 2801,
+
+ /**
+ * We encountered a DB error, repeating the request may work.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TIP_PICKUP_DB_ERROR_SOFT = 2802,
+
+ /**
+ * We encountered a DB error, repeating the request will not help. This is an internal server error.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TIP_PICKUP_DB_ERROR_HARD = 2803,
+
+ /**
+ * The same pickup ID was already used for picking up a different amount. This points to a very strange internal error as the pickup ID is derived from the denomination key which is tied to a particular amount. Hence this should also be an internal server error.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TIP_PICKUP_AMOUNT_CHANGED = 2804,
+
+ /**
+ * We failed to contact the exchange to obtain the denomination keys.
+ * Returned with an HTTP status code of #MHD_HTTP_FAILED_DEPENDENCY (424).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TIP_PICKUP_EXCHANGE_DOWN = 2805,
+
+ /**
+ * We contacted the exchange to obtain any denomination keys, but got no valid keys.
+ * Returned with an HTTP status code of #MHD_HTTP_FAILED_DEPENDENCY (424).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TIP_PICKUP_EXCHANGE_LACKED_KEYS = 2806,
+
+ /**
+ * We contacted the exchange to obtain at least one of the denomination keys specified in the request. Returned with a response code "not found" (404).
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TIP_PICKUP_EXCHANGE_LACKED_KEY = 2807,
+
+ /**
+ * We encountered an arithmetic issue totaling up the amount to withdraw.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TIP_PICKUP_EXCHANGE_AMOUNT_OVERFLOW = 2808,
+
+ /**
+ * The number of planchets specified exceeded the limit.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TIP_PICKUP_EXCHANGE_TOO_MANY_PLANCHETS = 2809,
+
+ /**
+ * The merchant failed to initialize the withdraw operation.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TIP_PICKUP_WITHDRAW_FAILED = 2810,
+
+ /**
+ * The merchant failed to initialize the withdraw operation.
+ * Returned with an HTTP status code of #MHD_HTTP_FAILED_DEPENDENCY (424).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TIP_PICKUP_WITHDRAW_FAILED_AT_EXCHANGE = 2811,
+
+ /**
+ * The client failed to unblind the signature returned by the merchant. Generated client-side.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TIP_PICKUP_UNBLIND_FAILURE = 2812,
+
+ /**
+ * Merchant failed to access its database to lookup the tip.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GET_TIPS_DB_LOOKUP_ERROR = 2813,
+
+ /**
+ * Merchant failed find the tip in its database.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GET_TIPS_ID_UNKNOWN = 2814,
+
+ /**
+ * The merchant failed to contact the exchange.
+ * Returned with an HTTP status code of #MHD_HTTP_FAILED_DEPENDENCY (424).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TIP_PICKUP_CONTACT_EXCHANGE_ERROR = 2815,
+
+ /**
+ * The merchant failed to obtain keys from the exchange.
+ * Returned with an HTTP status code of #MHD_HTTP_FAILED_DEPENDENCY (424).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TIP_PICKUP_EXCHANGE_KEYS_ERROR = 2816,
+
+ /**
+ * The merchant failed to store data in its own database.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TIP_PICKUP_DB_STORE_HARD_ERROR = 2817,
+
+ /**
+ * The merchant failed to get a timely response from the exchange.
+ * Returned with an HTTP status code of #MHD_HTTP_REQUEST_TIMEOUT (408).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TIP_PICKUP_EXCHANGE_TIMEOUT = 2818,
+
+ /**
+ * The exchange returned a failure code for the withdraw operation.
+ * Returned with an HTTP status code of #MHD_HTTP_FAILED_DEPENDENCY (424).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TIP_PICKUP_EXCHANGE_ERROR = 2819,
+
+ /**
+ * The merchant failed to add up the amounts to compute the pick up value.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TIP_PICKUP_SUMMATION_FAILED = 2820,
+
+ /**
+ * The tip expired.
+ * Returned with an HTTP status code of #MHD_HTTP_GONE (410).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TIP_PICKUP_HAS_EXPIRED = 2821,
+
+ /**
+ * The requested withdraw amount exceeds the amount remaining to be picked up.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TIP_PICKUP_AMOUNT_EXCEEDS_TIP_REMAINING = 2822,
+
+ /**
+ * The merchant failed to store data in its own database.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TIP_PICKUP_DB_STORE_SOFT_ERROR = 2823,
+
+ /**
+ * The merchant did not find the specified denomination key in the exchange's key set.
+ * Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TIP_PICKUP_DENOMINATION_UNKNOWN = 2824,
+
+ /**
+ * We failed to fetch contract terms from our merchant database.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GET_ORDERS_DB_LOOKUP_ERROR = 2900,
+
+ /**
+ * We failed to find the contract terms from our merchant database.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GET_ORDERS_ID_UNKNOWN = 2901,
+
+ /**
+ * The merchant had a timeout contacting the exchange, thus not providing wire details in the response.
+ * Returned with an HTTP status code of #MHD_HTTP_REQUEST_TIMEOUT (408).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GET_ORDERS_EXCHANGE_TIMEOUT = 2902,
+
+ /**
+ * The exchange failed to provide a valid answer to the tracking request, thus those details are not in the response.
+ * Returned with an HTTP status code of #MHD_HTTP_OK (200).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GET_ORDERS_EXCHANGE_TRACKING_FAILURE = 2903,
+
+ /**
+ * The merchant backend failed to persist tracking details in its database, thus those details are not in the response.
+ * Returned with an HTTP status code of #MHD_HTTP_OK (200).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GET_ORDERS_DB_STORE_TRACKING_FAILURE = 2904,
+
+ /**
+ * The merchant backend encountered a failure in computing the deposit total.
+ * Returned with an HTTP status code of #MHD_HTTP_OK (200).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GET_ORDERS_AMOUNT_ARITHMETIC_FAILURE = 2905,
+
+ /**
+ * The merchant backend failed trying to contact the exchange for tracking details, thus those details are not in the response.
+ * Returned with an HTTP status code of #MHD_HTTP_FAILED_DEPENDENCY (424).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GET_ORDERS_EXCHANGE_LOOKUP_FAILURE = 2906,
+
+ /**
+ * The merchant backend failed to construct the request for tracking to the exchange, thus tracking details are not in the response.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GET_ORDERS_EXCHANGE_REQUEST_FAILURE = 2907,
+
+ /**
+ * The merchant backend had a database failure trying to find information about the contract of the order.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GET_ORDERS_DB_FETCH_CONTRACT_TERMS_ERROR = 2908,
+
+ /**
+ * The merchant backend could not find an order with the given identifier.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GET_ORDERS_ORDER_NOT_FOUND = 2909,
+
+ /**
+ * The merchant backend could not compute the hash of the proposal.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GET_ORDERS_FAILED_COMPUTE_PROPOSAL_HASH = 2910,
+
+ /**
+ * The merchant backend could not fetch the payment status from its database.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GET_ORDERS_DB_FETCH_PAYMENT_STATUS = 2911,
+
+ /**
+ * The merchant backend had an error looking up information in its database.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GET_ORDERS_DB_FETCH_TRANSACTION_ERROR = 2912,
+
+ /**
+ * The contract obtained from the merchant backend was malformed.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GET_ORDERS_CONTRACT_CONTENT_INVALID = 2913,
+
+ /**
+ * We failed to contract terms from our merchant database.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ CHECK_PAYMENT_DB_FETCH_CONTRACT_TERMS_ERROR = 2914,
+
+ /**
+ * We failed to contract terms from our merchant database.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ CHECK_PAYMENT_DB_FETCH_ORDER_ERROR = 2915,
+
+ /**
+ * The order id we're checking is unknown, likely the frontend did not create the order first.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ CHECK_PAYMENT_ORDER_ID_UNKNOWN = 2916,
+
+ /**
+ * Failed computing a hash code (likely server out-of-memory).
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ CHECK_PAYMENT_FAILED_COMPUTE_PROPOSAL_HASH = 2917,
+
+ /**
+ * Signature "session_sig" failed to verify.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ CHECK_PAYMENT_SESSION_SIGNATURE_INVALID = 2918,
+
+ /**
+ * The order we found does not match the provided contract hash.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GET_ORDER_WRONG_CONTRACT = 2919,
+
+ /**
+ * The response we received from the merchant is malformed. This error is generated client-side.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ CHECK_PAYMENT_RESPONSE_MALFORMED = 2920,
+
+ /**
+ * The merchant backend failed trying to contact the exchange for tracking details, thus those details are not in the response.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ GET_ORDERS_EXCHANGE_LOOKUP_START_FAILURE = 2921,
+
+ /**
+ * The response we received from the merchant is malformed. This error is generated client-side.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_ORDER_GET_REPLY_MALFORMED = 2922,
+
+ /**
+ * The token used to authenticate the client is invalid for this order.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ MERCHANT_GET_ORDER_INVALID_TOKEN = 2923,
+
+ /**
+ * The signature from the exchange on the deposit confirmation is invalid. Returned with a "400 Bad Request" status code.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DEPOSIT_CONFIRMATION_SIGNATURE_INVALID = 3000,
+
+ /**
+ * The auditor had trouble storing the deposit confirmation in its database.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ DEPOSIT_CONFIRMATION_STORE_DB_ERROR = 3001,
+
+ /**
+ * The auditor had trouble retrieving the exchange list from its database.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ LIST_EXCHANGES_DB_ERROR = 3002,
+
+ /**
+ * The auditor had trouble storing an exchange in its database.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ AUDITOR_EXCHANGE_STORE_DB_ERROR = 3003,
+
+ /**
+ * The auditor (!) responded with a reply that did not satsify the protocol. This error is not used in the protocol but created client- side.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ AUDITOR_EXCHANGES_REPLY_MALFORMED = 3004,
+
+ /**
+ * The exchange failed to compute ECDH.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TEST_ECDH_ERROR = 4000,
+
+ /**
+ * The EdDSA test signature is invalid.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TEST_EDDSA_INVALID = 4001,
+
+ /**
+ * The exchange failed to compute the EdDSA test signature.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TEST_EDDSA_ERROR = 4002,
+
+ /**
+ * The exchange failed to generate an RSA key.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TEST_RSA_GEN_ERROR = 4003,
+
+ /**
+ * The exchange failed to compute the public RSA key.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TEST_RSA_PUB_ERROR = 4004,
+
+ /**
+ * The exchange failed to compute the RSA signature.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ TEST_RSA_SIGN_ERROR = 4005,
+
+ /**
+ * The JSON in the server's response was malformed. This response is provided with HTTP status code of 0.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ SERVER_JSON_INVALID = 5000,
+
+ /**
+ * A signature in the server's response was malformed. This response is provided with HTTP status code of 0.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ SERVER_SIGNATURE_INVALID = 5001,
+
+ /**
+ * Wire transfer attempted with credit and debit party being the same bank account.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_SAME_ACCOUNT = 5102,
+
+ /**
+ * Wire transfer impossible, due to financial limitation of the party that attempted the payment.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_UNALLOWED_DEBIT = 5103,
+
+ /**
+ * Arithmetic operation between two amounts of different currency was attempted.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_CURRENCY_MISMATCH = 5104,
+
+ /**
+ * At least one GET parameter was either missing or invalid for the requested operation.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_PARAMETER_MISSING_OR_INVALID = 5105,
+
+ /**
+ * JSON body sent was invalid for the requested operation.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_JSON_INVALID = 5106,
+
+ /**
+ * Negative number was used (as value and/or fraction) to initiate a Amount object.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_NEGATIVE_NUMBER_AMOUNT = 5107,
+
+ /**
+ * A number too big was used (as value and/or fraction) to initiate a amount object.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_NUMBER_TOO_BIG = 5108,
+
+ /**
+ * Could not login for the requested operation.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_LOGIN_FAILED = 5109,
+
+ /**
+ * The bank account referenced in the requested operation was not found. Returned along "400 Not found".
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_UNKNOWN_ACCOUNT = 5110,
+
+ /**
+ * The transaction referenced in the requested operation (typically a reject operation), was not found.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_TRANSACTION_NOT_FOUND = 5111,
+
+ /**
+ * Bank received a malformed amount string.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_BAD_FORMAT_AMOUNT = 5112,
+
+ /**
+ * The client does not own the account credited by the transaction which is to be rejected, so it has no rights do reject it. To be returned along HTTP 403 Forbidden.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_REJECT_NO_RIGHTS = 5200,
+
+ /**
+ * This error code is returned when no known exception types captured the exception, and comes along with a 500 Internal Server Error.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_UNMANAGED_EXCEPTION = 5300,
+
+ /**
+ * This error code is used for all those exceptions that do not really need a specific error code to return to the client, but need to signal the middleware that the bank is not responding with 500 Internal Server Error. Used for example when a client is trying to register with a unavailable username.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_SOFT_EXCEPTION = 5400,
+
+ /**
+ * The request UID for a request to transfer funds has already been used, but with different details for the transfer.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ BANK_TRANSFER_REQUEST_UID_REUSED = 5500,
+
+ /**
+ * The sync service failed to access its database.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ SYNC_DB_FETCH_ERROR = 6000,
+
+ /**
+ * The sync service failed find the record in its database.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ SYNC_BACKUP_UNKNOWN = 6001,
+
+ /**
+ * The sync service failed find the account in its database.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ SYNC_ACCOUNT_UNKNOWN = 6002,
+
+ /**
+ * The SHA-512 hash provided in the If-None-Match header is malformed.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ SYNC_BAD_IF_NONE_MATCH = 6003,
+
+ /**
+ * The SHA-512 hash provided in the If-Match header is malformed or missing.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ SYNC_BAD_IF_MATCH = 6004,
+
+ /**
+ * The signature provided in the "Sync-Signature" header is malformed or missing.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ SYNC_BAD_SYNC_SIGNATURE = 6005,
+
+ /**
+ * The signature provided in the "Sync-Signature" header does not match the account, old or new Etags.
+ * Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ SYNC_INVALID_SIGNATURE = 6007,
+
+ /**
+ * The "Content-length" field for the upload is either not a number, or too big, or missing.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ SYNC_BAD_CONTENT_LENGTH = 6008,
+
+ /**
+ * The "Content-length" field for the upload is too big based on the server's terms of service.
+ * Returned with an HTTP status code of #MHD_HTTP_PAYLOAD_TOO_LARGE (413).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ SYNC_EXCESSIVE_CONTENT_LENGTH = 6009,
+
+ /**
+ * The server is out of memory to handle the upload. Trying again later may succeed.
+ * Returned with an HTTP status code of #MHD_HTTP_PAYLOAD_TOO_LARGE (413).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ SYNC_OUT_OF_MEMORY_ON_CONTENT_LENGTH = 6010,
+
+ /**
+ * The uploaded data does not match the Etag.
+ * Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ SYNC_INVALID_UPLOAD = 6011,
+
+ /**
+ * We failed to check for existing upload data in the database.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ SYNC_DATABASE_FETCH_ERROR = 6012,
+
+ /**
+ * HTTP server was being shutdown while this operation was pending.
+ * Returned with an HTTP status code of #MHD_HTTP_SERVICE_UNAVAILABLE (503).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ SYNC_SHUTDOWN = 6013,
+
+ /**
+ * HTTP server experienced a timeout while awaiting promised payment.
+ * Returned with an HTTP status code of #MHD_HTTP_REQUEST_TIMEOUT (408).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ SYNC_PAYMENT_TIMEOUT = 6014,
+
+ /**
+ * Sync could not store order data in its own database.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ SYNC_PAYMENT_CREATE_DB_ERROR = 6015,
+
+ /**
+ * Sync could not store payment confirmation in its own database.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ SYNC_PAYMENT_CONFIRM_DB_ERROR = 6016,
+
+ /**
+ * Sync could not fetch information about possible existing orders from its own database.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ SYNC_PAYMENT_CHECK_ORDER_DB_ERROR = 6017,
+
+ /**
+ * Sync could not setup the payment request with its own backend.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ SYNC_PAYMENT_CREATE_BACKEND_ERROR = 6018,
+
+ /**
+ * The sync service failed find the backup to be updated in its database.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ SYNC_PREVIOUS_BACKUP_UNKNOWN = 6019,
+
+ /**
+ * The wallet does not implement a version of the exchange protocol that is compatible with the protocol version of the exchange.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_IMPLEMENTED (501).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_EXCHANGE_PROTOCOL_VERSION_INCOMPATIBLE = 7000,
+
+ /**
+ * The wallet encountered an unexpected exception. This is likely a bug in the wallet implementation.
+ * Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_UNEXPECTED_EXCEPTION = 7001,
+
+ /**
+ * The wallet received a response from a server, but the response can't be parsed.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_RECEIVED_MALFORMED_RESPONSE = 7002,
+
+ /**
+ * The wallet tried to make a network request, but it received no response.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_NETWORK_ERROR = 7003,
+
+ /**
+ * The wallet tried to make a network request, but it was throttled.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_HTTP_REQUEST_THROTTLED = 7004,
+
+ /**
+ * The wallet made a request to a service, but received an error response it does not know how to handle.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_UNEXPECTED_REQUEST_ERROR = 7005,
+
+ /**
+ * The denominations offered by the exchange are insufficient. Likely the exchange is badly configured or not maintained.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_EXCHANGE_DENOMINATIONS_INSUFFICIENT = 7006,
+
+ /**
+ * The wallet does not support the operation requested by a client.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_CORE_API_OPERATION_UNKNOWN = 7007,
+
+ /**
+ * The given taler://pay URI is invalid.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_INVALID_TALER_PAY_URI = 7008,
+
+ /**
+ * The exchange does not know about the reserve (yet), and thus withdrawal can't progress.
+ * Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ WALLET_WITHDRAW_RESERVE_UNKNOWN_AT_EXCHANGE = 7010,
+
+ /**
+ * End of error code range.
+ * Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
+ * (A value of 0 indicates that the error is generated client-side).
+ */
+ END = 9999,
+}
diff --git a/packages/taler-wallet-core/src/crypto/primitives/kdf.d.ts.map b/packages/taler-wallet-core/src/crypto/primitives/kdf.d.ts.map
new file mode 100644
index 000000000..0495859a5
--- /dev/null
+++ b/packages/taler-wallet-core/src/crypto/primitives/kdf.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"kdf.d.ts","sourceRoot":"","sources":["kdf.ts"],"names":[],"mappings":"AAmBA,wBAAgB,MAAM,CAAC,IAAI,EAAE,UAAU,GAAG,UAAU,CAEnD;AAED,wBAAgB,IAAI,CAClB,MAAM,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,UAAU,EACrC,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,UAAU,EACf,OAAO,EAAE,UAAU,GAClB,UAAU,CAuBZ;AAED,wBAAgB,UAAU,CAAC,GAAG,EAAE,UAAU,EAAE,OAAO,EAAE,UAAU,GAAG,UAAU,CAE3E;AAED,wBAAgB,UAAU,CAAC,GAAG,EAAE,UAAU,EAAE,OAAO,EAAE,UAAU,GAAG,UAAU,CAE3E;AAED,wBAAgB,GAAG,CACjB,YAAY,EAAE,MAAM,EACpB,GAAG,EAAE,UAAU,EACf,IAAI,EAAE,UAAU,EAChB,IAAI,EAAE,UAAU,GACf,UAAU,CAyBZ"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/crypto/primitives/kdf.ts b/packages/taler-wallet-core/src/crypto/primitives/kdf.ts
new file mode 100644
index 000000000..edc681bc1
--- /dev/null
+++ b/packages/taler-wallet-core/src/crypto/primitives/kdf.ts
@@ -0,0 +1,92 @@
+/*
+ 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 * as nacl from "./nacl-fast";
+import { sha256 } from "./sha256";
+
+export function sha512(data: Uint8Array): Uint8Array {
+ return nacl.hash(data);
+}
+
+export function hmac(
+ digest: (d: Uint8Array) => Uint8Array,
+ blockSize: number,
+ key: Uint8Array,
+ message: Uint8Array,
+): Uint8Array {
+ if (key.byteLength > blockSize) {
+ key = digest(key);
+ }
+ if (key.byteLength < blockSize) {
+ const k = key;
+ key = new Uint8Array(blockSize);
+ key.set(k, 0);
+ }
+ const okp = new Uint8Array(blockSize);
+ const ikp = new Uint8Array(blockSize);
+ for (let i = 0; i < blockSize; i++) {
+ ikp[i] = key[i] ^ 0x36;
+ okp[i] = key[i] ^ 0x5c;
+ }
+ const b1 = new Uint8Array(blockSize + message.byteLength);
+ b1.set(ikp, 0);
+ b1.set(message, blockSize);
+ const h0 = digest(b1);
+ const b2 = new Uint8Array(blockSize + h0.length);
+ b2.set(okp, 0);
+ b2.set(h0, blockSize);
+ return digest(b2);
+}
+
+export function hmacSha512(key: Uint8Array, message: Uint8Array): Uint8Array {
+ return hmac(sha512, 128, key, message);
+}
+
+export function hmacSha256(key: Uint8Array, message: Uint8Array): Uint8Array {
+ return hmac(sha256, 64, key, message);
+}
+
+export function kdf(
+ outputLength: number,
+ ikm: Uint8Array,
+ salt: Uint8Array,
+ info: Uint8Array,
+): Uint8Array {
+ // extract
+ const prk = hmacSha512(salt, ikm);
+
+ // expand
+ const N = Math.ceil(outputLength / 32);
+ const output = new Uint8Array(N * 32);
+ for (let i = 0; i < N; i++) {
+ let buf;
+ if (i == 0) {
+ buf = new Uint8Array(info.byteLength + 1);
+ buf.set(info, 0);
+ } else {
+ buf = new Uint8Array(info.byteLength + 1 + 32);
+ for (let j = 0; j < 32; j++) {
+ buf[j] = output[(i - 1) * 32 + j];
+ }
+ buf.set(info, 32);
+ }
+ buf[buf.length - 1] = i + 1;
+ const chunk = hmacSha256(prk, buf);
+ output.set(chunk, i * 32);
+ }
+
+ return output.slice(0, outputLength);
+}
diff --git a/packages/taler-wallet-core/src/crypto/primitives/nacl-fast.d.ts.map b/packages/taler-wallet-core/src/crypto/primitives/nacl-fast.d.ts.map
new file mode 100644
index 000000000..6dab0be11
--- /dev/null
+++ b/packages/taler-wallet-core/src/crypto/primitives/nacl-fast.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"nacl-fast.d.ts","sourceRoot":"","sources":["nacl-fast.ts"],"names":[],"mappings":"AA8zCA;;GAEG;AACH,qBAAa,SAAS;IACpB,OAAO,CAAC,EAAE,CAAqB;IAC/B,OAAO,CAAC,EAAE,CAAqB;IAE/B,OAAO,CAAC,IAAI,CAAuB;IACnC,OAAO,CAAC,CAAC,CAAK;IACd,OAAO,CAAC,KAAK,CAAK;;IAsBlB,MAAM,CAAC,IAAI,EAAE,UAAU,GAAG,SAAS;IAuBnC,MAAM,IAAI,UAAU;CAgBrB;AAqTD,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,UAAU,CAIjD;AAED,wBAAgB,UAAU,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,UAAU,GAAG,UAAU,CAOnE;AAED,wBAAgB,eAAe,CAAC,CAAC,EAAE,UAAU,GAAG,UAAU,CAMzD;AAED,eAAO,MAAM,uBAAuB,KAAgC,CAAC;AACrE,eAAO,MAAM,6BAA6B,KAA0B,CAAC;AAErE,wBAAgB,IAAI,CAAC,GAAG,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,GAAG,UAAU,CAOvE;AAED,wBAAgB,SAAS,CACvB,SAAS,EAAE,UAAU,EACrB,SAAS,EAAE,UAAU,GACpB,UAAU,GAAG,IAAI,CAUnB;AAED,wBAAgB,aAAa,CAC3B,GAAG,EAAE,UAAU,EACf,SAAS,EAAE,UAAU,GACpB,UAAU,CAKZ;AAED,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,UAAU,EACf,GAAG,EAAE,UAAU,EACf,SAAS,EAAE,UAAU,GACpB,OAAO,CAWT;AAED,wBAAgB,YAAY,IAAI;IAC9B,SAAS,EAAE,UAAU,CAAC;IACtB,SAAS,EAAE,UAAU,CAAC;CACvB,CAKA;AAED,wBAAgB,oCAAoC,CAClD,SAAS,EAAE,UAAU,GACpB,UAAU,CAmBZ;AAED,wBAAgB,0BAA0B,CACxC,SAAS,EAAE,UAAU,GACpB;IACD,SAAS,EAAE,UAAU,CAAC;IACtB,SAAS,EAAE,UAAU,CAAC;CACvB,CAOA;AAED,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,UAAU,GACf;IACD,SAAS,EAAE,UAAU,CAAC;IACtB,SAAS,EAAE,UAAU,CAAC;CACvB,CAQA;AAED,eAAO,MAAM,oBAAoB,KAA6B,CAAC;AAC/D,eAAO,MAAM,oBAAoB,KAA6B,CAAC;AAC/D,eAAO,MAAM,eAAe,KAAwB,CAAC;AACrD,eAAO,MAAM,oBAAoB,KAAoB,CAAC;AAEtD,wBAAgB,IAAI,CAAC,GAAG,EAAE,UAAU,GAAG,UAAU,CAKhD;AAED,eAAO,MAAM,eAAe,KAAoB,CAAC;AAEjD,wBAAgB,MAAM,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,UAAU,GAAG,OAAO,CAM5D;AAED,wBAAgB,OAAO,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAEpE;AAED,wBAAgB,6BAA6B,CAC3C,UAAU,EAAE,UAAU,GACrB,UAAU,CAqBZ"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/crypto/primitives/nacl-fast.ts b/packages/taler-wallet-core/src/crypto/primitives/nacl-fast.ts
new file mode 100644
index 000000000..c2d40691a
--- /dev/null
+++ b/packages/taler-wallet-core/src/crypto/primitives/nacl-fast.ts
@@ -0,0 +1,1941 @@
+// Ported in 2014 by Dmitry Chestnykh and Devi Mandiri.
+// TypeScript port in 2019 by Florian Dold.
+// Public domain.
+//
+// Implementation derived from TweetNaCl version 20140427.
+// See for details: http://tweetnacl.cr.yp.to/
+
+const gf = function (init: number[] = []): Float64Array {
+ const r = new Float64Array(16);
+ if (init) for (let i = 0; i < init.length; i++) r[i] = init[i];
+ return r;
+};
+
+// Pluggable, initialized in high-level API below.
+let randombytes = function (x: Uint8Array, n: number): void {
+ throw new Error("no PRNG");
+};
+
+const _9 = new Uint8Array(32);
+_9[0] = 9;
+
+// prettier-ignore
+const gf0 = gf();
+const gf1 = gf([1]);
+const _121665 = gf([0xdb41, 1]);
+const D = gf([
+ 0x78a3,
+ 0x1359,
+ 0x4dca,
+ 0x75eb,
+ 0xd8ab,
+ 0x4141,
+ 0x0a4d,
+ 0x0070,
+ 0xe898,
+ 0x7779,
+ 0x4079,
+ 0x8cc7,
+ 0xfe73,
+ 0x2b6f,
+ 0x6cee,
+ 0x5203,
+]);
+const D2 = gf([
+ 0xf159,
+ 0x26b2,
+ 0x9b94,
+ 0xebd6,
+ 0xb156,
+ 0x8283,
+ 0x149a,
+ 0x00e0,
+ 0xd130,
+ 0xeef3,
+ 0x80f2,
+ 0x198e,
+ 0xfce7,
+ 0x56df,
+ 0xd9dc,
+ 0x2406,
+]);
+const X = gf([
+ 0xd51a,
+ 0x8f25,
+ 0x2d60,
+ 0xc956,
+ 0xa7b2,
+ 0x9525,
+ 0xc760,
+ 0x692c,
+ 0xdc5c,
+ 0xfdd6,
+ 0xe231,
+ 0xc0a4,
+ 0x53fe,
+ 0xcd6e,
+ 0x36d3,
+ 0x2169,
+]);
+const Y = gf([
+ 0x6658,
+ 0x6666,
+ 0x6666,
+ 0x6666,
+ 0x6666,
+ 0x6666,
+ 0x6666,
+ 0x6666,
+ 0x6666,
+ 0x6666,
+ 0x6666,
+ 0x6666,
+ 0x6666,
+ 0x6666,
+ 0x6666,
+ 0x6666,
+]);
+const I = gf([
+ 0xa0b0,
+ 0x4a0e,
+ 0x1b27,
+ 0xc4ee,
+ 0xe478,
+ 0xad2f,
+ 0x1806,
+ 0x2f43,
+ 0xd7a7,
+ 0x3dfb,
+ 0x0099,
+ 0x2b4d,
+ 0xdf0b,
+ 0x4fc1,
+ 0x2480,
+ 0x2b83,
+]);
+
+function ts64(x: Uint8Array, i: number, h: number, l: number): void {
+ x[i] = (h >> 24) & 0xff;
+ x[i + 1] = (h >> 16) & 0xff;
+ x[i + 2] = (h >> 8) & 0xff;
+ x[i + 3] = h & 0xff;
+ x[i + 4] = (l >> 24) & 0xff;
+ x[i + 5] = (l >> 16) & 0xff;
+ x[i + 6] = (l >> 8) & 0xff;
+ x[i + 7] = l & 0xff;
+}
+
+function vn(
+ x: Uint8Array,
+ xi: number,
+ y: Uint8Array,
+ yi: number,
+ n: number,
+): number {
+ let i,
+ d = 0;
+ for (i = 0; i < n; i++) d |= x[xi + i] ^ y[yi + i];
+ return (1 & ((d - 1) >>> 8)) - 1;
+}
+
+function crypto_verify_32(
+ x: Uint8Array,
+ xi: number,
+ y: Uint8Array,
+ yi: number,
+): number {
+ return vn(x, xi, y, yi, 32);
+}
+
+function set25519(r: Float64Array, a: Float64Array): void {
+ let i;
+ for (i = 0; i < 16; i++) r[i] = a[i] | 0;
+}
+
+function car25519(o: Float64Array): void {
+ let i,
+ v,
+ c = 1;
+ for (i = 0; i < 16; i++) {
+ v = o[i] + c + 65535;
+ c = Math.floor(v / 65536);
+ o[i] = v - c * 65536;
+ }
+ o[0] += c - 1 + 37 * (c - 1);
+}
+
+function sel25519(p: Float64Array, q: Float64Array, b: number): void {
+ let t;
+ const c = ~(b - 1);
+ for (let i = 0; i < 16; i++) {
+ t = c & (p[i] ^ q[i]);
+ p[i] ^= t;
+ q[i] ^= t;
+ }
+}
+
+function pack25519(o: Uint8Array, n: Float64Array): void {
+ let i, j, b;
+ const m = gf(),
+ t = gf();
+ for (i = 0; i < 16; i++) t[i] = n[i];
+ car25519(t);
+ car25519(t);
+ car25519(t);
+ for (j = 0; j < 2; j++) {
+ m[0] = t[0] - 0xffed;
+ for (i = 1; i < 15; i++) {
+ m[i] = t[i] - 0xffff - ((m[i - 1] >> 16) & 1);
+ m[i - 1] &= 0xffff;
+ }
+ m[15] = t[15] - 0x7fff - ((m[14] >> 16) & 1);
+ b = (m[15] >> 16) & 1;
+ m[14] &= 0xffff;
+ sel25519(t, m, 1 - b);
+ }
+ for (i = 0; i < 16; i++) {
+ o[2 * i] = t[i] & 0xff;
+ o[2 * i + 1] = t[i] >> 8;
+ }
+}
+
+function neq25519(a: Float64Array, b: Float64Array): number {
+ const c = new Uint8Array(32),
+ d = new Uint8Array(32);
+ pack25519(c, a);
+ pack25519(d, b);
+ return crypto_verify_32(c, 0, d, 0);
+}
+
+function par25519(a: Float64Array): number {
+ const d = new Uint8Array(32);
+ pack25519(d, a);
+ return d[0] & 1;
+}
+
+function unpack25519(o: Float64Array, n: Uint8Array): void {
+ let i;
+ for (i = 0; i < 16; i++) o[i] = n[2 * i] + (n[2 * i + 1] << 8);
+ o[15] &= 0x7fff;
+}
+
+function A(o: Float64Array, a: Float64Array, b: Float64Array): void {
+ for (let i = 0; i < 16; i++) o[i] = a[i] + b[i];
+}
+
+function Z(o: Float64Array, a: Float64Array, b: Float64Array): void {
+ for (let i = 0; i < 16; i++) o[i] = a[i] - b[i];
+}
+
+function M(o: Float64Array, a: Float64Array, b: Float64Array): void {
+ let v,
+ c,
+ t0 = 0,
+ t1 = 0,
+ t2 = 0,
+ t3 = 0,
+ t4 = 0,
+ t5 = 0,
+ t6 = 0,
+ t7 = 0,
+ t8 = 0,
+ t9 = 0,
+ t10 = 0,
+ t11 = 0,
+ t12 = 0,
+ t13 = 0,
+ t14 = 0,
+ t15 = 0,
+ t16 = 0,
+ t17 = 0,
+ t18 = 0,
+ t19 = 0,
+ t20 = 0,
+ t21 = 0,
+ t22 = 0,
+ t23 = 0,
+ t24 = 0,
+ t25 = 0,
+ t26 = 0,
+ t27 = 0,
+ t28 = 0,
+ t29 = 0,
+ t30 = 0;
+ const b0 = b[0],
+ b1 = b[1],
+ b2 = b[2],
+ b3 = b[3],
+ b4 = b[4],
+ b5 = b[5],
+ b6 = b[6],
+ b7 = b[7],
+ b8 = b[8],
+ b9 = b[9],
+ b10 = b[10],
+ b11 = b[11],
+ b12 = b[12],
+ b13 = b[13],
+ b14 = b[14],
+ b15 = b[15];
+
+ v = a[0];
+ t0 += v * b0;
+ t1 += v * b1;
+ t2 += v * b2;
+ t3 += v * b3;
+ t4 += v * b4;
+ t5 += v * b5;
+ t6 += v * b6;
+ t7 += v * b7;
+ t8 += v * b8;
+ t9 += v * b9;
+ t10 += v * b10;
+ t11 += v * b11;
+ t12 += v * b12;
+ t13 += v * b13;
+ t14 += v * b14;
+ t15 += v * b15;
+ v = a[1];
+ t1 += v * b0;
+ t2 += v * b1;
+ t3 += v * b2;
+ t4 += v * b3;
+ t5 += v * b4;
+ t6 += v * b5;
+ t7 += v * b6;
+ t8 += v * b7;
+ t9 += v * b8;
+ t10 += v * b9;
+ t11 += v * b10;
+ t12 += v * b11;
+ t13 += v * b12;
+ t14 += v * b13;
+ t15 += v * b14;
+ t16 += v * b15;
+ v = a[2];
+ t2 += v * b0;
+ t3 += v * b1;
+ t4 += v * b2;
+ t5 += v * b3;
+ t6 += v * b4;
+ t7 += v * b5;
+ t8 += v * b6;
+ t9 += v * b7;
+ t10 += v * b8;
+ t11 += v * b9;
+ t12 += v * b10;
+ t13 += v * b11;
+ t14 += v * b12;
+ t15 += v * b13;
+ t16 += v * b14;
+ t17 += v * b15;
+ v = a[3];
+ t3 += v * b0;
+ t4 += v * b1;
+ t5 += v * b2;
+ t6 += v * b3;
+ t7 += v * b4;
+ t8 += v * b5;
+ t9 += v * b6;
+ t10 += v * b7;
+ t11 += v * b8;
+ t12 += v * b9;
+ t13 += v * b10;
+ t14 += v * b11;
+ t15 += v * b12;
+ t16 += v * b13;
+ t17 += v * b14;
+ t18 += v * b15;
+ v = a[4];
+ t4 += v * b0;
+ t5 += v * b1;
+ t6 += v * b2;
+ t7 += v * b3;
+ t8 += v * b4;
+ t9 += v * b5;
+ t10 += v * b6;
+ t11 += v * b7;
+ t12 += v * b8;
+ t13 += v * b9;
+ t14 += v * b10;
+ t15 += v * b11;
+ t16 += v * b12;
+ t17 += v * b13;
+ t18 += v * b14;
+ t19 += v * b15;
+ v = a[5];
+ t5 += v * b0;
+ t6 += v * b1;
+ t7 += v * b2;
+ t8 += v * b3;
+ t9 += v * b4;
+ t10 += v * b5;
+ t11 += v * b6;
+ t12 += v * b7;
+ t13 += v * b8;
+ t14 += v * b9;
+ t15 += v * b10;
+ t16 += v * b11;
+ t17 += v * b12;
+ t18 += v * b13;
+ t19 += v * b14;
+ t20 += v * b15;
+ v = a[6];
+ t6 += v * b0;
+ t7 += v * b1;
+ t8 += v * b2;
+ t9 += v * b3;
+ t10 += v * b4;
+ t11 += v * b5;
+ t12 += v * b6;
+ t13 += v * b7;
+ t14 += v * b8;
+ t15 += v * b9;
+ t16 += v * b10;
+ t17 += v * b11;
+ t18 += v * b12;
+ t19 += v * b13;
+ t20 += v * b14;
+ t21 += v * b15;
+ v = a[7];
+ t7 += v * b0;
+ t8 += v * b1;
+ t9 += v * b2;
+ t10 += v * b3;
+ t11 += v * b4;
+ t12 += v * b5;
+ t13 += v * b6;
+ t14 += v * b7;
+ t15 += v * b8;
+ t16 += v * b9;
+ t17 += v * b10;
+ t18 += v * b11;
+ t19 += v * b12;
+ t20 += v * b13;
+ t21 += v * b14;
+ t22 += v * b15;
+ v = a[8];
+ t8 += v * b0;
+ t9 += v * b1;
+ t10 += v * b2;
+ t11 += v * b3;
+ t12 += v * b4;
+ t13 += v * b5;
+ t14 += v * b6;
+ t15 += v * b7;
+ t16 += v * b8;
+ t17 += v * b9;
+ t18 += v * b10;
+ t19 += v * b11;
+ t20 += v * b12;
+ t21 += v * b13;
+ t22 += v * b14;
+ t23 += v * b15;
+ v = a[9];
+ t9 += v * b0;
+ t10 += v * b1;
+ t11 += v * b2;
+ t12 += v * b3;
+ t13 += v * b4;
+ t14 += v * b5;
+ t15 += v * b6;
+ t16 += v * b7;
+ t17 += v * b8;
+ t18 += v * b9;
+ t19 += v * b10;
+ t20 += v * b11;
+ t21 += v * b12;
+ t22 += v * b13;
+ t23 += v * b14;
+ t24 += v * b15;
+ v = a[10];
+ t10 += v * b0;
+ t11 += v * b1;
+ t12 += v * b2;
+ t13 += v * b3;
+ t14 += v * b4;
+ t15 += v * b5;
+ t16 += v * b6;
+ t17 += v * b7;
+ t18 += v * b8;
+ t19 += v * b9;
+ t20 += v * b10;
+ t21 += v * b11;
+ t22 += v * b12;
+ t23 += v * b13;
+ t24 += v * b14;
+ t25 += v * b15;
+ v = a[11];
+ t11 += v * b0;
+ t12 += v * b1;
+ t13 += v * b2;
+ t14 += v * b3;
+ t15 += v * b4;
+ t16 += v * b5;
+ t17 += v * b6;
+ t18 += v * b7;
+ t19 += v * b8;
+ t20 += v * b9;
+ t21 += v * b10;
+ t22 += v * b11;
+ t23 += v * b12;
+ t24 += v * b13;
+ t25 += v * b14;
+ t26 += v * b15;
+ v = a[12];
+ t12 += v * b0;
+ t13 += v * b1;
+ t14 += v * b2;
+ t15 += v * b3;
+ t16 += v * b4;
+ t17 += v * b5;
+ t18 += v * b6;
+ t19 += v * b7;
+ t20 += v * b8;
+ t21 += v * b9;
+ t22 += v * b10;
+ t23 += v * b11;
+ t24 += v * b12;
+ t25 += v * b13;
+ t26 += v * b14;
+ t27 += v * b15;
+ v = a[13];
+ t13 += v * b0;
+ t14 += v * b1;
+ t15 += v * b2;
+ t16 += v * b3;
+ t17 += v * b4;
+ t18 += v * b5;
+ t19 += v * b6;
+ t20 += v * b7;
+ t21 += v * b8;
+ t22 += v * b9;
+ t23 += v * b10;
+ t24 += v * b11;
+ t25 += v * b12;
+ t26 += v * b13;
+ t27 += v * b14;
+ t28 += v * b15;
+ v = a[14];
+ t14 += v * b0;
+ t15 += v * b1;
+ t16 += v * b2;
+ t17 += v * b3;
+ t18 += v * b4;
+ t19 += v * b5;
+ t20 += v * b6;
+ t21 += v * b7;
+ t22 += v * b8;
+ t23 += v * b9;
+ t24 += v * b10;
+ t25 += v * b11;
+ t26 += v * b12;
+ t27 += v * b13;
+ t28 += v * b14;
+ t29 += v * b15;
+ v = a[15];
+ t15 += v * b0;
+ t16 += v * b1;
+ t17 += v * b2;
+ t18 += v * b3;
+ t19 += v * b4;
+ t20 += v * b5;
+ t21 += v * b6;
+ t22 += v * b7;
+ t23 += v * b8;
+ t24 += v * b9;
+ t25 += v * b10;
+ t26 += v * b11;
+ t27 += v * b12;
+ t28 += v * b13;
+ t29 += v * b14;
+ t30 += v * b15;
+
+ t0 += 38 * t16;
+ t1 += 38 * t17;
+ t2 += 38 * t18;
+ t3 += 38 * t19;
+ t4 += 38 * t20;
+ t5 += 38 * t21;
+ t6 += 38 * t22;
+ t7 += 38 * t23;
+ t8 += 38 * t24;
+ t9 += 38 * t25;
+ t10 += 38 * t26;
+ t11 += 38 * t27;
+ t12 += 38 * t28;
+ t13 += 38 * t29;
+ t14 += 38 * t30;
+ // t15 left as is
+
+ // first car
+ c = 1;
+ v = t0 + c + 65535;
+ c = Math.floor(v / 65536);
+ t0 = v - c * 65536;
+ v = t1 + c + 65535;
+ c = Math.floor(v / 65536);
+ t1 = v - c * 65536;
+ v = t2 + c + 65535;
+ c = Math.floor(v / 65536);
+ t2 = v - c * 65536;
+ v = t3 + c + 65535;
+ c = Math.floor(v / 65536);
+ t3 = v - c * 65536;
+ v = t4 + c + 65535;
+ c = Math.floor(v / 65536);
+ t4 = v - c * 65536;
+ v = t5 + c + 65535;
+ c = Math.floor(v / 65536);
+ t5 = v - c * 65536;
+ v = t6 + c + 65535;
+ c = Math.floor(v / 65536);
+ t6 = v - c * 65536;
+ v = t7 + c + 65535;
+ c = Math.floor(v / 65536);
+ t7 = v - c * 65536;
+ v = t8 + c + 65535;
+ c = Math.floor(v / 65536);
+ t8 = v - c * 65536;
+ v = t9 + c + 65535;
+ c = Math.floor(v / 65536);
+ t9 = v - c * 65536;
+ v = t10 + c + 65535;
+ c = Math.floor(v / 65536);
+ t10 = v - c * 65536;
+ v = t11 + c + 65535;
+ c = Math.floor(v / 65536);
+ t11 = v - c * 65536;
+ v = t12 + c + 65535;
+ c = Math.floor(v / 65536);
+ t12 = v - c * 65536;
+ v = t13 + c + 65535;
+ c = Math.floor(v / 65536);
+ t13 = v - c * 65536;
+ v = t14 + c + 65535;
+ c = Math.floor(v / 65536);
+ t14 = v - c * 65536;
+ v = t15 + c + 65535;
+ c = Math.floor(v / 65536);
+ t15 = v - c * 65536;
+ t0 += c - 1 + 37 * (c - 1);
+
+ // second car
+ c = 1;
+ v = t0 + c + 65535;
+ c = Math.floor(v / 65536);
+ t0 = v - c * 65536;
+ v = t1 + c + 65535;
+ c = Math.floor(v / 65536);
+ t1 = v - c * 65536;
+ v = t2 + c + 65535;
+ c = Math.floor(v / 65536);
+ t2 = v - c * 65536;
+ v = t3 + c + 65535;
+ c = Math.floor(v / 65536);
+ t3 = v - c * 65536;
+ v = t4 + c + 65535;
+ c = Math.floor(v / 65536);
+ t4 = v - c * 65536;
+ v = t5 + c + 65535;
+ c = Math.floor(v / 65536);
+ t5 = v - c * 65536;
+ v = t6 + c + 65535;
+ c = Math.floor(v / 65536);
+ t6 = v - c * 65536;
+ v = t7 + c + 65535;
+ c = Math.floor(v / 65536);
+ t7 = v - c * 65536;
+ v = t8 + c + 65535;
+ c = Math.floor(v / 65536);
+ t8 = v - c * 65536;
+ v = t9 + c + 65535;
+ c = Math.floor(v / 65536);
+ t9 = v - c * 65536;
+ v = t10 + c + 65535;
+ c = Math.floor(v / 65536);
+ t10 = v - c * 65536;
+ v = t11 + c + 65535;
+ c = Math.floor(v / 65536);
+ t11 = v - c * 65536;
+ v = t12 + c + 65535;
+ c = Math.floor(v / 65536);
+ t12 = v - c * 65536;
+ v = t13 + c + 65535;
+ c = Math.floor(v / 65536);
+ t13 = v - c * 65536;
+ v = t14 + c + 65535;
+ c = Math.floor(v / 65536);
+ t14 = v - c * 65536;
+ v = t15 + c + 65535;
+ c = Math.floor(v / 65536);
+ t15 = v - c * 65536;
+ t0 += c - 1 + 37 * (c - 1);
+
+ o[0] = t0;
+ o[1] = t1;
+ o[2] = t2;
+ o[3] = t3;
+ o[4] = t4;
+ o[5] = t5;
+ o[6] = t6;
+ o[7] = t7;
+ o[8] = t8;
+ o[9] = t9;
+ o[10] = t10;
+ o[11] = t11;
+ o[12] = t12;
+ o[13] = t13;
+ o[14] = t14;
+ o[15] = t15;
+}
+
+function S(o: Float64Array, a: Float64Array): void {
+ M(o, a, a);
+}
+
+function inv25519(o: Float64Array, i: Float64Array): void {
+ const c = gf();
+ let a;
+ for (a = 0; a < 16; a++) c[a] = i[a];
+ for (a = 253; a >= 0; a--) {
+ S(c, c);
+ if (a !== 2 && a !== 4) M(c, c, i);
+ }
+ for (a = 0; a < 16; a++) o[a] = c[a];
+}
+
+function pow2523(o: Float64Array, i: Float64Array): void {
+ const c = gf();
+ let a;
+ for (a = 0; a < 16; a++) c[a] = i[a];
+ for (a = 250; a >= 0; a--) {
+ S(c, c);
+ if (a !== 1) M(c, c, i);
+ }
+ for (a = 0; a < 16; a++) o[a] = c[a];
+}
+
+function crypto_scalarmult(
+ q: Uint8Array,
+ n: Uint8Array,
+ p: Uint8Array,
+): number {
+ const z = new Uint8Array(32);
+ const x = new Float64Array(80);
+ let r;
+ let i;
+ const a = gf(),
+ b = gf(),
+ c = gf(),
+ d = gf(),
+ e = gf(),
+ f = gf();
+ for (i = 0; i < 31; i++) z[i] = n[i];
+ z[31] = (n[31] & 127) | 64;
+ z[0] &= 248;
+ unpack25519(x, p);
+ for (i = 0; i < 16; i++) {
+ b[i] = x[i];
+ d[i] = a[i] = c[i] = 0;
+ }
+ a[0] = d[0] = 1;
+ for (i = 254; i >= 0; --i) {
+ r = (z[i >>> 3] >>> (i & 7)) & 1;
+ sel25519(a, b, r);
+ sel25519(c, d, r);
+ A(e, a, c);
+ Z(a, a, c);
+ A(c, b, d);
+ Z(b, b, d);
+ S(d, e);
+ S(f, a);
+ M(a, c, a);
+ M(c, b, e);
+ A(e, a, c);
+ Z(a, a, c);
+ S(b, a);
+ Z(c, d, f);
+ M(a, c, _121665);
+ A(a, a, d);
+ M(c, c, a);
+ M(a, d, f);
+ M(d, b, x);
+ S(b, e);
+ sel25519(a, b, r);
+ sel25519(c, d, r);
+ }
+ for (i = 0; i < 16; i++) {
+ x[i + 16] = a[i];
+ x[i + 32] = c[i];
+ x[i + 48] = b[i];
+ x[i + 64] = d[i];
+ }
+ const x32 = x.subarray(32);
+ const x16 = x.subarray(16);
+ inv25519(x32, x32);
+ M(x16, x16, x32);
+ pack25519(q, x16);
+ return 0;
+}
+
+function crypto_scalarmult_base(q: Uint8Array, n: Uint8Array): number {
+ return crypto_scalarmult(q, n, _9);
+}
+
+// prettier-ignore
+const K = [
+ 0x428a2f98, 0xd728ae22, 0x71374491, 0x23ef65cd,
+ 0xb5c0fbcf, 0xec4d3b2f, 0xe9b5dba5, 0x8189dbbc,
+ 0x3956c25b, 0xf348b538, 0x59f111f1, 0xb605d019,
+ 0x923f82a4, 0xaf194f9b, 0xab1c5ed5, 0xda6d8118,
+ 0xd807aa98, 0xa3030242, 0x12835b01, 0x45706fbe,
+ 0x243185be, 0x4ee4b28c, 0x550c7dc3, 0xd5ffb4e2,
+ 0x72be5d74, 0xf27b896f, 0x80deb1fe, 0x3b1696b1,
+ 0x9bdc06a7, 0x25c71235, 0xc19bf174, 0xcf692694,
+ 0xe49b69c1, 0x9ef14ad2, 0xefbe4786, 0x384f25e3,
+ 0x0fc19dc6, 0x8b8cd5b5, 0x240ca1cc, 0x77ac9c65,
+ 0x2de92c6f, 0x592b0275, 0x4a7484aa, 0x6ea6e483,
+ 0x5cb0a9dc, 0xbd41fbd4, 0x76f988da, 0x831153b5,
+ 0x983e5152, 0xee66dfab, 0xa831c66d, 0x2db43210,
+ 0xb00327c8, 0x98fb213f, 0xbf597fc7, 0xbeef0ee4,
+ 0xc6e00bf3, 0x3da88fc2, 0xd5a79147, 0x930aa725,
+ 0x06ca6351, 0xe003826f, 0x14292967, 0x0a0e6e70,
+ 0x27b70a85, 0x46d22ffc, 0x2e1b2138, 0x5c26c926,
+ 0x4d2c6dfc, 0x5ac42aed, 0x53380d13, 0x9d95b3df,
+ 0x650a7354, 0x8baf63de, 0x766a0abb, 0x3c77b2a8,
+ 0x81c2c92e, 0x47edaee6, 0x92722c85, 0x1482353b,
+ 0xa2bfe8a1, 0x4cf10364, 0xa81a664b, 0xbc423001,
+ 0xc24b8b70, 0xd0f89791, 0xc76c51a3, 0x0654be30,
+ 0xd192e819, 0xd6ef5218, 0xd6990624, 0x5565a910,
+ 0xf40e3585, 0x5771202a, 0x106aa070, 0x32bbd1b8,
+ 0x19a4c116, 0xb8d2d0c8, 0x1e376c08, 0x5141ab53,
+ 0x2748774c, 0xdf8eeb99, 0x34b0bcb5, 0xe19b48a8,
+ 0x391c0cb3, 0xc5c95a63, 0x4ed8aa4a, 0xe3418acb,
+ 0x5b9cca4f, 0x7763e373, 0x682e6ff3, 0xd6b2b8a3,
+ 0x748f82ee, 0x5defb2fc, 0x78a5636f, 0x43172f60,
+ 0x84c87814, 0xa1f0ab72, 0x8cc70208, 0x1a6439ec,
+ 0x90befffa, 0x23631e28, 0xa4506ceb, 0xde82bde9,
+ 0xbef9a3f7, 0xb2c67915, 0xc67178f2, 0xe372532b,
+ 0xca273ece, 0xea26619c, 0xd186b8c7, 0x21c0c207,
+ 0xeada7dd6, 0xcde0eb1e, 0xf57d4f7f, 0xee6ed178,
+ 0x06f067aa, 0x72176fba, 0x0a637dc5, 0xa2c898a6,
+ 0x113f9804, 0xbef90dae, 0x1b710b35, 0x131c471b,
+ 0x28db77f5, 0x23047d84, 0x32caab7b, 0x40c72493,
+ 0x3c9ebe0a, 0x15c9bebc, 0x431d67c4, 0x9c100d4c,
+ 0x4cc5d4be, 0xcb3e42b6, 0x597f299c, 0xfc657e2a,
+ 0x5fcb6fab, 0x3ad6faec, 0x6c44198c, 0x4a475817
+];
+
+function crypto_hashblocks_hl(
+ hh: Int32Array,
+ hl: Int32Array,
+ m: Uint8Array,
+ n: number,
+): number {
+ const wh = new Int32Array(16),
+ wl = new Int32Array(16);
+ let bh0,
+ bh1,
+ bh2,
+ bh3,
+ bh4,
+ bh5,
+ bh6,
+ bh7,
+ bl0,
+ bl1,
+ bl2,
+ bl3,
+ bl4,
+ bl5,
+ bl6,
+ bl7,
+ th,
+ tl,
+ i,
+ j,
+ h,
+ l,
+ a,
+ b,
+ c,
+ d;
+
+ let ah0 = hh[0],
+ ah1 = hh[1],
+ ah2 = hh[2],
+ ah3 = hh[3],
+ ah4 = hh[4],
+ ah5 = hh[5],
+ ah6 = hh[6],
+ ah7 = hh[7],
+ al0 = hl[0],
+ al1 = hl[1],
+ al2 = hl[2],
+ al3 = hl[3],
+ al4 = hl[4],
+ al5 = hl[5],
+ al6 = hl[6],
+ al7 = hl[7];
+
+ let pos = 0;
+ while (n >= 128) {
+ for (i = 0; i < 16; i++) {
+ j = 8 * i + pos;
+ wh[i] = (m[j + 0] << 24) | (m[j + 1] << 16) | (m[j + 2] << 8) | m[j + 3];
+ wl[i] = (m[j + 4] << 24) | (m[j + 5] << 16) | (m[j + 6] << 8) | m[j + 7];
+ }
+ for (i = 0; i < 80; i++) {
+ bh0 = ah0;
+ bh1 = ah1;
+ bh2 = ah2;
+ bh3 = ah3;
+ bh4 = ah4;
+ bh5 = ah5;
+ bh6 = ah6;
+ bh7 = ah7;
+
+ bl0 = al0;
+ bl1 = al1;
+ bl2 = al2;
+ bl3 = al3;
+ bl4 = al4;
+ bl5 = al5;
+ bl6 = al6;
+ bl7 = al7;
+
+ // add
+ h = ah7;
+ l = al7;
+
+ a = l & 0xffff;
+ b = l >>> 16;
+ c = h & 0xffff;
+ d = h >>> 16;
+
+ // Sigma1
+ h =
+ ((ah4 >>> 14) | (al4 << (32 - 14))) ^
+ ((ah4 >>> 18) | (al4 << (32 - 18))) ^
+ ((al4 >>> (41 - 32)) | (ah4 << (32 - (41 - 32))));
+ l =
+ ((al4 >>> 14) | (ah4 << (32 - 14))) ^
+ ((al4 >>> 18) | (ah4 << (32 - 18))) ^
+ ((ah4 >>> (41 - 32)) | (al4 << (32 - (41 - 32))));
+
+ a += l & 0xffff;
+ b += l >>> 16;
+ c += h & 0xffff;
+ d += h >>> 16;
+
+ // Ch
+ h = (ah4 & ah5) ^ (~ah4 & ah6);
+ l = (al4 & al5) ^ (~al4 & al6);
+
+ a += l & 0xffff;
+ b += l >>> 16;
+ c += h & 0xffff;
+ d += h >>> 16;
+
+ // K
+ h = K[i * 2];
+ l = K[i * 2 + 1];
+
+ a += l & 0xffff;
+ b += l >>> 16;
+ c += h & 0xffff;
+ d += h >>> 16;
+
+ // w
+ h = wh[i % 16];
+ l = wl[i % 16];
+
+ a += l & 0xffff;
+ b += l >>> 16;
+ c += h & 0xffff;
+ d += h >>> 16;
+
+ b += a >>> 16;
+ c += b >>> 16;
+ d += c >>> 16;
+
+ th = (c & 0xffff) | (d << 16);
+ tl = (a & 0xffff) | (b << 16);
+
+ // add
+ h = th;
+ l = tl;
+
+ a = l & 0xffff;
+ b = l >>> 16;
+ c = h & 0xffff;
+ d = h >>> 16;
+
+ // Sigma0
+ h =
+ ((ah0 >>> 28) | (al0 << (32 - 28))) ^
+ ((al0 >>> (34 - 32)) | (ah0 << (32 - (34 - 32)))) ^
+ ((al0 >>> (39 - 32)) | (ah0 << (32 - (39 - 32))));
+ l =
+ ((al0 >>> 28) | (ah0 << (32 - 28))) ^
+ ((ah0 >>> (34 - 32)) | (al0 << (32 - (34 - 32)))) ^
+ ((ah0 >>> (39 - 32)) | (al0 << (32 - (39 - 32))));
+
+ a += l & 0xffff;
+ b += l >>> 16;
+ c += h & 0xffff;
+ d += h >>> 16;
+
+ // Maj
+ h = (ah0 & ah1) ^ (ah0 & ah2) ^ (ah1 & ah2);
+ l = (al0 & al1) ^ (al0 & al2) ^ (al1 & al2);
+
+ a += l & 0xffff;
+ b += l >>> 16;
+ c += h & 0xffff;
+ d += h >>> 16;
+
+ b += a >>> 16;
+ c += b >>> 16;
+ d += c >>> 16;
+
+ bh7 = (c & 0xffff) | (d << 16);
+ bl7 = (a & 0xffff) | (b << 16);
+
+ // add
+ h = bh3;
+ l = bl3;
+
+ a = l & 0xffff;
+ b = l >>> 16;
+ c = h & 0xffff;
+ d = h >>> 16;
+
+ h = th;
+ l = tl;
+
+ a += l & 0xffff;
+ b += l >>> 16;
+ c += h & 0xffff;
+ d += h >>> 16;
+
+ b += a >>> 16;
+ c += b >>> 16;
+ d += c >>> 16;
+
+ bh3 = (c & 0xffff) | (d << 16);
+ bl3 = (a & 0xffff) | (b << 16);
+
+ ah1 = bh0;
+ ah2 = bh1;
+ ah3 = bh2;
+ ah4 = bh3;
+ ah5 = bh4;
+ ah6 = bh5;
+ ah7 = bh6;
+ ah0 = bh7;
+
+ al1 = bl0;
+ al2 = bl1;
+ al3 = bl2;
+ al4 = bl3;
+ al5 = bl4;
+ al6 = bl5;
+ al7 = bl6;
+ al0 = bl7;
+
+ if (i % 16 === 15) {
+ for (j = 0; j < 16; j++) {
+ // add
+ h = wh[j];
+ l = wl[j];
+
+ a = l & 0xffff;
+ b = l >>> 16;
+ c = h & 0xffff;
+ d = h >>> 16;
+
+ h = wh[(j + 9) % 16];
+ l = wl[(j + 9) % 16];
+
+ a += l & 0xffff;
+ b += l >>> 16;
+ c += h & 0xffff;
+ d += h >>> 16;
+
+ // sigma0
+ th = wh[(j + 1) % 16];
+ tl = wl[(j + 1) % 16];
+ h =
+ ((th >>> 1) | (tl << (32 - 1))) ^
+ ((th >>> 8) | (tl << (32 - 8))) ^
+ (th >>> 7);
+ l =
+ ((tl >>> 1) | (th << (32 - 1))) ^
+ ((tl >>> 8) | (th << (32 - 8))) ^
+ ((tl >>> 7) | (th << (32 - 7)));
+
+ a += l & 0xffff;
+ b += l >>> 16;
+ c += h & 0xffff;
+ d += h >>> 16;
+
+ // sigma1
+ th = wh[(j + 14) % 16];
+ tl = wl[(j + 14) % 16];
+ h =
+ ((th >>> 19) | (tl << (32 - 19))) ^
+ ((tl >>> (61 - 32)) | (th << (32 - (61 - 32)))) ^
+ (th >>> 6);
+ l =
+ ((tl >>> 19) | (th << (32 - 19))) ^
+ ((th >>> (61 - 32)) | (tl << (32 - (61 - 32)))) ^
+ ((tl >>> 6) | (th << (32 - 6)));
+
+ a += l & 0xffff;
+ b += l >>> 16;
+ c += h & 0xffff;
+ d += h >>> 16;
+
+ b += a >>> 16;
+ c += b >>> 16;
+ d += c >>> 16;
+
+ wh[j] = (c & 0xffff) | (d << 16);
+ wl[j] = (a & 0xffff) | (b << 16);
+ }
+ }
+ }
+
+ // add
+ h = ah0;
+ l = al0;
+
+ a = l & 0xffff;
+ b = l >>> 16;
+ c = h & 0xffff;
+ d = h >>> 16;
+
+ h = hh[0];
+ l = hl[0];
+
+ a += l & 0xffff;
+ b += l >>> 16;
+ c += h & 0xffff;
+ d += h >>> 16;
+
+ b += a >>> 16;
+ c += b >>> 16;
+ d += c >>> 16;
+
+ hh[0] = ah0 = (c & 0xffff) | (d << 16);
+ hl[0] = al0 = (a & 0xffff) | (b << 16);
+
+ h = ah1;
+ l = al1;
+
+ a = l & 0xffff;
+ b = l >>> 16;
+ c = h & 0xffff;
+ d = h >>> 16;
+
+ h = hh[1];
+ l = hl[1];
+
+ a += l & 0xffff;
+ b += l >>> 16;
+ c += h & 0xffff;
+ d += h >>> 16;
+
+ b += a >>> 16;
+ c += b >>> 16;
+ d += c >>> 16;
+
+ hh[1] = ah1 = (c & 0xffff) | (d << 16);
+ hl[1] = al1 = (a & 0xffff) | (b << 16);
+
+ h = ah2;
+ l = al2;
+
+ a = l & 0xffff;
+ b = l >>> 16;
+ c = h & 0xffff;
+ d = h >>> 16;
+
+ h = hh[2];
+ l = hl[2];
+
+ a += l & 0xffff;
+ b += l >>> 16;
+ c += h & 0xffff;
+ d += h >>> 16;
+
+ b += a >>> 16;
+ c += b >>> 16;
+ d += c >>> 16;
+
+ hh[2] = ah2 = (c & 0xffff) | (d << 16);
+ hl[2] = al2 = (a & 0xffff) | (b << 16);
+
+ h = ah3;
+ l = al3;
+
+ a = l & 0xffff;
+ b = l >>> 16;
+ c = h & 0xffff;
+ d = h >>> 16;
+
+ h = hh[3];
+ l = hl[3];
+
+ a += l & 0xffff;
+ b += l >>> 16;
+ c += h & 0xffff;
+ d += h >>> 16;
+
+ b += a >>> 16;
+ c += b >>> 16;
+ d += c >>> 16;
+
+ hh[3] = ah3 = (c & 0xffff) | (d << 16);
+ hl[3] = al3 = (a & 0xffff) | (b << 16);
+
+ h = ah4;
+ l = al4;
+
+ a = l & 0xffff;
+ b = l >>> 16;
+ c = h & 0xffff;
+ d = h >>> 16;
+
+ h = hh[4];
+ l = hl[4];
+
+ a += l & 0xffff;
+ b += l >>> 16;
+ c += h & 0xffff;
+ d += h >>> 16;
+
+ b += a >>> 16;
+ c += b >>> 16;
+ d += c >>> 16;
+
+ hh[4] = ah4 = (c & 0xffff) | (d << 16);
+ hl[4] = al4 = (a & 0xffff) | (b << 16);
+
+ h = ah5;
+ l = al5;
+
+ a = l & 0xffff;
+ b = l >>> 16;
+ c = h & 0xffff;
+ d = h >>> 16;
+
+ h = hh[5];
+ l = hl[5];
+
+ a += l & 0xffff;
+ b += l >>> 16;
+ c += h & 0xffff;
+ d += h >>> 16;
+
+ b += a >>> 16;
+ c += b >>> 16;
+ d += c >>> 16;
+
+ hh[5] = ah5 = (c & 0xffff) | (d << 16);
+ hl[5] = al5 = (a & 0xffff) | (b << 16);
+
+ h = ah6;
+ l = al6;
+
+ a = l & 0xffff;
+ b = l >>> 16;
+ c = h & 0xffff;
+ d = h >>> 16;
+
+ h = hh[6];
+ l = hl[6];
+
+ a += l & 0xffff;
+ b += l >>> 16;
+ c += h & 0xffff;
+ d += h >>> 16;
+
+ b += a >>> 16;
+ c += b >>> 16;
+ d += c >>> 16;
+
+ hh[6] = ah6 = (c & 0xffff) | (d << 16);
+ hl[6] = al6 = (a & 0xffff) | (b << 16);
+
+ h = ah7;
+ l = al7;
+
+ a = l & 0xffff;
+ b = l >>> 16;
+ c = h & 0xffff;
+ d = h >>> 16;
+
+ h = hh[7];
+ l = hl[7];
+
+ a += l & 0xffff;
+ b += l >>> 16;
+ c += h & 0xffff;
+ d += h >>> 16;
+
+ b += a >>> 16;
+ c += b >>> 16;
+ d += c >>> 16;
+
+ hh[7] = ah7 = (c & 0xffff) | (d << 16);
+ hl[7] = al7 = (a & 0xffff) | (b << 16);
+
+ pos += 128;
+ n -= 128;
+ }
+
+ return n;
+}
+
+function crypto_hash(out: Uint8Array, m: Uint8Array, n: number): number {
+ const hh = new Int32Array(8);
+ const hl = new Int32Array(8);
+ const x = new Uint8Array(256);
+ const b = n;
+
+ hh[0] = 0x6a09e667;
+ hh[1] = 0xbb67ae85;
+ hh[2] = 0x3c6ef372;
+ hh[3] = 0xa54ff53a;
+ hh[4] = 0x510e527f;
+ hh[5] = 0x9b05688c;
+ hh[6] = 0x1f83d9ab;
+ hh[7] = 0x5be0cd19;
+
+ hl[0] = 0xf3bcc908;
+ hl[1] = 0x84caa73b;
+ hl[2] = 0xfe94f82b;
+ hl[3] = 0x5f1d36f1;
+ hl[4] = 0xade682d1;
+ hl[5] = 0x2b3e6c1f;
+ hl[6] = 0xfb41bd6b;
+ hl[7] = 0x137e2179;
+
+ crypto_hashblocks_hl(hh, hl, m, n);
+ n %= 128;
+
+ for (let i = 0; i < n; i++) x[i] = m[b - n + i];
+ x[n] = 128;
+
+ n = 256 - 128 * (n < 112 ? 1 : 0);
+ x[n - 9] = 0;
+ ts64(x, n - 8, (b / 0x20000000) | 0, b << 3);
+ crypto_hashblocks_hl(hh, hl, x, n);
+
+ for (let i = 0; i < 8; i++) ts64(out, 8 * i, hh[i], hl[i]);
+
+ return 0;
+}
+
+/**
+ * Incremental version of crypto_hash.
+ */
+export class HashState {
+ private hh = new Int32Array(8);
+ private hl = new Int32Array(8);
+
+ private next = new Uint8Array(128);
+ private p = 0;
+ private total = 0;
+
+ constructor() {
+ this.hh[0] = 0x6a09e667;
+ this.hh[1] = 0xbb67ae85;
+ this.hh[2] = 0x3c6ef372;
+ this.hh[3] = 0xa54ff53a;
+ this.hh[4] = 0x510e527f;
+ this.hh[5] = 0x9b05688c;
+ this.hh[6] = 0x1f83d9ab;
+ this.hh[7] = 0x5be0cd19;
+
+ this.hl[0] = 0xf3bcc908;
+ this.hl[1] = 0x84caa73b;
+ this.hl[2] = 0xfe94f82b;
+ this.hl[3] = 0x5f1d36f1;
+ this.hl[4] = 0xade682d1;
+ this.hl[5] = 0x2b3e6c1f;
+ this.hl[6] = 0xfb41bd6b;
+ this.hl[7] = 0x137e2179;
+ }
+
+ update(data: Uint8Array): HashState {
+ this.total += data.length;
+ let i = 0;
+ while (i < data.length) {
+ const r = 128 - this.p;
+ if (r > data.length - i) {
+ for (let j = 0; i + j < data.length; j++) {
+ this.next[this.p + j] = data[i + j];
+ }
+ this.p += data.length - i;
+ break;
+ } else {
+ for (let j = 0; this.p + j < 128; j++) {
+ this.next[this.p + j] = data[i + j];
+ }
+ crypto_hashblocks_hl(this.hh, this.hl, this.next, 128);
+ i += 128 - this.p;
+ this.p = 0;
+ }
+ }
+ return this;
+ }
+
+ finish(): Uint8Array {
+ const out = new Uint8Array(64);
+ let n = this.p;
+ const x = new Uint8Array(256);
+ const b = this.total;
+ for (let i = 0; i < n; i++) x[i] = this.next[i];
+ x[n] = 128;
+
+ n = 256 - 128 * (n < 112 ? 1 : 0);
+ x[n - 9] = 0;
+ ts64(x, n - 8, (b / 0x20000000) | 0, b << 3);
+ crypto_hashblocks_hl(this.hh, this.hl, x, n);
+
+ for (let i = 0; i < 8; i++) ts64(out, 8 * i, this.hh[i], this.hl[i]);
+ return out;
+ }
+}
+
+function add(p: Float64Array[], q: Float64Array[]): void {
+ const a = gf(),
+ b = gf(),
+ c = gf(),
+ d = gf(),
+ e = gf(),
+ f = gf(),
+ g = gf(),
+ h = gf(),
+ t = gf();
+
+ Z(a, p[1], p[0]);
+ Z(t, q[1], q[0]);
+ M(a, a, t);
+ A(b, p[0], p[1]);
+ A(t, q[0], q[1]);
+ M(b, b, t);
+ M(c, p[3], q[3]);
+ M(c, c, D2);
+ M(d, p[2], q[2]);
+ A(d, d, d);
+ Z(e, b, a);
+ Z(f, d, c);
+ A(g, d, c);
+ A(h, b, a);
+
+ M(p[0], e, f);
+ M(p[1], h, g);
+ M(p[2], g, f);
+ M(p[3], e, h);
+}
+
+function cswap(p: Float64Array[], q: Float64Array[], b: number): void {
+ let i;
+ for (i = 0; i < 4; i++) {
+ sel25519(p[i], q[i], b);
+ }
+}
+
+function pack(r: Uint8Array, p: Float64Array[]): void {
+ const tx = gf(),
+ ty = gf(),
+ zi = gf();
+ inv25519(zi, p[2]);
+ M(tx, p[0], zi);
+ M(ty, p[1], zi);
+ pack25519(r, ty);
+ r[31] ^= par25519(tx) << 7;
+}
+
+function scalarmult(p: Float64Array[], q: Float64Array[], s: Uint8Array): void {
+ let b, i;
+ set25519(p[0], gf0);
+ set25519(p[1], gf1);
+ set25519(p[2], gf1);
+ set25519(p[3], gf0);
+ for (i = 255; i >= 0; --i) {
+ b = (s[(i / 8) | 0] >> (i & 7)) & 1;
+ cswap(p, q, b);
+ add(q, p);
+ add(p, p);
+ cswap(p, q, b);
+ }
+}
+
+function scalarbase(p: Float64Array[], s: Uint8Array): void {
+ const q = [gf(), gf(), gf(), gf()];
+ set25519(q[0], X);
+ set25519(q[1], Y);
+ set25519(q[2], gf1);
+ M(q[3], X, Y);
+ scalarmult(p, q, s);
+}
+
+function crypto_sign_keypair(
+ pk: Uint8Array,
+ sk: Uint8Array,
+ seeded: boolean,
+): number {
+ const d = new Uint8Array(64);
+ const p = [gf(), gf(), gf(), gf()];
+
+ if (!seeded) randombytes(sk, 32);
+ crypto_hash(d, sk, 32);
+ d[0] &= 248;
+ d[31] &= 127;
+ d[31] |= 64;
+
+ scalarbase(p, d);
+ pack(pk, p);
+
+ for (let i = 0; i < 32; i++) sk[i + 32] = pk[i];
+ return 0;
+}
+
+const L = new Float64Array([
+ 0xed,
+ 0xd3,
+ 0xf5,
+ 0x5c,
+ 0x1a,
+ 0x63,
+ 0x12,
+ 0x58,
+ 0xd6,
+ 0x9c,
+ 0xf7,
+ 0xa2,
+ 0xde,
+ 0xf9,
+ 0xde,
+ 0x14,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0x10,
+]);
+
+function modL(r: Uint8Array, x: Float64Array): void {
+ let carry, i, j, k;
+ for (i = 63; i >= 32; --i) {
+ carry = 0;
+ for (j = i - 32, k = i - 12; j < k; ++j) {
+ x[j] += carry - 16 * x[i] * L[j - (i - 32)];
+ carry = (x[j] + 128) >> 8;
+ x[j] -= carry * 256;
+ }
+ x[j] += carry;
+ x[i] = 0;
+ }
+ carry = 0;
+ for (j = 0; j < 32; j++) {
+ x[j] += carry - (x[31] >> 4) * L[j];
+ carry = x[j] >> 8;
+ x[j] &= 255;
+ }
+ for (j = 0; j < 32; j++) x[j] -= carry * L[j];
+ for (i = 0; i < 32; i++) {
+ x[i + 1] += x[i] >> 8;
+ r[i] = x[i] & 255;
+ }
+}
+
+function reduce(r: Uint8Array): void {
+ const x = new Float64Array(64);
+ for (let i = 0; i < 64; i++) x[i] = r[i];
+ for (let i = 0; i < 64; i++) r[i] = 0;
+ modL(r, x);
+}
+
+// Note: difference from C - smlen returned, not passed as argument.
+function crypto_sign(
+ sm: Uint8Array,
+ m: Uint8Array,
+ n: number,
+ sk: Uint8Array,
+): number {
+ const d = new Uint8Array(64),
+ h = new Uint8Array(64),
+ r = new Uint8Array(64);
+ let i, j;
+ const x = new Float64Array(64);
+ const p = [gf(), gf(), gf(), gf()];
+
+ crypto_hash(d, sk, 32);
+ d[0] &= 248;
+ d[31] &= 127;
+ d[31] |= 64;
+
+ const smlen = n + 64;
+ for (i = 0; i < n; i++) sm[64 + i] = m[i];
+ for (i = 0; i < 32; i++) sm[32 + i] = d[32 + i];
+
+ crypto_hash(r, sm.subarray(32), n + 32);
+ reduce(r);
+ scalarbase(p, r);
+ pack(sm, p);
+
+ for (i = 32; i < 64; i++) sm[i] = sk[i];
+ crypto_hash(h, sm, n + 64);
+ reduce(h);
+
+ for (i = 0; i < 64; i++) x[i] = 0;
+ for (i = 0; i < 32; i++) x[i] = r[i];
+ for (i = 0; i < 32; i++) {
+ for (j = 0; j < 32; j++) {
+ x[i + j] += h[i] * d[j];
+ }
+ }
+
+ modL(sm.subarray(32), x);
+ return smlen;
+}
+
+function unpackneg(r: Float64Array[], p: Uint8Array): number {
+ const t = gf();
+ const chk = gf();
+ const num = gf();
+ const den = gf();
+ const den2 = gf();
+ const den4 = gf();
+ const den6 = gf();
+
+ set25519(r[2], gf1);
+ unpack25519(r[1], p);
+ S(num, r[1]);
+ M(den, num, D);
+ Z(num, num, r[2]);
+ A(den, r[2], den);
+
+ S(den2, den);
+ S(den4, den2);
+ M(den6, den4, den2);
+ M(t, den6, num);
+ M(t, t, den);
+
+ pow2523(t, t);
+ M(t, t, num);
+ M(t, t, den);
+ M(t, t, den);
+ M(r[0], t, den);
+
+ S(chk, r[0]);
+ M(chk, chk, den);
+ if (neq25519(chk, num)) M(r[0], r[0], I);
+
+ S(chk, r[0]);
+ M(chk, chk, den);
+ if (neq25519(chk, num)) return -1;
+
+ if (par25519(r[0]) === p[31] >> 7) Z(r[0], gf0, r[0]);
+
+ M(r[3], r[0], r[1]);
+ return 0;
+}
+
+function crypto_sign_open(
+ m: Uint8Array,
+ sm: Uint8Array,
+ n: number,
+ pk: Uint8Array,
+): number {
+ let i, mlen;
+ const t = new Uint8Array(32),
+ h = new Uint8Array(64);
+ const p = [gf(), gf(), gf(), gf()],
+ q = [gf(), gf(), gf(), gf()];
+
+ mlen = -1;
+ if (n < 64) return -1;
+
+ if (unpackneg(q, pk)) return -1;
+
+ for (i = 0; i < n; i++) m[i] = sm[i];
+ for (i = 0; i < 32; i++) m[i + 32] = pk[i];
+ crypto_hash(h, m, n);
+ reduce(h);
+ scalarmult(p, q, h);
+
+ scalarbase(q, sm.subarray(32));
+ add(p, q);
+ pack(t, p);
+
+ n -= 64;
+ if (crypto_verify_32(sm, 0, t, 0)) {
+ for (i = 0; i < n; i++) m[i] = 0;
+ return -1;
+ }
+
+ for (i = 0; i < n; i++) m[i] = sm[i + 64];
+ mlen = n;
+ return mlen;
+}
+
+const crypto_scalarmult_BYTES = 32,
+ crypto_scalarmult_SCALARBYTES = 32,
+ crypto_sign_BYTES = 64,
+ crypto_sign_PUBLICKEYBYTES = 32,
+ crypto_sign_SECRETKEYBYTES = 64,
+ crypto_sign_SEEDBYTES = 32,
+ crypto_hash_BYTES = 64;
+
+/* High-level API */
+
+function checkArrayTypes(...args: Uint8Array[]): void {
+ for (let i = 0; i < args.length; i++) {
+ if (!(args[i] instanceof Uint8Array))
+ throw new TypeError("unexpected type, use Uint8Array");
+ }
+}
+
+function cleanup(arr: Uint8Array): void {
+ for (let i = 0; i < arr.length; i++) arr[i] = 0;
+}
+
+export function randomBytes(n: number): Uint8Array {
+ const b = new Uint8Array(n);
+ randombytes(b, n);
+ return b;
+}
+
+export function scalarMult(n: Uint8Array, p: Uint8Array): Uint8Array {
+ checkArrayTypes(n, p);
+ if (n.length !== crypto_scalarmult_SCALARBYTES) throw new Error("bad n size");
+ if (p.length !== crypto_scalarmult_BYTES) throw new Error("bad p size");
+ const q = new Uint8Array(crypto_scalarmult_BYTES);
+ crypto_scalarmult(q, n, p);
+ return q;
+}
+
+export function scalarMult_base(n: Uint8Array): Uint8Array {
+ checkArrayTypes(n);
+ if (n.length !== crypto_scalarmult_SCALARBYTES) throw new Error("bad n size");
+ const q = new Uint8Array(crypto_scalarmult_BYTES);
+ crypto_scalarmult_base(q, n);
+ return q;
+}
+
+export const scalarMult_scalarLength = crypto_scalarmult_SCALARBYTES;
+export const scalarMult_groupElementLength = crypto_scalarmult_BYTES;
+
+export function sign(msg: Uint8Array, secretKey: Uint8Array): Uint8Array {
+ checkArrayTypes(msg, secretKey);
+ if (secretKey.length !== crypto_sign_SECRETKEYBYTES)
+ throw new Error("bad secret key size");
+ const signedMsg = new Uint8Array(crypto_sign_BYTES + msg.length);
+ crypto_sign(signedMsg, msg, msg.length, secretKey);
+ return signedMsg;
+}
+
+export function sign_open(
+ signedMsg: Uint8Array,
+ publicKey: Uint8Array,
+): Uint8Array | null {
+ checkArrayTypes(signedMsg, publicKey);
+ if (publicKey.length !== crypto_sign_PUBLICKEYBYTES)
+ throw new Error("bad public key size");
+ const tmp = new Uint8Array(signedMsg.length);
+ const mlen = crypto_sign_open(tmp, signedMsg, signedMsg.length, publicKey);
+ if (mlen < 0) return null;
+ const m = new Uint8Array(mlen);
+ for (let i = 0; i < m.length; i++) m[i] = tmp[i];
+ return m;
+}
+
+export function sign_detached(
+ msg: Uint8Array,
+ secretKey: Uint8Array,
+): Uint8Array {
+ const signedMsg = sign(msg, secretKey);
+ const sig = new Uint8Array(crypto_sign_BYTES);
+ for (let i = 0; i < sig.length; i++) sig[i] = signedMsg[i];
+ return sig;
+}
+
+export function sign_detached_verify(
+ msg: Uint8Array,
+ sig: Uint8Array,
+ publicKey: Uint8Array,
+): boolean {
+ checkArrayTypes(msg, sig, publicKey);
+ if (sig.length !== crypto_sign_BYTES) throw new Error("bad signature size");
+ if (publicKey.length !== crypto_sign_PUBLICKEYBYTES)
+ throw new Error("bad public key size");
+ const sm = new Uint8Array(crypto_sign_BYTES + msg.length);
+ const m = new Uint8Array(crypto_sign_BYTES + msg.length);
+ let i;
+ for (i = 0; i < crypto_sign_BYTES; i++) sm[i] = sig[i];
+ for (i = 0; i < msg.length; i++) sm[i + crypto_sign_BYTES] = msg[i];
+ return crypto_sign_open(m, sm, sm.length, publicKey) >= 0;
+}
+
+export function sign_keyPair(): {
+ publicKey: Uint8Array;
+ secretKey: Uint8Array;
+} {
+ const pk = new Uint8Array(crypto_sign_PUBLICKEYBYTES);
+ const sk = new Uint8Array(crypto_sign_SECRETKEYBYTES);
+ crypto_sign_keypair(pk, sk, false);
+ return { publicKey: pk, secretKey: sk };
+}
+
+export function x25519_edwards_keyPair_fromSecretKey(
+ secretKey: Uint8Array,
+): Uint8Array {
+ const p = [gf(), gf(), gf(), gf()];
+ const pk = new Uint8Array(32);
+
+ const d = new Uint8Array(64);
+ if (secretKey.length != 32) {
+ throw new Error("bad secret key size");
+ }
+ d.set(secretKey, 0);
+ //crypto_hash(d, secretKey, 32);
+
+ d[0] &= 248;
+ d[31] &= 127;
+ d[31] |= 64;
+
+ scalarbase(p, d);
+ pack(pk, p);
+
+ return pk;
+}
+
+export function sign_keyPair_fromSecretKey(
+ secretKey: Uint8Array,
+): {
+ publicKey: Uint8Array;
+ secretKey: Uint8Array;
+} {
+ checkArrayTypes(secretKey);
+ if (secretKey.length !== crypto_sign_SECRETKEYBYTES)
+ throw new Error("bad secret key size");
+ const pk = new Uint8Array(crypto_sign_PUBLICKEYBYTES);
+ for (let i = 0; i < pk.length; i++) pk[i] = secretKey[32 + i];
+ return { publicKey: pk, secretKey: new Uint8Array(secretKey) };
+}
+
+export function sign_keyPair_fromSeed(
+ seed: Uint8Array,
+): {
+ publicKey: Uint8Array;
+ secretKey: Uint8Array;
+} {
+ checkArrayTypes(seed);
+ if (seed.length !== crypto_sign_SEEDBYTES) throw new Error("bad seed size");
+ const pk = new Uint8Array(crypto_sign_PUBLICKEYBYTES);
+ const sk = new Uint8Array(crypto_sign_SECRETKEYBYTES);
+ for (let i = 0; i < 32; i++) sk[i] = seed[i];
+ crypto_sign_keypair(pk, sk, true);
+ return { publicKey: pk, secretKey: sk };
+}
+
+export const sign_publicKeyLength = crypto_sign_PUBLICKEYBYTES;
+export const sign_secretKeyLength = crypto_sign_SECRETKEYBYTES;
+export const sign_seedLength = crypto_sign_SEEDBYTES;
+export const sign_signatureLength = crypto_sign_BYTES;
+
+export function hash(msg: Uint8Array): Uint8Array {
+ checkArrayTypes(msg);
+ const h = new Uint8Array(crypto_hash_BYTES);
+ crypto_hash(h, msg, msg.length);
+ return h;
+}
+
+export const hash_hashLength = crypto_hash_BYTES;
+
+export function verify(x: Uint8Array, y: Uint8Array): boolean {
+ checkArrayTypes(x, y);
+ // Zero length arguments are considered not equal.
+ if (x.length === 0 || y.length === 0) return false;
+ if (x.length !== y.length) return false;
+ return vn(x, 0, y, 0, x.length) === 0 ? true : false;
+}
+
+export function setPRNG(fn: (x: Uint8Array, n: number) => void): void {
+ randombytes = fn;
+}
+
+export function sign_ed25519_pk_to_curve25519(
+ ed25519_pk: Uint8Array,
+): Uint8Array {
+ const ge_a = [gf(), gf(), gf(), gf()];
+ const x = gf();
+ const one_minus_y = gf();
+ const x25519_pk = new Uint8Array(32);
+
+ if (unpackneg(ge_a, ed25519_pk)) {
+ throw Error("invalid public key");
+ }
+
+ set25519(one_minus_y, gf1);
+ Z(one_minus_y, one_minus_y, ge_a[1]);
+
+ set25519(x, gf1);
+ A(x, x, ge_a[1]);
+
+ inv25519(one_minus_y, one_minus_y);
+ M(x, x, one_minus_y);
+ pack25519(x25519_pk, x);
+
+ return x25519_pk;
+}
+
+(function () {
+ // Initialize PRNG if environment provides CSPRNG.
+ // If not, methods calling randombytes will throw.
+ // @ts-ignore-error
+ const cr = typeof self !== "undefined" ? self.crypto || self.msCrypto : null;
+ if (cr && cr.getRandomValues) {
+ // Browsers.
+ const QUOTA = 65536;
+ setPRNG(function (x: Uint8Array, n: number) {
+ let i;
+ const v = new Uint8Array(n);
+ for (i = 0; i < n; i += QUOTA) {
+ cr.getRandomValues(v.subarray(i, i + Math.min(n - i, QUOTA)));
+ }
+ for (i = 0; i < n; i++) x[i] = v[i];
+ cleanup(v);
+ });
+ } else if (typeof require !== "undefined") {
+ // Node.js.
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
+ const cr = require("crypto");
+ if (cr && cr.randomBytes) {
+ setPRNG(function (x: Uint8Array, n: number) {
+ const v = cr.randomBytes(n);
+ for (let i = 0; i < n; i++) x[i] = v[i];
+ cleanup(v);
+ });
+ }
+ }
+})();
diff --git a/packages/taler-wallet-core/src/crypto/primitives/sha256.d.ts.map b/packages/taler-wallet-core/src/crypto/primitives/sha256.d.ts.map
new file mode 100644
index 000000000..91ebcd392
--- /dev/null
+++ b/packages/taler-wallet-core/src/crypto/primitives/sha256.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"sha256.d.ts","sourceRoot":"","sources":["sha256.ts"],"names":[],"mappings":"AAaA,eAAO,MAAM,YAAY,KAAK,CAAC;AAC/B,eAAO,MAAM,SAAS,KAAK,CAAC;AAwK5B,qBAAa,UAAU;IACrB,YAAY,EAAE,MAAM,CAAgB;IACpC,SAAS,EAAE,MAAM,CAAa;IAG9B,OAAO,CAAC,KAAK,CAAiC;IAC9C,OAAO,CAAC,IAAI,CAAkC;IAC9C,OAAO,CAAC,MAAM,CAAmC;IACjD,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,WAAW,CAAK;IAExB,QAAQ,UAAS;;IAQjB,KAAK,IAAI,IAAI;IAgBb,KAAK,IAAI,IAAI;IAiBb,MAAM,CAAC,IAAI,EAAE,UAAU,EAAE,UAAU,GAAE,MAAoB,GAAG,IAAI;IA8BhE,MAAM,CAAC,GAAG,EAAE,UAAU,GAAG,IAAI;IAqC7B,MAAM,IAAI,UAAU;IAOpB,UAAU,CAAC,GAAG,EAAE,WAAW,GAAG,IAAI;IAOlC,aAAa,CAAC,IAAI,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI;CAQ5D;AAGD,qBAAa,IAAI;IACf,OAAO,CAAC,KAAK,CAAgC;IAC7C,OAAO,CAAC,KAAK,CAAgC;IAE7C,SAAS,EAAE,MAAM,CAAwB;IACzC,YAAY,EAAE,MAAM,CAA2B;IAI/C,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,MAAM,CAAc;gBAEhB,GAAG,EAAE,UAAU;IAiC3B,KAAK,IAAI,IAAI;IAOb,KAAK,IAAI,IAAI;IASb,MAAM,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI;IAM9B,MAAM,CAAC,GAAG,EAAE,UAAU,GAAG,IAAI;IAW7B,MAAM,IAAI,UAAU;CAKrB;AAGD,wBAAgB,MAAM,CAAC,IAAI,EAAE,UAAU,GAAG,UAAU,CAKnD;AAGD,wBAAgB,UAAU,CAAC,GAAG,EAAE,UAAU,EAAE,IAAI,EAAE,UAAU,GAAG,UAAU,CAKxE"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/crypto/primitives/sha256.ts b/packages/taler-wallet-core/src/crypto/primitives/sha256.ts
new file mode 100644
index 000000000..97723dbfc
--- /dev/null
+++ b/packages/taler-wallet-core/src/crypto/primitives/sha256.ts
@@ -0,0 +1,426 @@
+// SHA-256 for JavaScript.
+//
+// Written in 2014-2016 by Dmitry Chestnykh.
+// Public domain, no warranty.
+//
+// Functions (accept and return Uint8Arrays):
+//
+// sha256(message) -> hash
+// sha256.hmac(key, message) -> mac
+//
+// Classes:
+//
+// new sha256.Hash()
+export const digestLength = 32;
+export const blockSize = 64;
+
+// SHA-256 constants
+const K = new Uint32Array([
+ 0x428a2f98,
+ 0x71374491,
+ 0xb5c0fbcf,
+ 0xe9b5dba5,
+ 0x3956c25b,
+ 0x59f111f1,
+ 0x923f82a4,
+ 0xab1c5ed5,
+ 0xd807aa98,
+ 0x12835b01,
+ 0x243185be,
+ 0x550c7dc3,
+ 0x72be5d74,
+ 0x80deb1fe,
+ 0x9bdc06a7,
+ 0xc19bf174,
+ 0xe49b69c1,
+ 0xefbe4786,
+ 0x0fc19dc6,
+ 0x240ca1cc,
+ 0x2de92c6f,
+ 0x4a7484aa,
+ 0x5cb0a9dc,
+ 0x76f988da,
+ 0x983e5152,
+ 0xa831c66d,
+ 0xb00327c8,
+ 0xbf597fc7,
+ 0xc6e00bf3,
+ 0xd5a79147,
+ 0x06ca6351,
+ 0x14292967,
+ 0x27b70a85,
+ 0x2e1b2138,
+ 0x4d2c6dfc,
+ 0x53380d13,
+ 0x650a7354,
+ 0x766a0abb,
+ 0x81c2c92e,
+ 0x92722c85,
+ 0xa2bfe8a1,
+ 0xa81a664b,
+ 0xc24b8b70,
+ 0xc76c51a3,
+ 0xd192e819,
+ 0xd6990624,
+ 0xf40e3585,
+ 0x106aa070,
+ 0x19a4c116,
+ 0x1e376c08,
+ 0x2748774c,
+ 0x34b0bcb5,
+ 0x391c0cb3,
+ 0x4ed8aa4a,
+ 0x5b9cca4f,
+ 0x682e6ff3,
+ 0x748f82ee,
+ 0x78a5636f,
+ 0x84c87814,
+ 0x8cc70208,
+ 0x90befffa,
+ 0xa4506ceb,
+ 0xbef9a3f7,
+ 0xc67178f2,
+]);
+
+function hashBlocks(
+ w: Int32Array,
+ v: Int32Array,
+ p: Uint8Array,
+ pos: number,
+ len: number,
+): number {
+ let a: number,
+ b: number,
+ c: number,
+ d: number,
+ e: number,
+ f: number,
+ g: number,
+ h: number,
+ u: number,
+ i: number,
+ j: number,
+ t1: number,
+ t2: number;
+ while (len >= 64) {
+ a = v[0];
+ b = v[1];
+ c = v[2];
+ d = v[3];
+ e = v[4];
+ f = v[5];
+ g = v[6];
+ h = v[7];
+
+ for (i = 0; i < 16; i++) {
+ j = pos + i * 4;
+ w[i] =
+ ((p[j] & 0xff) << 24) |
+ ((p[j + 1] & 0xff) << 16) |
+ ((p[j + 2] & 0xff) << 8) |
+ (p[j + 3] & 0xff);
+ }
+
+ for (i = 16; i < 64; i++) {
+ u = w[i - 2];
+ t1 =
+ ((u >>> 17) | (u << (32 - 17))) ^
+ ((u >>> 19) | (u << (32 - 19))) ^
+ (u >>> 10);
+
+ u = w[i - 15];
+ t2 =
+ ((u >>> 7) | (u << (32 - 7))) ^
+ ((u >>> 18) | (u << (32 - 18))) ^
+ (u >>> 3);
+
+ w[i] = ((t1 + w[i - 7]) | 0) + ((t2 + w[i - 16]) | 0);
+ }
+
+ for (i = 0; i < 64; i++) {
+ t1 =
+ ((((((e >>> 6) | (e << (32 - 6))) ^
+ ((e >>> 11) | (e << (32 - 11))) ^
+ ((e >>> 25) | (e << (32 - 25)))) +
+ ((e & f) ^ (~e & g))) |
+ 0) +
+ ((h + ((K[i] + w[i]) | 0)) | 0)) |
+ 0;
+
+ t2 =
+ ((((a >>> 2) | (a << (32 - 2))) ^
+ ((a >>> 13) | (a << (32 - 13))) ^
+ ((a >>> 22) | (a << (32 - 22)))) +
+ ((a & b) ^ (a & c) ^ (b & c))) |
+ 0;
+
+ h = g;
+ g = f;
+ f = e;
+ e = (d + t1) | 0;
+ d = c;
+ c = b;
+ b = a;
+ a = (t1 + t2) | 0;
+ }
+
+ v[0] += a;
+ v[1] += b;
+ v[2] += c;
+ v[3] += d;
+ v[4] += e;
+ v[5] += f;
+ v[6] += g;
+ v[7] += h;
+
+ pos += 64;
+ len -= 64;
+ }
+ return pos;
+}
+
+// Hash implements SHA256 hash algorithm.
+export class HashSha256 {
+ digestLength: number = digestLength;
+ blockSize: number = blockSize;
+
+ // Note: Int32Array is used instead of Uint32Array for performance reasons.
+ private state: Int32Array = new Int32Array(8); // hash state
+ private temp: Int32Array = new Int32Array(64); // temporary state
+ private buffer: Uint8Array = new Uint8Array(128); // buffer for data to hash
+ private bufferLength = 0; // number of bytes in buffer
+ private bytesHashed = 0; // number of total bytes hashed
+
+ finished = false; // indicates whether the hash was finalized
+
+ constructor() {
+ this.reset();
+ }
+
+ // Resets hash state making it possible
+ // to re-use this instance to hash other data.
+ reset(): this {
+ this.state[0] = 0x6a09e667;
+ this.state[1] = 0xbb67ae85;
+ this.state[2] = 0x3c6ef372;
+ this.state[3] = 0xa54ff53a;
+ this.state[4] = 0x510e527f;
+ this.state[5] = 0x9b05688c;
+ this.state[6] = 0x1f83d9ab;
+ this.state[7] = 0x5be0cd19;
+ this.bufferLength = 0;
+ this.bytesHashed = 0;
+ this.finished = false;
+ return this;
+ }
+
+ // Cleans internal buffers and re-initializes hash state.
+ clean(): void {
+ for (let i = 0; i < this.buffer.length; i++) {
+ this.buffer[i] = 0;
+ }
+ for (let i = 0; i < this.temp.length; i++) {
+ this.temp[i] = 0;
+ }
+ this.reset();
+ }
+
+ // Updates hash state with the given data.
+ //
+ // Optionally, length of the data can be specified to hash
+ // fewer bytes than data.length.
+ //
+ // Throws error when trying to update already finalized hash:
+ // instance must be reset to use it again.
+ update(data: Uint8Array, dataLength: number = data.length): this {
+ if (this.finished) {
+ throw new Error("SHA256: can't update because hash was finished.");
+ }
+ let dataPos = 0;
+ this.bytesHashed += dataLength;
+ if (this.bufferLength > 0) {
+ while (this.bufferLength < 64 && dataLength > 0) {
+ this.buffer[this.bufferLength++] = data[dataPos++];
+ dataLength--;
+ }
+ if (this.bufferLength === 64) {
+ hashBlocks(this.temp, this.state, this.buffer, 0, 64);
+ this.bufferLength = 0;
+ }
+ }
+ if (dataLength >= 64) {
+ dataPos = hashBlocks(this.temp, this.state, data, dataPos, dataLength);
+ dataLength %= 64;
+ }
+ while (dataLength > 0) {
+ this.buffer[this.bufferLength++] = data[dataPos++];
+ dataLength--;
+ }
+ return this;
+ }
+
+ // Finalizes hash state and puts hash into out.
+ //
+ // If hash was already finalized, puts the same value.
+ finish(out: Uint8Array): this {
+ if (!this.finished) {
+ const bytesHashed = this.bytesHashed;
+ const left = this.bufferLength;
+ const bitLenHi = (bytesHashed / 0x20000000) | 0;
+ const bitLenLo = bytesHashed << 3;
+ const padLength = bytesHashed % 64 < 56 ? 64 : 128;
+
+ this.buffer[left] = 0x80;
+ for (let i = left + 1; i < padLength - 8; i++) {
+ this.buffer[i] = 0;
+ }
+ this.buffer[padLength - 8] = (bitLenHi >>> 24) & 0xff;
+ this.buffer[padLength - 7] = (bitLenHi >>> 16) & 0xff;
+ this.buffer[padLength - 6] = (bitLenHi >>> 8) & 0xff;
+ this.buffer[padLength - 5] = (bitLenHi >>> 0) & 0xff;
+ this.buffer[padLength - 4] = (bitLenLo >>> 24) & 0xff;
+ this.buffer[padLength - 3] = (bitLenLo >>> 16) & 0xff;
+ this.buffer[padLength - 2] = (bitLenLo >>> 8) & 0xff;
+ this.buffer[padLength - 1] = (bitLenLo >>> 0) & 0xff;
+
+ hashBlocks(this.temp, this.state, this.buffer, 0, padLength);
+
+ this.finished = true;
+ }
+
+ for (let i = 0; i < 8; i++) {
+ out[i * 4 + 0] = (this.state[i] >>> 24) & 0xff;
+ out[i * 4 + 1] = (this.state[i] >>> 16) & 0xff;
+ out[i * 4 + 2] = (this.state[i] >>> 8) & 0xff;
+ out[i * 4 + 3] = (this.state[i] >>> 0) & 0xff;
+ }
+
+ return this;
+ }
+
+ // Returns the final hash digest.
+ digest(): Uint8Array {
+ const out = new Uint8Array(this.digestLength);
+ this.finish(out);
+ return out;
+ }
+
+ // Internal function for use in HMAC for optimization.
+ _saveState(out: Uint32Array): void {
+ for (let i = 0; i < this.state.length; i++) {
+ out[i] = this.state[i];
+ }
+ }
+
+ // Internal function for use in HMAC for optimization.
+ _restoreState(from: Uint32Array, bytesHashed: number): void {
+ for (let i = 0; i < this.state.length; i++) {
+ this.state[i] = from[i];
+ }
+ this.bytesHashed = bytesHashed;
+ this.finished = false;
+ this.bufferLength = 0;
+ }
+}
+
+// HMAC implements HMAC-SHA256 message authentication algorithm.
+export class HMAC {
+ private inner: HashSha256 = new HashSha256();
+ private outer: HashSha256 = new HashSha256();
+
+ blockSize: number = this.inner.blockSize;
+ digestLength: number = this.inner.digestLength;
+
+ // Copies of hash states after keying.
+ // Need for quick reset without hashing they key again.
+ private istate: Uint32Array;
+ private ostate: Uint32Array;
+
+ constructor(key: Uint8Array) {
+ const pad = new Uint8Array(this.blockSize);
+ if (key.length > this.blockSize) {
+ new HashSha256().update(key).finish(pad).clean();
+ } else {
+ for (let i = 0; i < key.length; i++) {
+ pad[i] = key[i];
+ }
+ }
+ for (let i = 0; i < pad.length; i++) {
+ pad[i] ^= 0x36;
+ }
+ this.inner.update(pad);
+
+ for (let i = 0; i < pad.length; i++) {
+ pad[i] ^= 0x36 ^ 0x5c;
+ }
+ this.outer.update(pad);
+
+ this.istate = new Uint32Array(8);
+ this.ostate = new Uint32Array(8);
+
+ this.inner._saveState(this.istate);
+ this.outer._saveState(this.ostate);
+
+ for (let i = 0; i < pad.length; i++) {
+ pad[i] = 0;
+ }
+ }
+
+ // Returns HMAC state to the state initialized with key
+ // to make it possible to run HMAC over the other data with the same
+ // key without creating a new instance.
+ reset(): this {
+ this.inner._restoreState(this.istate, this.inner.blockSize);
+ this.outer._restoreState(this.ostate, this.outer.blockSize);
+ return this;
+ }
+
+ // Cleans HMAC state.
+ clean(): void {
+ for (let i = 0; i < this.istate.length; i++) {
+ this.ostate[i] = this.istate[i] = 0;
+ }
+ this.inner.clean();
+ this.outer.clean();
+ }
+
+ // Updates state with provided data.
+ update(data: Uint8Array): this {
+ this.inner.update(data);
+ return this;
+ }
+
+ // Finalizes HMAC and puts the result in out.
+ finish(out: Uint8Array): this {
+ if (this.outer.finished) {
+ this.outer.finish(out);
+ } else {
+ this.inner.finish(out);
+ this.outer.update(out, this.digestLength).finish(out);
+ }
+ return this;
+ }
+
+ // Returns message authentication code.
+ digest(): Uint8Array {
+ const out = new Uint8Array(this.digestLength);
+ this.finish(out);
+ return out;
+ }
+}
+
+// Returns SHA256 hash of data.
+export function sha256(data: Uint8Array): Uint8Array {
+ const h = new HashSha256().update(data);
+ const digest = h.digest();
+ h.clean();
+ return digest;
+}
+
+// Returns HMAC-SHA256 of data under the key.
+export function hmacSha256(key: Uint8Array, data: Uint8Array): Uint8Array {
+ const h = new HMAC(key).update(data);
+ const digest = h.digest();
+ h.clean();
+ return digest;
+}
diff --git a/packages/taler-wallet-core/src/crypto/talerCrypto-test.ts b/packages/taler-wallet-core/src/crypto/talerCrypto-test.ts
new file mode 100644
index 000000000..b273b0188
--- /dev/null
+++ b/packages/taler-wallet-core/src/crypto/talerCrypto-test.ts
@@ -0,0 +1,197 @@
+/*
+ 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/>
+ */
+
+/**
+ * Imports
+ */
+import test from "ava";
+import {
+ encodeCrock,
+ decodeCrock,
+ ecdheGetPublic,
+ eddsaGetPublic,
+ keyExchangeEddsaEcdhe,
+ keyExchangeEcdheEddsa,
+ rsaBlind,
+ rsaUnblind,
+ stringToBytes,
+ bytesToString,
+ rsaVerify,
+} from "./talerCrypto";
+import { sha512, kdf } from "./primitives/kdf";
+import * as nacl from "./primitives/nacl-fast";
+
+test("encoding", (t) => {
+ const s = "Hello, World";
+ const encStr = encodeCrock(stringToBytes(s));
+ const outBuf = decodeCrock(encStr);
+ const sOut = bytesToString(outBuf);
+ t.deepEqual(s, sOut);
+});
+
+test("taler-exchange-tvg hash code", (t) => {
+ const input = "91JPRV3F5GG4EKJN41A62V35E8";
+ const output =
+ "CW96WR74JS8T53EC8GKSGD49QKH4ZNFTZXDAWMMV5GJ1E4BM6B8GPN5NVHDJ8ZVXNCW7Q4WBYCV61HCA3PZC2YJD850DT29RHHN7ESR";
+
+ const myOutput = encodeCrock(sha512(decodeCrock(input)));
+
+ t.deepEqual(myOutput, output);
+});
+
+test("taler-exchange-tvg ecdhe key", (t) => {
+ const priv1 = "X4T4N0M8PVQXQEBW2BA7049KFSM7J437NSDFC6GDNM3N5J9367A0";
+ const pub1 = "M997P494MS6A95G1P0QYWW2VNPSHSX5Q6JBY5B9YMNYWP0B50X3G";
+ const priv2 = "14A0MMQ64DCV8HE0CS3WBC9DHFJAHXRGV7NEARFJPC5R5E1697E0";
+ const skm =
+ "NXRY2YCY7H9B6KM928ZD55WG964G59YR0CPX041DYXKBZZ85SAWNPQ8B30QRM5FMHYCXJAN0EAADJYWEF1X3PAC2AJN28626TR5A6AR";
+
+ const myPub1 = nacl.scalarMult_base(decodeCrock(priv1));
+ t.deepEqual(encodeCrock(myPub1), pub1);
+
+ const mySkm = nacl.hash(
+ nacl.scalarMult(decodeCrock(priv2), decodeCrock(pub1)),
+ );
+ t.deepEqual(encodeCrock(mySkm), skm);
+});
+
+test("taler-exchange-tvg eddsa key", (t) => {
+ const priv = "9TM70AKDTS57AWY9JK2J4TMBTMW6K62WHHGZWYDG0VM5ABPZKD40";
+ const pub = "8GSJZ649T2PXMKZC01Y4ANNBE7MF14QVK9SQEC4E46ZHKCVG8AS0";
+
+ const pair = nacl.sign_keyPair_fromSeed(decodeCrock(priv));
+ t.deepEqual(encodeCrock(pair.publicKey), pub);
+});
+
+test("taler-exchange-tvg kdf", (t) => {
+ const salt = "94KPT83PCNS7J83KC5P78Y8";
+ const ikm = "94KPT83MD1JJ0WV5CDS6AX10D5Q70XBM41NPAY90DNGQ8SBJD5GPR";
+ const ctx =
+ "94KPT83141HPYVKMCNW78833D1TPWTSC41GPRWVF41NPWVVQDRG62WS04XMPWSKF4WG6JVH0EHM6A82J8S1G";
+ const outLen = 64;
+ const out =
+ "GTMR4QT05Z9WF5HKVG0WK9RPXGHSMHJNW377G9GJXCA8B0FEKPF4D27RJMSJZYWSQNTBJ5EYVV7ZW18B48Z0JVJJ80RHB706Y96Q358";
+
+ const myOut = kdf(
+ outLen,
+ decodeCrock(ikm),
+ decodeCrock(salt),
+ decodeCrock(ctx),
+ );
+
+ t.deepEqual(encodeCrock(myOut), out);
+});
+
+test("taler-exchange-tvg eddsa_ecdh", (t) => {
+ const priv_ecdhe = "4AFZWMSGTVCHZPQ0R81NWXDCK4N58G7SDBBE5KXE080Y50370JJG";
+ const pub_ecdhe = "FXFN5GPAFTKVPWJDPVXQ87167S8T82T5ZV8CDYC0NH2AE14X0M30";
+ const priv_eddsa = "1KG54M8T3X8BSFSZXCR3SQBSR7Y9P53NX61M864S7TEVMJ2XVPF0";
+ const pub_eddsa = "7BXWKG6N224C57RTDV8XEAHR108HG78NMA995BE8QAT5GC1S7E80";
+ const key_material =
+ "PKZ42Z56SVK2796HG1QYBRJ6ZQM2T9QGA3JA4AAZ8G7CWK9FPX175Q9JE5P0ZAX3HWWPHAQV4DPCK10R9X3SAXHRV0WF06BHEC2ZTKR";
+
+ const myEcdhePub = ecdheGetPublic(decodeCrock(priv_ecdhe));
+ t.deepEqual(encodeCrock(myEcdhePub), pub_ecdhe);
+
+ const myEddsaPub = eddsaGetPublic(decodeCrock(priv_eddsa));
+ t.deepEqual(encodeCrock(myEddsaPub), pub_eddsa);
+
+ const myKm1 = keyExchangeEddsaEcdhe(
+ decodeCrock(priv_eddsa),
+ decodeCrock(pub_ecdhe),
+ );
+ t.deepEqual(encodeCrock(myKm1), key_material);
+
+ const myKm2 = keyExchangeEcdheEddsa(
+ decodeCrock(priv_ecdhe),
+ decodeCrock(pub_eddsa),
+ );
+ t.deepEqual(encodeCrock(myKm2), key_material);
+});
+
+test("taler-exchange-tvg blind signing", (t) => {
+ const messageHash =
+ "TT1R28D79EJEJ9PC35AQS35CCG85DSXSZ508MV2HS2FN4ME6AHESZX5WP485R8A75KG53FN6F1YNW95008663TKAPWB81420VG17BY8";
+ const rsaPublicKey =
+ "040000Y62RSDDKZXTE7GDVA302ZZR0DY224RSDT6WDWR1XGT8E3YG80XV6TMT3ZCNP8XC84W0N6MSZ0EF8S3YB1JJ2AXY9JQZW3MCA0CG38ER4YE2RY4Q2666DEZSNKT29V6CKZVCDHXSAKY8W6RPEKEQ5YSBYQK23MRK3CQTNNJXQFDKEMRHEC5Y6RDHAC5RJCV8JJ8BF18VPKZ2Q7BB14YN1HJ22H8EZGW0RDGG9YPEWA9183BHEQ651PP81J514TJ9K8DH23AJ50SZFNS429HQ390VRP5E4MQ7RK7ZJXXTSZAQSRTC0QF28P23PD37C17QFQB0BBC54MB8MDH7RW104STG6VN0J22P39JP4EXPVGK5D9AX5W869MDQ6SRD42ZYK5H20227Q8CCWSQ6C3132WP0F0H04002";
+ const bks = "7QD31RPJH0W306RJWBRG646Z2FTA1F89BKSXPDAG7YM0N5Z0B610";
+ const bm =
+ "GA8PC6YH9VF5MW6P2DKTV0W0ZTQ24DZ9EAN5QH3SQXRH7SCZHFMM21ZY05F0BS7MFW8TSEP4SEB280BYP5ACHNQWGE10PCXDDMK7ECXJDPHJ224JBCV4KYNWG6NBR3SC9HK8FXVFX55GFBJFNQHNZGEB8DB0KN9MSVYFDXN45KPMSNY03FVX0JZ0R3YG9XQ8XVGB5SYZCF0QSHWH61MT0Q10CZD2V114BT64D3GD86EJ5S9WBMYG51SDN5CSKEJ734YAJ4HCEWW0RDN8GXA9ZMA18SKVW8T3TTBCPJRF2Y77JGQ08GF35SYGA2HWFV1HGVS8RCTER6GB9SZHRG4T7919H9C1KFAP50G2KSV6X42D6KNJANNSGKQH649TJ00YJQXPHPNFBSS198RY2C243D4B4W";
+ const bs =
+ "5VW0MS5PRBA3W8TPATSTDA2YRFQM1Z7F2DWKQ8ATMZYYY768Q3STZ3HGNVYQ6JB5NKP80G5HGE58616FPA70SX9PTW7EN8EJ23E26FASBWZBP8E2RWQQ5E0F72B2PWRP5ZCA2J3AB3F6P86XK4PZYT64RF94MDGHY0GSDSSBH5YSFB3VM0KVXA52H2Y2G9S85AVCSD3BTMHQRF5BJJ8JE00T4GK70PSTVCGMRKRNA7DGW7GD2F35W55AXF7R2YJC8PAGNSJYWKC3PC75A5N8H69K299AK5PM3CDDHNS4BMRNGF7K49CR4ZBFRXDAWMB3X6T05Q4NKSG0F1KP5JA0XBMF2YJK7KEPRD1EWCHJE44T9YXBTK4W9CV77X7Z9P407ZC6YB3M2ARANZXHJKSM3XC33M";
+ const sig =
+ "PFT6WQJGCM9DE6264DJS6RMG4XDMCDBJKZGSXAF3BEXWZ979Q13NETKK05S1YV91CX3Y034FSS86SSHZTTE8097RRESQP52EKFGTWJXKHZJEQJ49YHMBNQDHW4CFBJECNJSV2PMHWVGXV7HB84R6P0S3ES559HWQX01Q9MYDEGRNHKW87QR2BNSG951D5NQGAKEJ2SSJBE18S6WYAC24FAP8TT8ANECH5371J0DJY0YR0VWAFWVJDV8XQSFXWMJ80N3A80SPSHPYJY3WZZXW63WQ46WHYY56ZSNE5G1RZ5CR0XYV2ECKPM8R0FS58EV16WTRAM1ABBFVNAT3CAEFAZCWP3XHPVBQY5NZVTD5QS2Q8SKJQ2XB30E11CWDN9KTV5CBK4DN72EVG73F3W3BATAKHG";
+
+ const myBm = rsaBlind(
+ decodeCrock(messageHash),
+ decodeCrock(bks),
+ decodeCrock(rsaPublicKey),
+ );
+ t.deepEqual(encodeCrock(myBm), bm);
+
+ const mySig = rsaUnblind(
+ decodeCrock(bs),
+ decodeCrock(rsaPublicKey),
+ decodeCrock(bks),
+ );
+ t.deepEqual(encodeCrock(mySig), sig);
+
+ const v = rsaVerify(
+ decodeCrock(messageHash),
+ decodeCrock(sig),
+ decodeCrock(rsaPublicKey),
+ );
+ t.true(v);
+});
+
+test("incremental hashing #1", (t) => {
+ const n = 1024;
+ const d = nacl.randomBytes(n);
+
+ const h1 = nacl.hash(d);
+ const h2 = new nacl.HashState().update(d).finish();
+
+ const s = new nacl.HashState();
+ for (let i = 0; i < n; i++) {
+ const b = new Uint8Array(1);
+ b[0] = d[i];
+ s.update(b);
+ }
+
+ const h3 = s.finish();
+
+ t.deepEqual(encodeCrock(h1), encodeCrock(h2));
+ t.deepEqual(encodeCrock(h1), encodeCrock(h3));
+});
+
+test("incremental hashing #2", (t) => {
+ const n = 10;
+ const d = nacl.randomBytes(n);
+
+ const h1 = nacl.hash(d);
+ const h2 = new nacl.HashState().update(d).finish();
+ const s = new nacl.HashState();
+ for (let i = 0; i < n; i++) {
+ const b = new Uint8Array(1);
+ b[0] = d[i];
+ s.update(b);
+ }
+
+ const h3 = s.finish();
+
+ t.deepEqual(encodeCrock(h1), encodeCrock(h3));
+ t.deepEqual(encodeCrock(h1), encodeCrock(h2));
+});
diff --git a/packages/taler-wallet-core/src/crypto/talerCrypto.d.ts.map b/packages/taler-wallet-core/src/crypto/talerCrypto.d.ts.map
new file mode 100644
index 000000000..cbc5ba16c
--- /dev/null
+++ b/packages/taler-wallet-core/src/crypto/talerCrypto.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"talerCrypto.d.ts","sourceRoot":"","sources":["talerCrypto.ts"],"names":[],"mappings":"AAgBA;;GAEG;AAEH,OAAO,KAAK,IAAI,MAAM,wBAAwB,CAAC;AAgB/C,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,GAAG,UAAU,CAEpD;AA6CD,wBAAgB,WAAW,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM,CAuBrD;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,CA0BvD;AAED,wBAAgB,cAAc,CAAC,SAAS,EAAE,UAAU,GAAG,UAAU,CAGhE;AAED,wBAAgB,cAAc,CAAC,SAAS,EAAE,UAAU,GAAG,UAAU,CAEhE;AAED,wBAAgB,qBAAqB,CACnC,SAAS,EAAE,UAAU,EACrB,QAAQ,EAAE,UAAU,GACnB,UAAU,CAQZ;AAED,wBAAgB,qBAAqB,CACnC,SAAS,EAAE,UAAU,EACrB,QAAQ,EAAE,UAAU,GACnB,UAAU,CAIZ;AAwCD,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,GAAG,UAAU,CAEnD;AAED,wBAAgB,aAAa,CAAC,CAAC,EAAE,UAAU,GAAG,MAAM,CAEnD;AAuED,wBAAgB,QAAQ,CACtB,EAAE,EAAE,UAAU,EACd,GAAG,EAAE,UAAU,EACf,SAAS,EAAE,UAAU,GACpB,UAAU,CAOZ;AAED,wBAAgB,UAAU,CACxB,GAAG,EAAE,UAAU,EACf,SAAS,EAAE,UAAU,EACrB,GAAG,EAAE,UAAU,GACd,UAAU,CAOZ;AAED,wBAAgB,SAAS,CACvB,EAAE,EAAE,UAAU,EACd,MAAM,EAAE,UAAU,EAClB,SAAS,EAAE,UAAU,GACpB,OAAO,CAMT;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,UAAU,CAAC;IACrB,SAAS,EAAE,UAAU,CAAC;CACvB;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,UAAU,CAAC;IACrB,SAAS,EAAE,UAAU,CAAC;CACvB;AAED,wBAAgB,kBAAkB,IAAI,YAAY,CAIjD;AAED,wBAAgB,kBAAkB,IAAI,YAAY,CAIjD;AAED,wBAAgB,uBAAuB,IAAI,UAAU,CAEpD;AAED,wBAAgB,IAAI,CAAC,CAAC,EAAE,UAAU,GAAG,UAAU,CAE9C;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,GAAG,UAAU,CAG5E;AAED,wBAAgB,WAAW,CACzB,GAAG,EAAE,UAAU,EACf,GAAG,EAAE,UAAU,EACf,QAAQ,EAAE,UAAU,GACnB,OAAO,CAET;AAED,wBAAgB,iBAAiB,IAAI,IAAI,CAAC,SAAS,CAElD;AAED,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,UAAU,CAAC;IACpB,QAAQ,EAAE,UAAU,CAAC;IACrB,GAAG,EAAE,UAAU,CAAC;CACjB;AAED,wBAAgB,oBAAoB,CAClC,UAAU,EAAE,UAAU,EACtB,UAAU,EAAE,MAAM,GACjB,SAAS,CAcX"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/crypto/talerCrypto.ts b/packages/taler-wallet-core/src/crypto/talerCrypto.ts
new file mode 100644
index 000000000..3ce5491c1
--- /dev/null
+++ b/packages/taler-wallet-core/src/crypto/talerCrypto.ts
@@ -0,0 +1,391 @@
+/*
+ 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/>
+ */
+
+/**
+ * Native implementation of GNU Taler crypto.
+ */
+
+import * as nacl from "./primitives/nacl-fast";
+import bigint from "big-integer";
+import { kdf } from "./primitives/kdf";
+
+// @ts-ignore
+const decoder = new TextDecoder();
+if (typeof decoder !== "object") {
+ throw Error("FATAL: TextDecoder not available");
+}
+
+// @ts-ignore
+const encoder = new TextEncoder();
+if (typeof encoder !== "object") {
+ throw Error("FATAL: TextEncoder not available");
+}
+
+export function getRandomBytes(n: number): Uint8Array {
+ return nacl.randomBytes(n);
+}
+
+const encTable = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
+
+class EncodingError extends Error {
+ constructor() {
+ super("Encoding error");
+ Object.setPrototypeOf(this, EncodingError.prototype);
+ }
+}
+
+function getValue(chr: string): number {
+ let a = chr;
+ switch (chr) {
+ case "O":
+ case "o":
+ a = "0;";
+ break;
+ case "i":
+ case "I":
+ case "l":
+ case "L":
+ a = "1";
+ break;
+ case "u":
+ case "U":
+ a = "V";
+ }
+
+ if (a >= "0" && a <= "9") {
+ return a.charCodeAt(0) - "0".charCodeAt(0);
+ }
+
+ if (a >= "a" && a <= "z") a = a.toUpperCase();
+ let dec = 0;
+ if (a >= "A" && a <= "Z") {
+ if ("I" < a) dec++;
+ if ("L" < a) dec++;
+ if ("O" < a) dec++;
+ if ("U" < a) dec++;
+ return a.charCodeAt(0) - "A".charCodeAt(0) + 10 - dec;
+ }
+ throw new EncodingError();
+}
+
+export function encodeCrock(data: ArrayBuffer): string {
+ const dataBytes = new Uint8Array(data);
+ let sb = "";
+ const size = data.byteLength;
+ let bitBuf = 0;
+ let numBits = 0;
+ let pos = 0;
+ while (pos < size || numBits > 0) {
+ if (pos < size && numBits < 5) {
+ const d = dataBytes[pos++];
+ bitBuf = (bitBuf << 8) | d;
+ numBits += 8;
+ }
+ if (numBits < 5) {
+ // zero-padding
+ bitBuf = bitBuf << (5 - numBits);
+ numBits = 5;
+ }
+ const v = (bitBuf >>> (numBits - 5)) & 31;
+ sb += encTable[v];
+ numBits -= 5;
+ }
+ return sb;
+}
+
+export function decodeCrock(encoded: string): Uint8Array {
+ const size = encoded.length;
+ let bitpos = 0;
+ let bitbuf = 0;
+ let readPosition = 0;
+ const outLen = Math.floor((size * 5) / 8);
+ const out = new Uint8Array(outLen);
+ let outPos = 0;
+
+ while (readPosition < size || bitpos > 0) {
+ if (readPosition < size) {
+ const v = getValue(encoded[readPosition++]);
+ bitbuf = (bitbuf << 5) | v;
+ bitpos += 5;
+ }
+ while (bitpos >= 8) {
+ const d = (bitbuf >>> (bitpos - 8)) & 0xff;
+ out[outPos++] = d;
+ bitpos -= 8;
+ }
+ if (readPosition == size && bitpos > 0) {
+ bitbuf = (bitbuf << (8 - bitpos)) & 0xff;
+ bitpos = bitbuf == 0 ? 0 : 8;
+ }
+ }
+ return out;
+}
+
+export function eddsaGetPublic(eddsaPriv: Uint8Array): Uint8Array {
+ const pair = nacl.sign_keyPair_fromSeed(eddsaPriv);
+ return pair.publicKey;
+}
+
+export function ecdheGetPublic(ecdhePriv: Uint8Array): Uint8Array {
+ return nacl.scalarMult_base(ecdhePriv);
+}
+
+export function keyExchangeEddsaEcdhe(
+ eddsaPriv: Uint8Array,
+ ecdhePub: Uint8Array,
+): Uint8Array {
+ const ph = nacl.hash(eddsaPriv);
+ const a = new Uint8Array(32);
+ for (let i = 0; i < 32; i++) {
+ a[i] = ph[i];
+ }
+ const x = nacl.scalarMult(a, ecdhePub);
+ return nacl.hash(x);
+}
+
+export function keyExchangeEcdheEddsa(
+ ecdhePriv: Uint8Array,
+ eddsaPub: Uint8Array,
+): Uint8Array {
+ const curve25519Pub = nacl.sign_ed25519_pk_to_curve25519(eddsaPub);
+ const x = nacl.scalarMult(ecdhePriv, curve25519Pub);
+ return nacl.hash(x);
+}
+
+interface RsaPub {
+ N: bigint.BigInteger;
+ e: bigint.BigInteger;
+}
+
+interface RsaBlindingKey {
+ r: bigint.BigInteger;
+}
+
+/**
+ * KDF modulo a big integer.
+ */
+function kdfMod(
+ n: bigint.BigInteger,
+ ikm: Uint8Array,
+ salt: Uint8Array,
+ info: Uint8Array,
+): bigint.BigInteger {
+ const nbits = n.bitLength().toJSNumber();
+ const buflen = Math.floor((nbits - 1) / 8 + 1);
+ const mask = (1 << (8 - (buflen * 8 - nbits))) - 1;
+ let counter = 0;
+ while (true) {
+ const ctx = new Uint8Array(info.byteLength + 2);
+ ctx.set(info, 0);
+ ctx[ctx.length - 2] = (counter >>> 8) & 0xff;
+ ctx[ctx.length - 1] = counter & 0xff;
+ const buf = kdf(buflen, ikm, salt, ctx);
+ const arr = Array.from(buf);
+ arr[0] = arr[0] & mask;
+ const r = bigint.fromArray(arr, 256, false);
+ if (r.lt(n)) {
+ return r;
+ }
+ counter++;
+ }
+}
+
+export function stringToBytes(s: string): Uint8Array {
+ return encoder.encode(s);
+}
+
+export function bytesToString(b: Uint8Array): string {
+ return decoder.decode(b);
+}
+
+function loadBigInt(arr: Uint8Array): bigint.BigInteger {
+ return bigint.fromArray(Array.from(arr), 256, false);
+}
+
+function rsaBlindingKeyDerive(
+ rsaPub: RsaPub,
+ bks: Uint8Array,
+): bigint.BigInteger {
+ const salt = stringToBytes("Blinding KDF extrator HMAC key");
+ const info = stringToBytes("Blinding KDF");
+ return kdfMod(rsaPub.N, bks, salt, info);
+}
+
+/*
+ * Test for malicious RSA key.
+ *
+ * Assuming n is an RSA modulous and r is generated using a call to
+ * GNUNET_CRYPTO_kdf_mod_mpi, if gcd(r,n) != 1 then n must be a
+ * malicious RSA key designed to deanomize the user.
+ *
+ * @param r KDF result
+ * @param n RSA modulus of the public key
+ */
+function rsaGcdValidate(r: bigint.BigInteger, n: bigint.BigInteger): void {
+ const t = bigint.gcd(r, n);
+ if (!t.equals(bigint.one)) {
+ throw Error("malicious RSA public key");
+ }
+}
+
+function rsaFullDomainHash(hm: Uint8Array, rsaPub: RsaPub): bigint.BigInteger {
+ const info = stringToBytes("RSA-FDA FTpsW!");
+ const salt = rsaPubEncode(rsaPub);
+ const r = kdfMod(rsaPub.N, hm, salt, info);
+ rsaGcdValidate(r, rsaPub.N);
+ return r;
+}
+
+function rsaPubDecode(rsaPub: Uint8Array): RsaPub {
+ const modulusLength = (rsaPub[0] << 8) | rsaPub[1];
+ const exponentLength = (rsaPub[2] << 8) | rsaPub[3];
+ if (4 + exponentLength + modulusLength != rsaPub.length) {
+ throw Error("invalid RSA public key (format wrong)");
+ }
+ const modulus = rsaPub.slice(4, 4 + modulusLength);
+ const exponent = rsaPub.slice(
+ 4 + modulusLength,
+ 4 + modulusLength + exponentLength,
+ );
+ const res = {
+ N: loadBigInt(modulus),
+ e: loadBigInt(exponent),
+ };
+ return res;
+}
+
+function rsaPubEncode(rsaPub: RsaPub): Uint8Array {
+ const mb = rsaPub.N.toArray(256).value;
+ const eb = rsaPub.e.toArray(256).value;
+ const out = new Uint8Array(4 + mb.length + eb.length);
+ out[0] = (mb.length >>> 8) & 0xff;
+ out[1] = mb.length & 0xff;
+ out[2] = (eb.length >>> 8) & 0xff;
+ out[3] = eb.length & 0xff;
+ out.set(mb, 4);
+ out.set(eb, 4 + mb.length);
+ return out;
+}
+
+export function rsaBlind(
+ hm: Uint8Array,
+ bks: Uint8Array,
+ rsaPubEnc: Uint8Array,
+): Uint8Array {
+ const rsaPub = rsaPubDecode(rsaPubEnc);
+ const data = rsaFullDomainHash(hm, rsaPub);
+ const r = rsaBlindingKeyDerive(rsaPub, bks);
+ const r_e = r.modPow(rsaPub.e, rsaPub.N);
+ const bm = r_e.multiply(data).mod(rsaPub.N);
+ return new Uint8Array(bm.toArray(256).value);
+}
+
+export function rsaUnblind(
+ sig: Uint8Array,
+ rsaPubEnc: Uint8Array,
+ bks: Uint8Array,
+): Uint8Array {
+ const rsaPub = rsaPubDecode(rsaPubEnc);
+ const blinded_s = loadBigInt(sig);
+ const r = rsaBlindingKeyDerive(rsaPub, bks);
+ const r_inv = r.modInv(rsaPub.N);
+ const s = blinded_s.multiply(r_inv).mod(rsaPub.N);
+ return new Uint8Array(s.toArray(256).value);
+}
+
+export function rsaVerify(
+ hm: Uint8Array,
+ rsaSig: Uint8Array,
+ rsaPubEnc: Uint8Array,
+): boolean {
+ const rsaPub = rsaPubDecode(rsaPubEnc);
+ const d = rsaFullDomainHash(hm, rsaPub);
+ const sig = loadBigInt(rsaSig);
+ const sig_e = sig.modPow(rsaPub.e, rsaPub.N);
+ return sig_e.equals(d);
+}
+
+export interface EddsaKeyPair {
+ eddsaPub: Uint8Array;
+ eddsaPriv: Uint8Array;
+}
+
+export interface EcdheKeyPair {
+ ecdhePub: Uint8Array;
+ ecdhePriv: Uint8Array;
+}
+
+export function createEddsaKeyPair(): EddsaKeyPair {
+ const eddsaPriv = nacl.randomBytes(32);
+ const eddsaPub = eddsaGetPublic(eddsaPriv);
+ return { eddsaPriv, eddsaPub };
+}
+
+export function createEcdheKeyPair(): EcdheKeyPair {
+ const ecdhePriv = nacl.randomBytes(32);
+ const ecdhePub = ecdheGetPublic(ecdhePriv);
+ return { ecdhePriv, ecdhePub };
+}
+
+export function createBlindingKeySecret(): Uint8Array {
+ return nacl.randomBytes(32);
+}
+
+export function hash(d: Uint8Array): Uint8Array {
+ return nacl.hash(d);
+}
+
+export function eddsaSign(msg: Uint8Array, eddsaPriv: Uint8Array): Uint8Array {
+ const pair = nacl.sign_keyPair_fromSeed(eddsaPriv);
+ return nacl.sign_detached(msg, pair.secretKey);
+}
+
+export function eddsaVerify(
+ msg: Uint8Array,
+ sig: Uint8Array,
+ eddsaPub: Uint8Array,
+): boolean {
+ return nacl.sign_detached_verify(msg, sig, eddsaPub);
+}
+
+export function createHashContext(): nacl.HashState {
+ return new nacl.HashState();
+}
+
+export interface FreshCoin {
+ coinPub: Uint8Array;
+ coinPriv: Uint8Array;
+ bks: Uint8Array;
+}
+
+export function setupRefreshPlanchet(
+ secretSeed: Uint8Array,
+ coinNumber: number,
+): FreshCoin {
+ const info = stringToBytes("taler-coin-derivation");
+ const saltArrBuf = new ArrayBuffer(4);
+ const salt = new Uint8Array(saltArrBuf);
+ const saltDataView = new DataView(saltArrBuf);
+ saltDataView.setUint32(0, coinNumber);
+ const out = kdf(64, secretSeed, salt, info);
+ const coinPriv = out.slice(0, 32);
+ const bks = out.slice(32, 64);
+ return {
+ bks,
+ coinPriv,
+ coinPub: eddsaGetPublic(coinPriv),
+ };
+}
diff --git a/packages/taler-wallet-core/src/crypto/workers/cryptoApi.d.ts.map b/packages/taler-wallet-core/src/crypto/workers/cryptoApi.d.ts.map
new file mode 100644
index 000000000..d8ab05823
--- /dev/null
+++ b/packages/taler-wallet-core/src/crypto/workers/cryptoApi.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"cryptoApi.d.ts","sourceRoot":"","sources":["cryptoApi.ts"],"names":[],"mappings":"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"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/crypto/workers/cryptoApi.ts b/packages/taler-wallet-core/src/crypto/workers/cryptoApi.ts
new file mode 100644
index 000000000..a272d5724
--- /dev/null
+++ b/packages/taler-wallet-core/src/crypto/workers/cryptoApi.ts
@@ -0,0 +1,446 @@
+/*
+ This file is part of TALER
+ (C) 2016 GNUnet e.V.
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * API to access the Taler crypto worker thread.
+ * @author Florian Dold
+ */
+
+/**
+ * Imports.
+ */
+import { AmountJson } from "../../util/amounts";
+
+import {
+ CoinRecord,
+ DenominationRecord,
+ RefreshSessionRecord,
+ TipPlanchet,
+ WireFee,
+ DenominationSelectionInfo,
+} from "../../types/dbTypes";
+
+import { CryptoWorker } from "./cryptoWorker";
+
+import { RecoupRequest, CoinDepositPermission } from "../../types/talerTypes";
+
+import {
+ BenchmarkResult,
+ PlanchetCreationResult,
+ PlanchetCreationRequest,
+ DepositInfo,
+} from "../../types/walletTypes";
+
+import * as timer from "../../util/timer";
+import { Logger } from "../../util/logging";
+
+const logger = new Logger("cryptoApi.ts");
+
+/**
+ * State of a crypto worker.
+ */
+interface WorkerState {
+ /**
+ * The actual worker thread.
+ */
+ w: CryptoWorker | null;
+
+ /**
+ * Work we're currently executing or null if not busy.
+ */
+ currentWorkItem: WorkItem | null;
+
+ /**
+ * Timer to terminate the worker if it's not busy enough.
+ */
+ terminationTimerHandle: timer.TimerHandle | null;
+}
+
+interface WorkItem {
+ operation: string;
+ args: any[];
+ resolve: any;
+ reject: any;
+
+ /**
+ * Serial id to identify a matching response.
+ */
+ rpcId: number;
+
+ /**
+ * Time when the work was submitted to a (non-busy) worker thread.
+ */
+ startTime: number;
+}
+
+/**
+ * Number of different priorities. Each priority p
+ * must be 0 <= p < NUM_PRIO.
+ */
+const NUM_PRIO = 5;
+
+export interface CryptoWorkerFactory {
+ /**
+ * Start a new worker.
+ */
+ startWorker(): CryptoWorker;
+
+ /**
+ * Query the number of workers that should be
+ * run at the same time.
+ */
+ getConcurrency(): number;
+}
+
+/**
+ * Crypto API that interfaces manages a background crypto thread
+ * for the execution of expensive operations.
+ */
+export class CryptoApi {
+ private nextRpcId = 1;
+ private workers: WorkerState[];
+ private workQueues: WorkItem[][];
+
+ private workerFactory: CryptoWorkerFactory;
+
+ /**
+ * Number of busy workers.
+ */
+ private numBusy = 0;
+
+ /**
+ * Did we stop accepting new requests?
+ */
+ private stopped = false;
+
+ /**
+ * Terminate all worker threads.
+ */
+ terminateWorkers(): void {
+ for (const worker of this.workers) {
+ if (worker.w) {
+ logger.trace("terminating worker");
+ worker.w.terminate();
+ if (worker.terminationTimerHandle) {
+ worker.terminationTimerHandle.clear();
+ worker.terminationTimerHandle = null;
+ }
+ if (worker.currentWorkItem) {
+ worker.currentWorkItem.reject(Error("explicitly terminated"));
+ worker.currentWorkItem = null;
+ }
+ worker.w = null;
+ }
+ }
+ }
+
+ stop(): void {
+ this.terminateWorkers();
+ this.stopped = true;
+ }
+
+ /**
+ * Start a worker (if not started) and set as busy.
+ */
+ wake(ws: WorkerState, work: WorkItem): void {
+ if (this.stopped) {
+ logger.trace("cryptoApi is stopped");
+ return;
+ }
+ if (ws.currentWorkItem !== null) {
+ throw Error("assertion failed");
+ }
+ ws.currentWorkItem = work;
+ this.numBusy++;
+ let worker: CryptoWorker;
+ if (!ws.w) {
+ worker = this.workerFactory.startWorker();
+ worker.onmessage = (m: any) => this.handleWorkerMessage(ws, m);
+ worker.onerror = (e: any) => this.handleWorkerError(ws, e);
+ ws.w = worker;
+ } else {
+ worker = ws.w;
+ }
+
+ const msg: any = {
+ args: work.args,
+ id: work.rpcId,
+ operation: work.operation,
+ };
+ this.resetWorkerTimeout(ws);
+ work.startTime = timer.performanceNow();
+ setTimeout(() => worker.postMessage(msg), 0);
+ }
+
+ resetWorkerTimeout(ws: WorkerState): void {
+ if (ws.terminationTimerHandle !== null) {
+ ws.terminationTimerHandle.clear();
+ ws.terminationTimerHandle = null;
+ }
+ const destroy = (): void => {
+ // terminate worker if it's idle
+ if (ws.w && ws.currentWorkItem === null) {
+ ws.w.terminate();
+ ws.w = null;
+ }
+ };
+ ws.terminationTimerHandle = timer.after(15 * 1000, destroy);
+ }
+
+ handleWorkerError(ws: WorkerState, e: any): void {
+ if (ws.currentWorkItem) {
+ console.error(
+ `error in worker during ${ws.currentWorkItem.operation}`,
+ e,
+ );
+ } else {
+ console.error("error in worker", e);
+ }
+ console.error(e.message);
+ try {
+ if (ws.w) {
+ ws.w.terminate();
+ ws.w = null;
+ }
+ } catch (e) {
+ console.error(e);
+ }
+ if (ws.currentWorkItem !== null) {
+ ws.currentWorkItem.reject(e);
+ ws.currentWorkItem = null;
+ this.numBusy--;
+ }
+ this.findWork(ws);
+ }
+
+ private findWork(ws: WorkerState): void {
+ // try to find more work for this worker
+ for (let i = 0; i < NUM_PRIO; i++) {
+ const q = this.workQueues[NUM_PRIO - i - 1];
+ if (q.length !== 0) {
+ const work: WorkItem | undefined = q.shift();
+ if (!work) {
+ continue;
+ }
+ this.wake(ws, work);
+ return;
+ }
+ }
+ }
+
+ handleWorkerMessage(ws: WorkerState, msg: any): void {
+ const id = msg.data.id;
+ if (typeof id !== "number") {
+ console.error("rpc id must be number");
+ return;
+ }
+ const currentWorkItem = ws.currentWorkItem;
+ ws.currentWorkItem = null;
+ this.numBusy--;
+ this.findWork(ws);
+ if (!currentWorkItem) {
+ console.error("unsolicited response from worker");
+ return;
+ }
+ if (id !== currentWorkItem.rpcId) {
+ console.error(`RPC with id ${id} has no registry entry`);
+ return;
+ }
+
+ currentWorkItem.resolve(msg.data.result);
+ }
+
+ constructor(workerFactory: CryptoWorkerFactory) {
+ this.workerFactory = workerFactory;
+ this.workers = new Array<WorkerState>(workerFactory.getConcurrency());
+
+ for (let i = 0; i < this.workers.length; i++) {
+ this.workers[i] = {
+ currentWorkItem: null,
+ terminationTimerHandle: null,
+ w: null,
+ };
+ }
+
+ this.workQueues = [];
+ for (let i = 0; i < NUM_PRIO; i++) {
+ this.workQueues.push([]);
+ }
+ }
+
+ private doRpc<T>(
+ operation: string,
+ priority: number,
+ ...args: any[]
+ ): Promise<T> {
+ const p: Promise<T> = new Promise<T>((resolve, reject) => {
+ const rpcId = this.nextRpcId++;
+ const workItem: WorkItem = {
+ operation,
+ args,
+ resolve,
+ reject,
+ rpcId,
+ startTime: 0,
+ };
+
+ if (this.numBusy === this.workers.length) {
+ const q = this.workQueues[priority];
+ if (!q) {
+ throw Error("assertion failed");
+ }
+ this.workQueues[priority].push(workItem);
+ return;
+ }
+
+ for (const ws of this.workers) {
+ if (ws.currentWorkItem !== null) {
+ continue;
+ }
+ this.wake(ws, workItem);
+ return;
+ }
+
+ throw Error("assertion failed");
+ });
+
+ return p;
+ }
+
+ createPlanchet(
+ req: PlanchetCreationRequest,
+ ): Promise<PlanchetCreationResult> {
+ return this.doRpc<PlanchetCreationResult>("createPlanchet", 1, req);
+ }
+
+ createTipPlanchet(denom: DenominationRecord): Promise<TipPlanchet> {
+ return this.doRpc<TipPlanchet>("createTipPlanchet", 1, denom);
+ }
+
+ hashString(str: string): Promise<string> {
+ return this.doRpc<string>("hashString", 1, str);
+ }
+
+ hashEncoded(encodedBytes: string): Promise<string> {
+ return this.doRpc<string>("hashEncoded", 1, encodedBytes);
+ }
+
+ isValidDenom(denom: DenominationRecord, masterPub: string): Promise<boolean> {
+ return this.doRpc<boolean>("isValidDenom", 2, denom, masterPub);
+ }
+
+ isValidWireFee(
+ type: string,
+ wf: WireFee,
+ masterPub: string,
+ ): Promise<boolean> {
+ return this.doRpc<boolean>("isValidWireFee", 2, type, wf, masterPub);
+ }
+
+ isValidPaymentSignature(
+ sig: string,
+ contractHash: string,
+ merchantPub: string,
+ ): Promise<boolean> {
+ return this.doRpc<boolean>(
+ "isValidPaymentSignature",
+ 1,
+ sig,
+ contractHash,
+ merchantPub,
+ );
+ }
+
+ signDepositPermission(
+ depositInfo: DepositInfo,
+ ): Promise<CoinDepositPermission> {
+ return this.doRpc<CoinDepositPermission>(
+ "signDepositPermission",
+ 3,
+ depositInfo,
+ );
+ }
+
+ createEddsaKeypair(): Promise<{ priv: string; pub: string }> {
+ return this.doRpc<{ priv: string; pub: string }>("createEddsaKeypair", 1);
+ }
+
+ rsaUnblind(sig: string, bk: string, pk: string): Promise<string> {
+ return this.doRpc<string>("rsaUnblind", 4, sig, bk, pk);
+ }
+
+ rsaVerify(hm: string, sig: string, pk: string): Promise<boolean> {
+ return this.doRpc<boolean>("rsaVerify", 4, hm, sig, pk);
+ }
+
+ isValidWireAccount(
+ paytoUri: string,
+ sig: string,
+ masterPub: string,
+ ): Promise<boolean> {
+ return this.doRpc<boolean>(
+ "isValidWireAccount",
+ 4,
+ paytoUri,
+ sig,
+ masterPub,
+ );
+ }
+
+ createRecoupRequest(coin: CoinRecord): Promise<RecoupRequest> {
+ return this.doRpc<RecoupRequest>("createRecoupRequest", 1, coin);
+ }
+
+ createRefreshSession(
+ exchangeBaseUrl: string,
+ kappa: number,
+ meltCoin: CoinRecord,
+ newCoinDenoms: DenominationSelectionInfo,
+ meltFee: AmountJson,
+ ): Promise<RefreshSessionRecord> {
+ return this.doRpc<RefreshSessionRecord>(
+ "createRefreshSession",
+ 4,
+ exchangeBaseUrl,
+ kappa,
+ meltCoin,
+ newCoinDenoms,
+ meltFee,
+ );
+ }
+
+ signCoinLink(
+ oldCoinPriv: string,
+ newDenomHash: string,
+ oldCoinPub: string,
+ transferPub: string,
+ coinEv: string,
+ ): Promise<string> {
+ return this.doRpc<string>(
+ "signCoinLink",
+ 4,
+ oldCoinPriv,
+ newDenomHash,
+ oldCoinPub,
+ transferPub,
+ coinEv,
+ );
+ }
+
+ benchmark(repetitions: number): Promise<BenchmarkResult> {
+ return this.doRpc<BenchmarkResult>("benchmark", 1, repetitions);
+ }
+}
diff --git a/packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.d.ts.map b/packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.d.ts.map
new file mode 100644
index 000000000..192c54d05
--- /dev/null
+++ b/packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"cryptoImplementation.d.ts","sourceRoot":"","sources":["cryptoImplementation.ts"],"names":[],"mappings":"AAgBA;;;;;;GAMG;AAEH;;GAEG;AAEH,OAAO,EACL,UAAU,EACV,kBAAkB,EAElB,oBAAoB,EACpB,WAAW,EACX,OAAO,EAEP,yBAAyB,EAC1B,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EAAE,qBAAqB,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAC9E,OAAO,EACL,eAAe,EACf,sBAAsB,EACtB,uBAAuB,EACvB,WAAW,EACZ,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,UAAU,EAAW,MAAM,oBAAoB,CAAC;AAgGzD,qBAAa,oBAAoB;IAC/B,MAAM,CAAC,aAAa,UAAS;IAE7B;;;OAGG;IACH,cAAc,CAAC,GAAG,EAAE,uBAAuB,GAAG,sBAAsB;IAoCpE;;OAEG;IACH,iBAAiB,CAAC,KAAK,EAAE,kBAAkB,GAAG,WAAW;IAmBzD;;OAEG;IACH,mBAAmB,CAAC,IAAI,EAAE,UAAU,GAAG,aAAa;IAoBpD;;OAEG;IACH,uBAAuB,CACrB,GAAG,EAAE,MAAM,EACX,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,MAAM,GAClB,OAAO;IASV;;OAEG;IACH,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO;IAarE;;OAEG;IACH,YAAY,CAAC,KAAK,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO;IAmBnE,kBAAkB,CAChB,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,MAAM,GAChB,OAAO;IAWV;;OAEG;IACH,kBAAkB,IAAI;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE;IAQnD;;OAEG;IACH,UAAU,CAAC,UAAU,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM;IAS9D;;OAEG;IACH,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO;IAIvD;;;OAGG;IACH,qBAAqB,CAAC,WAAW,EAAE,WAAW,GAAG,qBAAqB;IAyBtE;;OAEG;IACH,oBAAoB,CAClB,eAAe,EAAE,MAAM,EACvB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,UAAU,EACpB,aAAa,EAAE,yBAAyB,EACxC,OAAO,EAAE,UAAU,GAClB,oBAAoB;IA2HvB;;OAEG;IACH,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM;IAK/B;;OAEG;IACH,WAAW,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM;IAIzC,YAAY,CACV,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,EACpB,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,MAAM,GACb,MAAM;IAaT,SAAS,CAAC,WAAW,EAAE,MAAM,GAAG,eAAe;CAsDhD"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts b/packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts
new file mode 100644
index 000000000..4195ebded
--- /dev/null
+++ b/packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts
@@ -0,0 +1,579 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019-2020 Taler Systems SA
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Synchronous implementation of crypto-related functions for the wallet.
+ *
+ * The functionality is parameterized over an Emscripten environment.
+ *
+ * @author Florian Dold <dold@taler.net>
+ */
+
+/**
+ * Imports.
+ */
+
+import {
+ CoinRecord,
+ DenominationRecord,
+ RefreshPlanchetRecord,
+ RefreshSessionRecord,
+ TipPlanchet,
+ WireFee,
+ CoinSourceType,
+ DenominationSelectionInfo,
+} from "../../types/dbTypes";
+
+import { CoinDepositPermission, RecoupRequest } from "../../types/talerTypes";
+import {
+ BenchmarkResult,
+ PlanchetCreationResult,
+ PlanchetCreationRequest,
+ DepositInfo,
+} from "../../types/walletTypes";
+import { AmountJson, Amounts } from "../../util/amounts";
+import * as timer from "../../util/timer";
+import {
+ encodeCrock,
+ decodeCrock,
+ createEddsaKeyPair,
+ createBlindingKeySecret,
+ hash,
+ rsaBlind,
+ eddsaVerify,
+ eddsaSign,
+ rsaUnblind,
+ stringToBytes,
+ createHashContext,
+ createEcdheKeyPair,
+ keyExchangeEcdheEddsa,
+ setupRefreshPlanchet,
+ rsaVerify,
+} from "../talerCrypto";
+import { randomBytes } from "../primitives/nacl-fast";
+import { kdf } from "../primitives/kdf";
+import {
+ Timestamp,
+ getTimestampNow,
+ timestampTruncateToSecond,
+} from "../../util/time";
+
+enum SignaturePurpose {
+ WALLET_RESERVE_WITHDRAW = 1200,
+ WALLET_COIN_DEPOSIT = 1201,
+ MASTER_DENOMINATION_KEY_VALIDITY = 1025,
+ MASTER_WIRE_FEES = 1028,
+ MASTER_WIRE_DETAILS = 1030,
+ WALLET_COIN_MELT = 1202,
+ TEST = 4242,
+ MERCHANT_PAYMENT_OK = 1104,
+ WALLET_COIN_RECOUP = 1203,
+ WALLET_COIN_LINK = 1204,
+ EXCHANGE_CONFIRM_RECOUP = 1039,
+ EXCHANGE_CONFIRM_RECOUP_REFRESH = 1041,
+}
+
+function amountToBuffer(amount: AmountJson): Uint8Array {
+ const buffer = new ArrayBuffer(8 + 4 + 12);
+ const dvbuf = new DataView(buffer);
+ const u8buf = new Uint8Array(buffer);
+ const curr = stringToBytes(amount.currency);
+ dvbuf.setBigUint64(0, BigInt(amount.value));
+ dvbuf.setUint32(8, amount.fraction);
+ u8buf.set(curr, 8 + 4);
+
+ return u8buf;
+}
+
+function timestampRoundedToBuffer(ts: Timestamp): Uint8Array {
+ const b = new ArrayBuffer(8);
+ const v = new DataView(b);
+ const tsRounded = timestampTruncateToSecond(ts);
+ const s = BigInt(tsRounded.t_ms) * BigInt(1000);
+ v.setBigUint64(0, s);
+ return new Uint8Array(b);
+}
+
+class SignaturePurposeBuilder {
+ private chunks: Uint8Array[] = [];
+
+ constructor(private purposeNum: number) {}
+
+ put(bytes: Uint8Array): SignaturePurposeBuilder {
+ this.chunks.push(Uint8Array.from(bytes));
+ return this;
+ }
+
+ build(): Uint8Array {
+ let payloadLen = 0;
+ for (const c of this.chunks) {
+ payloadLen += c.byteLength;
+ }
+ const buf = new ArrayBuffer(4 + 4 + payloadLen);
+ const u8buf = new Uint8Array(buf);
+ let p = 8;
+ for (const c of this.chunks) {
+ u8buf.set(c, p);
+ p += c.byteLength;
+ }
+ const dvbuf = new DataView(buf);
+ dvbuf.setUint32(0, payloadLen + 4 + 4);
+ dvbuf.setUint32(4, this.purposeNum);
+ return u8buf;
+ }
+}
+
+function buildSigPS(purposeNum: number): SignaturePurposeBuilder {
+ return new SignaturePurposeBuilder(purposeNum);
+}
+
+export class CryptoImplementation {
+ static enableTracing = false;
+
+ /**
+ * Create a pre-coin of the given denomination to be withdrawn from then given
+ * reserve.
+ */
+ createPlanchet(req: PlanchetCreationRequest): PlanchetCreationResult {
+ const reservePub = decodeCrock(req.reservePub);
+ const reservePriv = decodeCrock(req.reservePriv);
+ const denomPub = decodeCrock(req.denomPub);
+ const coinKeyPair = createEddsaKeyPair();
+ const blindingFactor = createBlindingKeySecret();
+ const coinPubHash = hash(coinKeyPair.eddsaPub);
+ const ev = rsaBlind(coinPubHash, blindingFactor, denomPub);
+ const amountWithFee = Amounts.add(req.value, req.feeWithdraw).amount;
+ const denomPubHash = hash(denomPub);
+ const evHash = hash(ev);
+
+ const withdrawRequest = buildSigPS(SignaturePurpose.WALLET_RESERVE_WITHDRAW)
+ .put(reservePub)
+ .put(amountToBuffer(amountWithFee))
+ .put(denomPubHash)
+ .put(evHash)
+ .build();
+
+ const sig = eddsaSign(withdrawRequest, reservePriv);
+
+ const planchet: PlanchetCreationResult = {
+ blindingKey: encodeCrock(blindingFactor),
+ coinEv: encodeCrock(ev),
+ coinPriv: encodeCrock(coinKeyPair.eddsaPriv),
+ coinPub: encodeCrock(coinKeyPair.eddsaPub),
+ coinValue: req.value,
+ denomPub: encodeCrock(denomPub),
+ denomPubHash: encodeCrock(denomPubHash),
+ reservePub: encodeCrock(reservePub),
+ withdrawSig: encodeCrock(sig),
+ coinEvHash: encodeCrock(evHash),
+ };
+ return planchet;
+ }
+
+ /**
+ * Create a planchet used for tipping, including the private keys.
+ */
+ createTipPlanchet(denom: DenominationRecord): TipPlanchet {
+ const denomPub = decodeCrock(denom.denomPub);
+ const coinKeyPair = createEddsaKeyPair();
+ const blindingFactor = createBlindingKeySecret();
+ const coinPubHash = hash(coinKeyPair.eddsaPub);
+ const ev = rsaBlind(coinPubHash, blindingFactor, denomPub);
+
+ const tipPlanchet: TipPlanchet = {
+ blindingKey: encodeCrock(blindingFactor),
+ coinEv: encodeCrock(ev),
+ coinPriv: encodeCrock(coinKeyPair.eddsaPriv),
+ coinPub: encodeCrock(coinKeyPair.eddsaPub),
+ coinValue: denom.value,
+ denomPub: encodeCrock(denomPub),
+ denomPubHash: encodeCrock(hash(denomPub)),
+ };
+ return tipPlanchet;
+ }
+
+ /**
+ * Create and sign a message to recoup a coin.
+ */
+ createRecoupRequest(coin: CoinRecord): RecoupRequest {
+ const p = buildSigPS(SignaturePurpose.WALLET_COIN_RECOUP)
+ .put(decodeCrock(coin.coinPub))
+ .put(decodeCrock(coin.denomPubHash))
+ .put(decodeCrock(coin.blindingKey))
+ .build();
+
+ const coinPriv = decodeCrock(coin.coinPriv);
+ const coinSig = eddsaSign(p, coinPriv);
+ const paybackRequest: RecoupRequest = {
+ coin_blind_key_secret: coin.blindingKey,
+ coin_pub: coin.coinPub,
+ coin_sig: encodeCrock(coinSig),
+ denom_pub_hash: coin.denomPubHash,
+ denom_sig: coin.denomSig,
+ refreshed: coin.coinSource.type === CoinSourceType.Refresh,
+ };
+ return paybackRequest;
+ }
+
+ /**
+ * Check if a payment signature is valid.
+ */
+ isValidPaymentSignature(
+ sig: string,
+ contractHash: string,
+ merchantPub: string,
+ ): boolean {
+ const p = buildSigPS(SignaturePurpose.MERCHANT_PAYMENT_OK)
+ .put(decodeCrock(contractHash))
+ .build();
+ const sigBytes = decodeCrock(sig);
+ const pubBytes = decodeCrock(merchantPub);
+ return eddsaVerify(p, sigBytes, pubBytes);
+ }
+
+ /**
+ * Check if a wire fee is correctly signed.
+ */
+ isValidWireFee(type: string, wf: WireFee, masterPub: string): boolean {
+ const p = buildSigPS(SignaturePurpose.MASTER_WIRE_FEES)
+ .put(hash(stringToBytes(type + "\0")))
+ .put(timestampRoundedToBuffer(wf.startStamp))
+ .put(timestampRoundedToBuffer(wf.endStamp))
+ .put(amountToBuffer(wf.wireFee))
+ .put(amountToBuffer(wf.closingFee))
+ .build();
+ const sig = decodeCrock(wf.sig);
+ const pub = decodeCrock(masterPub);
+ return eddsaVerify(p, sig, pub);
+ }
+
+ /**
+ * Check if the signature of a denomination is valid.
+ */
+ isValidDenom(denom: DenominationRecord, masterPub: string): boolean {
+ const p = buildSigPS(SignaturePurpose.MASTER_DENOMINATION_KEY_VALIDITY)
+ .put(decodeCrock(masterPub))
+ .put(timestampRoundedToBuffer(denom.stampStart))
+ .put(timestampRoundedToBuffer(denom.stampExpireWithdraw))
+ .put(timestampRoundedToBuffer(denom.stampExpireDeposit))
+ .put(timestampRoundedToBuffer(denom.stampExpireLegal))
+ .put(amountToBuffer(denom.value))
+ .put(amountToBuffer(denom.feeWithdraw))
+ .put(amountToBuffer(denom.feeDeposit))
+ .put(amountToBuffer(denom.feeRefresh))
+ .put(amountToBuffer(denom.feeRefund))
+ .put(decodeCrock(denom.denomPubHash))
+ .build();
+ const sig = decodeCrock(denom.masterSig);
+ const pub = decodeCrock(masterPub);
+ return eddsaVerify(p, sig, pub);
+ }
+
+ isValidWireAccount(
+ paytoUri: string,
+ sig: string,
+ masterPub: string,
+ ): boolean {
+ const h = kdf(
+ 64,
+ stringToBytes("exchange-wire-signature"),
+ stringToBytes(paytoUri + "\0"),
+ new Uint8Array(0),
+ );
+ const p = buildSigPS(SignaturePurpose.MASTER_WIRE_DETAILS).put(h).build();
+ return eddsaVerify(p, decodeCrock(sig), decodeCrock(masterPub));
+ }
+
+ /**
+ * Create a new EdDSA key pair.
+ */
+ createEddsaKeypair(): { priv: string; pub: string } {
+ const pair = createEddsaKeyPair();
+ return {
+ priv: encodeCrock(pair.eddsaPriv),
+ pub: encodeCrock(pair.eddsaPub),
+ };
+ }
+
+ /**
+ * Unblind a blindly signed value.
+ */
+ rsaUnblind(blindedSig: string, bk: string, pk: string): string {
+ const denomSig = rsaUnblind(
+ decodeCrock(blindedSig),
+ decodeCrock(pk),
+ decodeCrock(bk),
+ );
+ return encodeCrock(denomSig);
+ }
+
+ /**
+ * Unblind a blindly signed value.
+ */
+ rsaVerify(hm: string, sig: string, pk: string): boolean {
+ return rsaVerify(hash(decodeCrock(hm)), decodeCrock(sig), decodeCrock(pk));
+ }
+
+ /**
+ * Generate updated coins (to store in the database)
+ * and deposit permissions for each given coin.
+ */
+ signDepositPermission(depositInfo: DepositInfo): CoinDepositPermission {
+ const d = buildSigPS(SignaturePurpose.WALLET_COIN_DEPOSIT)
+ .put(decodeCrock(depositInfo.contractTermsHash))
+ .put(decodeCrock(depositInfo.wireInfoHash))
+ .put(decodeCrock(depositInfo.denomPubHash))
+ .put(timestampRoundedToBuffer(depositInfo.timestamp))
+ .put(timestampRoundedToBuffer(depositInfo.refundDeadline))
+ .put(amountToBuffer(depositInfo.spendAmount))
+ .put(amountToBuffer(depositInfo.feeDeposit))
+ .put(decodeCrock(depositInfo.merchantPub))
+ .put(decodeCrock(depositInfo.coinPub))
+ .build();
+ const coinSig = eddsaSign(d, decodeCrock(depositInfo.coinPriv));
+
+ const s: CoinDepositPermission = {
+ coin_pub: depositInfo.coinPub,
+ coin_sig: encodeCrock(coinSig),
+ contribution: Amounts.stringify(depositInfo.spendAmount),
+ h_denom: depositInfo.denomPubHash,
+ exchange_url: depositInfo.exchangeBaseUrl,
+ ub_sig: depositInfo.denomSig,
+ };
+ return s;
+ }
+
+ /**
+ * Create a new refresh session.
+ */
+ createRefreshSession(
+ exchangeBaseUrl: string,
+ kappa: number,
+ meltCoin: CoinRecord,
+ newCoinDenoms: DenominationSelectionInfo,
+ meltFee: AmountJson,
+ ): RefreshSessionRecord {
+ const currency = newCoinDenoms.selectedDenoms[0].denom.value.currency;
+ let valueWithFee = Amounts.getZero(currency);
+
+ for (const ncd of newCoinDenoms.selectedDenoms) {
+ const t = Amounts.add(ncd.denom.value, ncd.denom.feeWithdraw).amount;
+ valueWithFee = Amounts.add(
+ valueWithFee,
+ Amounts.mult(t, ncd.count).amount,
+ ).amount;
+ }
+
+ // melt fee
+ valueWithFee = Amounts.add(valueWithFee, meltFee).amount;
+
+ const sessionHc = createHashContext();
+
+ const transferPubs: string[] = [];
+ const transferPrivs: string[] = [];
+
+ const planchetsForGammas: RefreshPlanchetRecord[][] = [];
+
+ for (let i = 0; i < kappa; i++) {
+ const transferKeyPair = createEcdheKeyPair();
+ sessionHc.update(transferKeyPair.ecdhePub);
+ transferPrivs.push(encodeCrock(transferKeyPair.ecdhePriv));
+ transferPubs.push(encodeCrock(transferKeyPair.ecdhePub));
+ }
+
+ for (const denomSel of newCoinDenoms.selectedDenoms) {
+ for (let i = 0; i < denomSel.count; i++) {
+ const r = decodeCrock(denomSel.denom.denomPub);
+ sessionHc.update(r);
+ }
+ }
+
+ sessionHc.update(decodeCrock(meltCoin.coinPub));
+ sessionHc.update(amountToBuffer(valueWithFee));
+
+ for (let i = 0; i < kappa; i++) {
+ const planchets: RefreshPlanchetRecord[] = [];
+ for (let j = 0; j < newCoinDenoms.selectedDenoms.length; j++) {
+ const denomSel = newCoinDenoms.selectedDenoms[j];
+ for (let k = 0; k < denomSel.count; k++) {
+ const coinNumber = planchets.length;
+ const transferPriv = decodeCrock(transferPrivs[i]);
+ const oldCoinPub = decodeCrock(meltCoin.coinPub);
+ const transferSecret = keyExchangeEcdheEddsa(
+ transferPriv,
+ oldCoinPub,
+ );
+ const fresh = setupRefreshPlanchet(transferSecret, coinNumber);
+ const coinPriv = fresh.coinPriv;
+ const coinPub = fresh.coinPub;
+ const blindingFactor = fresh.bks;
+ const pubHash = hash(coinPub);
+ const denomPub = decodeCrock(denomSel.denom.denomPub);
+ const ev = rsaBlind(pubHash, blindingFactor, denomPub);
+ const planchet: RefreshPlanchetRecord = {
+ blindingKey: encodeCrock(blindingFactor),
+ coinEv: encodeCrock(ev),
+ privateKey: encodeCrock(coinPriv),
+ publicKey: encodeCrock(coinPub),
+ };
+ planchets.push(planchet);
+ sessionHc.update(ev);
+ }
+ }
+ planchetsForGammas.push(planchets);
+ }
+
+ const sessionHash = sessionHc.finish();
+
+ const confirmData = buildSigPS(SignaturePurpose.WALLET_COIN_MELT)
+ .put(sessionHash)
+ .put(decodeCrock(meltCoin.denomPubHash))
+ .put(amountToBuffer(valueWithFee))
+ .put(amountToBuffer(meltFee))
+ .put(decodeCrock(meltCoin.coinPub))
+ .build();
+
+ const confirmSig = eddsaSign(confirmData, decodeCrock(meltCoin.coinPriv));
+
+ let valueOutput = Amounts.getZero(currency);
+ for (const denomSel of newCoinDenoms.selectedDenoms) {
+ const denom = denomSel.denom;
+ for (let i = 0; i < denomSel.count; i++) {
+ valueOutput = Amounts.add(valueOutput, denom.value).amount;
+ }
+ }
+
+ const newDenoms: string[] = [];
+ const newDenomHashes: string[] = [];
+
+ for (const denomSel of newCoinDenoms.selectedDenoms) {
+ const denom = denomSel.denom;
+ for (let i = 0; i < denomSel.count; i++) {
+ newDenoms.push(denom.denomPub);
+ newDenomHashes.push(denom.denomPubHash);
+ }
+ }
+
+ const refreshSession: RefreshSessionRecord = {
+ confirmSig: encodeCrock(confirmSig),
+ exchangeBaseUrl,
+ hash: encodeCrock(sessionHash),
+ meltCoinPub: meltCoin.coinPub,
+ newDenomHashes,
+ newDenoms,
+ norevealIndex: undefined,
+ planchetsForGammas: planchetsForGammas,
+ transferPrivs,
+ transferPubs,
+ amountRefreshOutput: valueOutput,
+ amountRefreshInput: valueWithFee,
+ timestampCreated: getTimestampNow(),
+ finishedTimestamp: undefined,
+ lastError: undefined,
+ };
+
+ return refreshSession;
+ }
+
+ /**
+ * Hash a string including the zero terminator.
+ */
+ hashString(str: string): string {
+ const b = stringToBytes(str + "\0");
+ return encodeCrock(hash(b));
+ }
+
+ /**
+ * Hash a crockford encoded value.
+ */
+ hashEncoded(encodedBytes: string): string {
+ return encodeCrock(hash(decodeCrock(encodedBytes)));
+ }
+
+ signCoinLink(
+ oldCoinPriv: string,
+ newDenomHash: string,
+ oldCoinPub: string,
+ transferPub: string,
+ coinEv: string,
+ ): string {
+ const coinEvHash = hash(decodeCrock(coinEv));
+ const coinLink = buildSigPS(SignaturePurpose.WALLET_COIN_LINK)
+ .put(decodeCrock(newDenomHash))
+ .put(decodeCrock(oldCoinPub))
+ .put(decodeCrock(transferPub))
+ .put(coinEvHash)
+ .build();
+ const coinPriv = decodeCrock(oldCoinPriv);
+ const sig = eddsaSign(coinLink, coinPriv);
+ return encodeCrock(sig);
+ }
+
+ benchmark(repetitions: number): BenchmarkResult {
+ let time_hash = 0;
+ for (let i = 0; i < repetitions; i++) {
+ const start = timer.performanceNow();
+ this.hashString("hello world");
+ time_hash += timer.performanceNow() - start;
+ }
+
+ let time_hash_big = 0;
+ for (let i = 0; i < repetitions; i++) {
+ const ba = randomBytes(4096);
+ const start = timer.performanceNow();
+ hash(ba);
+ time_hash_big += timer.performanceNow() - start;
+ }
+
+ let time_eddsa_create = 0;
+ for (let i = 0; i < repetitions; i++) {
+ const start = timer.performanceNow();
+ createEddsaKeyPair();
+ time_eddsa_create += timer.performanceNow() - start;
+ }
+
+ let time_eddsa_sign = 0;
+ const p = randomBytes(4096);
+
+ const pair = createEddsaKeyPair();
+
+ for (let i = 0; i < repetitions; i++) {
+ const start = timer.performanceNow();
+ eddsaSign(p, pair.eddsaPriv);
+ time_eddsa_sign += timer.performanceNow() - start;
+ }
+
+ const sig = eddsaSign(p, pair.eddsaPriv);
+
+ let time_eddsa_verify = 0;
+ for (let i = 0; i < repetitions; i++) {
+ const start = timer.performanceNow();
+ eddsaVerify(p, sig, pair.eddsaPub);
+ time_eddsa_verify += timer.performanceNow() - start;
+ }
+
+ return {
+ repetitions,
+ time: {
+ hash_small: time_hash,
+ hash_big: time_hash_big,
+ eddsa_create: time_eddsa_create,
+ eddsa_sign: time_eddsa_sign,
+ eddsa_verify: time_eddsa_verify,
+ },
+ };
+ }
+}
diff --git a/packages/taler-wallet-core/src/crypto/workers/cryptoWorker.d.ts.map b/packages/taler-wallet-core/src/crypto/workers/cryptoWorker.d.ts.map
new file mode 100644
index 000000000..cfdedef09
--- /dev/null
+++ b/packages/taler-wallet-core/src/crypto/workers/cryptoWorker.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"cryptoWorker.d.ts","sourceRoot":"","sources":["cryptoWorker.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,YAAY;IAC3B,WAAW,CAAC,OAAO,EAAE,GAAG,GAAG,IAAI,CAAC;IAEhC,SAAS,IAAI,IAAI,CAAC;IAElB,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,KAAK,IAAI,CAAC,GAAG,SAAS,CAAC;IAC1C,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,KAAK,IAAI,CAAC,GAAG,SAAS,CAAC;CACzC"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/crypto/workers/cryptoWorker.ts b/packages/taler-wallet-core/src/crypto/workers/cryptoWorker.ts
new file mode 100644
index 000000000..9f3ee6f50
--- /dev/null
+++ b/packages/taler-wallet-core/src/crypto/workers/cryptoWorker.ts
@@ -0,0 +1,8 @@
+export interface CryptoWorker {
+ postMessage(message: any): void;
+
+ terminate(): void;
+
+ onmessage: ((m: any) => void) | undefined;
+ onerror: ((m: any) => void) | undefined;
+}
diff --git a/packages/taler-wallet-core/src/crypto/workers/nodeThreadWorker.d.ts.map b/packages/taler-wallet-core/src/crypto/workers/nodeThreadWorker.d.ts.map
new file mode 100644
index 000000000..d89774cd5
--- /dev/null
+++ b/packages/taler-wallet-core/src/crypto/workers/nodeThreadWorker.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"nodeThreadWorker.d.ts","sourceRoot":"","sources":["nodeThreadWorker.ts"],"names":[],"mappings":"AAgBA;;GAEG;AACH,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAiC9C;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,GAAG,GAAG,IAAI,CA6ClD;AAED,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,KAAK,GAAG,IAAI,CAEhD;AAED,qBAAa,6BAA8B,YAAW,mBAAmB;IACvE,WAAW,IAAI,YAAY;IAO3B,cAAc,IAAI,MAAM;CAGzB"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/crypto/workers/nodeThreadWorker.ts b/packages/taler-wallet-core/src/crypto/workers/nodeThreadWorker.ts
new file mode 100644
index 000000000..6c9dfc569
--- /dev/null
+++ b/packages/taler-wallet-core/src/crypto/workers/nodeThreadWorker.ts
@@ -0,0 +1,183 @@
+/*
+ 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/>
+ */
+
+/**
+ * Imports
+ */
+import { CryptoWorkerFactory } from "./cryptoApi";
+import { CryptoWorker } from "./cryptoWorker";
+import os from "os";
+import { CryptoImplementation } from "./cryptoImplementation";
+
+const f = __filename;
+
+const workerCode = `
+ // Try loading the glue library for Android
+ try {
+ require("akono");
+ } catch (e) {
+ // Probably we're not on Android ...
+ }
+ const worker_threads = require('worker_threads');
+ const parentPort = worker_threads.parentPort;
+ let tw;
+ try {
+ tw = require("${f}");
+ } catch (e) {
+ console.log("could not load from ${f}");
+ }
+ if (!tw) {
+ try {
+ tw = require("taler-wallet-android");
+ } catch (e) {
+ console.log("could not load taler-wallet-android either");
+ throw e;
+ }
+ }
+ parentPort.on("message", tw.handleWorkerMessage);
+ parentPort.on("error", tw.handleWorkerError);
+`;
+
+/**
+ * This function is executed in the worker thread to handle
+ * a message.
+ */
+export function handleWorkerMessage(msg: any): void {
+ const args = msg.args;
+ if (!Array.isArray(args)) {
+ console.error("args must be array");
+ return;
+ }
+ const id = msg.id;
+ if (typeof id !== "number") {
+ console.error("RPC id must be number");
+ return;
+ }
+ const operation = msg.operation;
+ if (typeof operation !== "string") {
+ console.error("RPC operation must be string");
+ return;
+ }
+
+ const handleRequest = async (): Promise<void> => {
+ const impl = new CryptoImplementation();
+
+ if (!(operation in impl)) {
+ console.error(`crypto operation '${operation}' not found`);
+ return;
+ }
+
+ try {
+ const result = (impl as any)[operation](...args);
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
+ const worker_threads = require("worker_threads");
+ const p = worker_threads.parentPort;
+ worker_threads.parentPort?.postMessage;
+ if (p) {
+ p.postMessage({ data: { result, id } });
+ } else {
+ console.error("parent port not available (not running in thread?");
+ }
+ } catch (e) {
+ console.error("error during operation", e);
+ return;
+ }
+ };
+
+ handleRequest().catch((e) => {
+ console.error("error in node worker", e);
+ });
+}
+
+export function handleWorkerError(e: Error): void {
+ console.log("got error from worker", e);
+}
+
+export class NodeThreadCryptoWorkerFactory implements CryptoWorkerFactory {
+ startWorker(): CryptoWorker {
+ if (typeof require === "undefined") {
+ throw Error("cannot make worker, require(...) not defined");
+ }
+ return new NodeThreadCryptoWorker();
+ }
+
+ getConcurrency(): number {
+ return Math.max(1, os.cpus().length - 1);
+ }
+}
+
+/**
+ * Worker implementation that uses node subprocesses.
+ */
+class NodeThreadCryptoWorker implements CryptoWorker {
+ /**
+ * Function to be called when we receive a message from the worker thread.
+ */
+ onmessage: undefined | ((m: any) => void);
+
+ /**
+ * Function to be called when we receive an error from the worker thread.
+ */
+ onerror: undefined | ((m: any) => void);
+
+ private nodeWorker: import("worker_threads").Worker;
+
+ constructor() {
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
+ const worker_threads = require("worker_threads");
+ this.nodeWorker = new worker_threads.Worker(workerCode, { eval: true });
+ this.nodeWorker.on("error", (err: Error) => {
+ console.error("error in node worker:", err);
+ if (this.onerror) {
+ this.onerror(err);
+ }
+ });
+ this.nodeWorker.on("message", (v: any) => {
+ if (this.onmessage) {
+ this.onmessage(v);
+ }
+ });
+ this.nodeWorker.unref();
+ }
+
+ /**
+ * Add an event listener for either an "error" or "message" event.
+ */
+ addEventListener(event: "message" | "error", fn: (x: any) => void): void {
+ switch (event) {
+ case "message":
+ this.onmessage = fn;
+ break;
+ case "error":
+ this.onerror = fn;
+ break;
+ }
+ }
+
+ /**
+ * Send a message to the worker thread.
+ */
+ postMessage(msg: any): void {
+ this.nodeWorker.postMessage(msg);
+ }
+
+ /**
+ * Forcibly terminate the worker thread.
+ */
+ terminate(): void {
+ this.nodeWorker.terminate();
+ }
+}
diff --git a/packages/taler-wallet-core/src/crypto/workers/synchronousWorker.ts b/packages/taler-wallet-core/src/crypto/workers/synchronousWorker.ts
new file mode 100644
index 000000000..5327670bd
--- /dev/null
+++ b/packages/taler-wallet-core/src/crypto/workers/synchronousWorker.ts
@@ -0,0 +1,136 @@
+/*
+ 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 { CryptoImplementation } from "./cryptoImplementation";
+
+import { CryptoWorkerFactory } from "./cryptoApi";
+import { CryptoWorker } from "./cryptoWorker";
+
+/**
+ * The synchronous crypto worker produced by this factory doesn't run in the
+ * background, but actually blocks the caller until the operation is done.
+ */
+export class SynchronousCryptoWorkerFactory implements CryptoWorkerFactory {
+ startWorker(): CryptoWorker {
+ if (typeof require === "undefined") {
+ throw Error("cannot make worker, require(...) not defined");
+ }
+ return new SynchronousCryptoWorker();
+ }
+
+ getConcurrency(): number {
+ return 1;
+ }
+}
+
+/**
+ * Worker implementation that uses node subprocesses.
+ */
+export class SynchronousCryptoWorker {
+ /**
+ * Function to be called when we receive a message from the worker thread.
+ */
+ onmessage: undefined | ((m: any) => void);
+
+ /**
+ * Function to be called when we receive an error from the worker thread.
+ */
+ onerror: undefined | ((m: any) => void);
+
+ constructor() {
+ this.onerror = undefined;
+ this.onmessage = undefined;
+ }
+
+ /**
+ * Add an event listener for either an "error" or "message" event.
+ */
+ addEventListener(event: "message" | "error", fn: (x: any) => void): void {
+ switch (event) {
+ case "message":
+ this.onmessage = fn;
+ break;
+ case "error":
+ this.onerror = fn;
+ break;
+ }
+ }
+
+ private dispatchMessage(msg: any): void {
+ if (this.onmessage) {
+ this.onmessage({ data: msg });
+ }
+ }
+
+ private async handleRequest(
+ operation: string,
+ id: number,
+ args: string[],
+ ): Promise<void> {
+ const impl = new CryptoImplementation();
+
+ if (!(operation in impl)) {
+ console.error(`crypto operation '${operation}' not found`);
+ return;
+ }
+
+ let result: any;
+ try {
+ result = (impl as any)[operation](...args);
+ } catch (e) {
+ console.log("error during operation", e);
+ return;
+ }
+
+ try {
+ setTimeout(() => this.dispatchMessage({ result, id }), 0);
+ } catch (e) {
+ console.log("got error during dispatch", e);
+ }
+ }
+
+ /**
+ * Send a message to the worker thread.
+ */
+ postMessage(msg: any): void {
+ const args = msg.args;
+ if (!Array.isArray(args)) {
+ console.error("args must be array");
+ return;
+ }
+ const id = msg.id;
+ if (typeof id !== "number") {
+ console.error("RPC id must be number");
+ return;
+ }
+ const operation = msg.operation;
+ if (typeof operation !== "string") {
+ console.error("RPC operation must be string");
+ return;
+ }
+
+ this.handleRequest(operation, id, args).catch((e) => {
+ console.error("Error while handling crypto request:", e);
+ });
+ }
+
+ /**
+ * Forcibly terminate the worker thread.
+ */
+ terminate(): void {
+ // This is a no-op.
+ }
+}
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts
new file mode 100644
index 000000000..a6eeb1205
--- /dev/null
+++ b/packages/taler-wallet-core/src/db.ts
@@ -0,0 +1,66 @@
+import { Stores } from "./types/dbTypes";
+import { openDatabase, Database, Store, Index } from "./util/query";
+import type { idbtypes } from "idb-bridge";
+
+/**
+ * Name of the Taler database. The name includes the
+ * major version of the DB schema. The version should be incremented
+ * with each major change. When incrementing the major version,
+ * the wallet should import data from the previous version.
+ */
+const TALER_DB_NAME = "taler-walletdb-v7";
+
+/**
+ * Current database minor version, should be incremented
+ * each time we do minor schema changes on the database.
+ * A change is considered minor when fields are added in a
+ * backwards-compatible way or object stores and indices
+ * are added.
+ */
+export const WALLET_DB_MINOR_VERSION = 1;
+
+/**
+ * Return a promise that resolves
+ * to the taler wallet db.
+ */
+export function openTalerDatabase(
+ idbFactory: idbtypes.IDBFactory,
+ onVersionChange: () => void,
+): Promise<idbtypes.IDBDatabase> {
+ const onUpgradeNeeded = (
+ db: idbtypes.IDBDatabase,
+ oldVersion: number,
+ newVersion: number,
+ ): void => {
+ switch (oldVersion) {
+ case 0: // DB does not exist yet
+ for (const n in Stores) {
+ if ((Stores as any)[n] instanceof Store) {
+ const si: Store<any> = (Stores as any)[n];
+ const s = db.createObjectStore(si.name, si.storeParams);
+ for (const indexName in si as any) {
+ if ((si as any)[indexName] instanceof Index) {
+ const ii: Index<any, any> = (si as any)[indexName];
+ s.createIndex(ii.indexName, ii.keyPath, ii.options);
+ }
+ }
+ }
+ }
+ break;
+ default:
+ throw Error("unsupported existig DB version");
+ }
+ };
+
+ return openDatabase(
+ idbFactory,
+ TALER_DB_NAME,
+ WALLET_DB_MINOR_VERSION,
+ onVersionChange,
+ onUpgradeNeeded,
+ );
+}
+
+export function deleteTalerDatabase(idbFactory: idbtypes.IDBFactory): void {
+ Database.deleteDatabase(idbFactory, TALER_DB_NAME);
+}
diff --git a/packages/taler-wallet-core/src/headless/NodeHttpLib.d.ts.map b/packages/taler-wallet-core/src/headless/NodeHttpLib.d.ts.map
new file mode 100644
index 000000000..06ba7a3e1
--- /dev/null
+++ b/packages/taler-wallet-core/src/headless/NodeHttpLib.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"NodeHttpLib.d.ts","sourceRoot":"","sources":["NodeHttpLib.ts"],"names":[],"mappings":"AAkBA;;GAEG;AACH,OAAO,EAEL,kBAAkB,EAClB,kBAAkB,EAClB,YAAY,EACb,MAAM,cAAc,CAAC;AAMtB;;GAEG;AACH,qBAAa,WAAY,YAAW,kBAAkB;IACpD,OAAO,CAAC,QAAQ,CAA0B;IAC1C,OAAO,CAAC,iBAAiB,CAAQ;IAEjC;;OAEG;IACH,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;YAIvB,GAAG;IA2EX,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,YAAY,CAAC;IAIjE,QAAQ,CACZ,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,GAAG,EACT,GAAG,CAAC,EAAE,kBAAkB,GACvB,OAAO,CAAC,YAAY,CAAC;CAGzB"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/headless/NodeHttpLib.ts b/packages/taler-wallet-core/src/headless/NodeHttpLib.ts
new file mode 100644
index 000000000..d109c3b7c
--- /dev/null
+++ b/packages/taler-wallet-core/src/headless/NodeHttpLib.ts
@@ -0,0 +1,133 @@
+/*
+ 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/>
+
+ SPDX-License-Identifier: AGPL3.0-or-later
+*/
+
+/**
+ * Imports.
+ */
+import {
+ Headers,
+ HttpRequestLibrary,
+ HttpRequestOptions,
+ HttpResponse,
+} from "../util/http";
+import { RequestThrottler } from "../util/RequestThrottler";
+import Axios from "axios";
+import { OperationFailedError, makeErrorDetails } from "../operations/errors";
+import { TalerErrorCode } from "../TalerErrorCode";
+
+/**
+ * Implementation of the HTTP request library interface for node.
+ */
+export class NodeHttpLib implements HttpRequestLibrary {
+ private throttle = new RequestThrottler();
+ private throttlingEnabled = true;
+
+ /**
+ * Set whether requests should be throttled.
+ */
+ setThrottling(enabled: boolean): void {
+ this.throttlingEnabled = enabled;
+ }
+
+ private async req(
+ method: "post" | "get",
+ url: string,
+ body: any,
+ opt?: HttpRequestOptions,
+ ): Promise<HttpResponse> {
+ if (this.throttlingEnabled && this.throttle.applyThrottle(url)) {
+ throw Error("request throttled");
+ }
+ const resp = await Axios({
+ method,
+ url: url,
+ responseType: "text",
+ headers: opt?.headers,
+ validateStatus: () => true,
+ transformResponse: (x) => x,
+ data: body,
+ });
+
+ const respText = resp.data;
+ if (typeof respText !== "string") {
+ throw new OperationFailedError(
+ makeErrorDetails(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ "unexpected response type",
+ {
+ httpStatusCode: resp.status,
+ requestUrl: url,
+ },
+ ),
+ );
+ }
+ const makeJson = async (): Promise<any> => {
+ let responseJson;
+ try {
+ responseJson = JSON.parse(respText);
+ } catch (e) {
+ throw new OperationFailedError(
+ makeErrorDetails(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ "invalid JSON",
+ {
+ httpStatusCode: resp.status,
+ requestUrl: url,
+ },
+ ),
+ );
+ }
+ if (responseJson === null || typeof responseJson !== "object") {
+ throw new OperationFailedError(
+ makeErrorDetails(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ "invalid JSON",
+ {
+ httpStatusCode: resp.status,
+ requestUrl: url,
+ },
+ ),
+ );
+ }
+ return responseJson;
+ };
+ const headers = new Headers();
+ for (const hn of Object.keys(resp.headers)) {
+ headers.set(hn, resp.headers[hn]);
+ }
+ return {
+ requestUrl: url,
+ headers,
+ status: resp.status,
+ text: async () => resp.data,
+ json: makeJson,
+ };
+ }
+
+ async get(url: string, opt?: HttpRequestOptions): Promise<HttpResponse> {
+ return this.req("get", url, undefined, opt);
+ }
+
+ async postJson(
+ url: string,
+ body: any,
+ opt?: HttpRequestOptions,
+ ): Promise<HttpResponse> {
+ return this.req("post", url, body, opt);
+ }
+}
diff --git a/packages/taler-wallet-core/src/headless/helpers.ts b/packages/taler-wallet-core/src/headless/helpers.ts
new file mode 100644
index 000000000..953493299
--- /dev/null
+++ b/packages/taler-wallet-core/src/headless/helpers.ts
@@ -0,0 +1,135 @@
+/*
+ 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/>
+ */
+
+/**
+ * Helpers to create headless wallets.
+ * @author Florian Dold <dold@taler.net>
+ */
+
+/**
+ * Imports.
+ */
+import { Wallet } from "../wallet";
+import { MemoryBackend, BridgeIDBFactory, shimIndexedDB } from "idb-bridge";
+import { openTalerDatabase } from "../db";
+import { HttpRequestLibrary } from "../util/http";
+import fs from "fs";
+import { NodeThreadCryptoWorkerFactory } from "../crypto/workers/nodeThreadWorker";
+import { WalletNotification } from "../types/notifications";
+import { Database } from "../util/query";
+import { NodeHttpLib } from "./NodeHttpLib";
+import { Logger } from "../util/logging";
+import { SynchronousCryptoWorkerFactory } from "../crypto/workers/synchronousWorker";
+import type { IDBFactory } from "idb-bridge/lib/idbtypes";
+
+const logger = new Logger("headless/helpers.ts");
+
+export interface DefaultNodeWalletArgs {
+ /**
+ * Location of the wallet database.
+ *
+ * If not specified, the wallet starts out with an empty database and
+ * the wallet database is stored only in memory.
+ */
+ persistentStoragePath?: string;
+
+ /**
+ * Handler for asynchronous notifications from the wallet.
+ */
+ notifyHandler?: (n: WalletNotification) => void;
+
+ /**
+ * If specified, use this as HTTP request library instead
+ * of the default one.
+ */
+ httpLib?: HttpRequestLibrary;
+}
+
+/**
+ * Get a wallet instance with default settings for node.
+ */
+export async function getDefaultNodeWallet(
+ args: DefaultNodeWalletArgs = {},
+): Promise<Wallet> {
+ BridgeIDBFactory.enableTracing = false;
+ const myBackend = new MemoryBackend();
+ myBackend.enableTracing = false;
+
+ const storagePath = args.persistentStoragePath;
+ if (storagePath) {
+ try {
+ const dbContentStr: string = fs.readFileSync(storagePath, {
+ encoding: "utf-8",
+ });
+ const dbContent = JSON.parse(dbContentStr);
+ myBackend.importDump(dbContent);
+ } catch (e) {
+ logger.warn("could not read wallet file");
+ }
+
+ myBackend.afterCommitCallback = async () => {
+ // Allow caller to stop persisting the wallet.
+ if (args.persistentStoragePath === undefined) {
+ return;
+ }
+ const dbContent = myBackend.exportDump();
+ fs.writeFileSync(storagePath, JSON.stringify(dbContent, undefined, 2), {
+ encoding: "utf-8",
+ });
+ };
+ }
+
+ BridgeIDBFactory.enableTracing = false;
+
+ const myBridgeIdbFactory = new BridgeIDBFactory(myBackend);
+ const myIdbFactory: IDBFactory = (myBridgeIdbFactory as any) as IDBFactory;
+
+ let myHttpLib;
+ if (args.httpLib) {
+ myHttpLib = args.httpLib;
+ } else {
+ myHttpLib = new NodeHttpLib();
+ }
+
+ const myVersionChange = (): Promise<void> => {
+ console.error("version change requested, should not happen");
+ throw Error();
+ };
+
+ shimIndexedDB(myBridgeIdbFactory);
+
+ const myDb = await openTalerDatabase(myIdbFactory, myVersionChange);
+
+ let workerFactory;
+ try {
+ // Try if we have worker threads available, fails in older node versions.
+ require("worker_threads");
+ workerFactory = new NodeThreadCryptoWorkerFactory();
+ } catch (e) {
+ console.log(
+ "worker threads not available, falling back to synchronous workers",
+ );
+ workerFactory = new SynchronousCryptoWorkerFactory();
+ }
+
+ const dbWrap = new Database(myDb);
+
+ const w = new Wallet(dbWrap, myHttpLib, workerFactory);
+ if (args.notifyHandler) {
+ w.addNotificationListener(args.notifyHandler);
+ }
+ return w;
+}
diff --git a/packages/taler-wallet-core/src/i18n/de.po b/packages/taler-wallet-core/src/i18n/de.po
new file mode 100644
index 000000000..bb355403d
--- /dev/null
+++ b/packages/taler-wallet-core/src/i18n/de.po
@@ -0,0 +1,363 @@
+# 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/>
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Wallet\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2016-11-23 00:00+0100\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: src/util/wire.ts:37
+#, c-format
+msgid "Invalid Wire"
+msgstr ""
+
+#: src/util/wire.ts:42 src/util/wire.ts:45
+#, c-format
+msgid "Invalid Test Wire Detail"
+msgstr ""
+
+#: src/util/wire.ts:47
+#, c-format
+msgid "Test Wire Acct #%1$s on %2$s"
+msgstr ""
+
+#: src/util/wire.ts:49
+#, c-format
+msgid "Unknown Wire Detail"
+msgstr ""
+
+#: src/webex/pages/benchmark.tsx:52
+#, c-format
+msgid "Operation"
+msgstr ""
+
+#: src/webex/pages/benchmark.tsx:53
+#, c-format
+msgid "time (ms/op)"
+msgstr ""
+
+#: src/webex/pages/pay.tsx:130
+#, fuzzy, c-format
+msgid "The merchant %1$s offers you to purchase:"
+msgstr "Der Händler %1$s möchte einen Vertrag über %2$s mit Ihnen abschließen."
+
+#: src/webex/pages/pay.tsx:136
+#, c-format
+msgid "The total price is %1$s (plus %2$s fees)."
+msgstr ""
+
+#: src/webex/pages/pay.tsx:141
+#, c-format
+msgid "The total price is %1$s."
+msgstr ""
+
+#: src/webex/pages/pay.tsx:163
+#, c-format
+msgid "Retry"
+msgstr ""
+
+#: src/webex/pages/pay.tsx:173
+#, fuzzy, c-format
+msgid "Confirm payment"
+msgstr "Bezahlung bestätigen"
+
+#: src/webex/pages/popup.tsx:153
+#, c-format
+msgid "Balance"
+msgstr "Saldo"
+
+#: src/webex/pages/popup.tsx:154
+#, c-format
+msgid "History"
+msgstr "Verlauf"
+
+#: src/webex/pages/popup.tsx:155
+#, c-format
+msgid "Debug"
+msgstr "Debug"
+
+#: src/webex/pages/popup.tsx:175
+#, fuzzy, c-format
+msgid "You have no balance to show. Need some %1$s getting started?"
+msgstr "Sie haben kein Digitalgeld. Wollen Sie %1$s? abheben?"
+
+#: src/webex/pages/popup.tsx:238
+#, c-format
+msgid "%1$s incoming"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:250
+#, c-format
+msgid "%1$s being spent"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:281
+#, c-format
+msgid "Error: could not retrieve balance information."
+msgstr ""
+
+#: src/webex/pages/popup.tsx:390
+#, c-format
+msgid "Invalid "
+msgstr ""
+
+#: src/webex/pages/popup.tsx:396
+#, c-format
+msgid "Fees "
+msgstr ""
+
+#: src/webex/pages/popup.tsx:434
+#, c-format
+msgid "Refresh sessions has completed"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:451
+#, c-format
+msgid "Order Refused"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:465
+#, c-format
+msgid "Order redirected"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:482
+#, c-format
+msgid "Payment aborted"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:512
+#, c-format
+msgid "Payment Sent"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:536
+#, c-format
+msgid "Order accepted"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:547
+#, c-format
+msgid "Reserve balance updated"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:559
+#, c-format
+msgid "Payment refund"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:584
+#, fuzzy, c-format
+msgid "Withdrawn"
+msgstr "Abheben bei %1$s"
+
+#: src/webex/pages/popup.tsx:596
+#, c-format
+msgid "Tip Accepted"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:606
+#, c-format
+msgid "Tip Declined"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:615
+#, c-format
+msgid "%1$s"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:707
+#, c-format
+msgid "Your wallet has no events recorded."
+msgstr "Ihre Geldbörse verzeichnet keine Vorkommnisse."
+
+#: src/webex/pages/return-coins.tsx:124
+#, c-format
+msgid "Wire to bank account"
+msgstr ""
+
+#: src/webex/pages/return-coins.tsx:206
+#, fuzzy, c-format
+msgid "Confirm"
+msgstr "Bezahlung bestätigen"
+
+#: src/webex/pages/return-coins.tsx:209
+#, fuzzy, c-format
+msgid "Cancel"
+msgstr "Saldo"
+
+#: src/webex/pages/withdraw.tsx:73
+#, c-format
+msgid "Could not get details for withdraw operation:"
+msgstr ""
+
+#: src/webex/pages/withdraw.tsx:89 src/webex/pages/withdraw.tsx:183
+#, c-format
+msgid "Chose different exchange provider"
+msgstr ""
+
+#: src/webex/pages/withdraw.tsx:109
+#, c-format
+msgid ""
+"Please select an exchange. You can review the details before after your "
+"selection."
+msgstr ""
+
+#: src/webex/pages/withdraw.tsx:121
+#, c-format
+msgid "Select %1$s"
+msgstr ""
+
+#: src/webex/pages/withdraw.tsx:143
+#, c-format
+msgid "Select custom exchange"
+msgstr ""
+
+#: src/webex/pages/withdraw.tsx:163
+#, c-format
+msgid "You are about to withdraw %1$s from your bank account into your wallet."
+msgstr ""
+
+#: src/webex/pages/withdraw.tsx:174
+#, c-format
+msgid "Accept fees and withdraw"
+msgstr ""
+
+#: src/webex/pages/withdraw.tsx:192
+#, c-format
+msgid "Cancel withdraw operation"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:249
+#, fuzzy, c-format
+msgid "Withdrawal fees:"
+msgstr "Abheben bei"
+
+#: src/webex/renderHtml.tsx:252
+#, c-format
+msgid "Rounding loss:"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:254
+#, c-format
+msgid "Earliest expiration (for deposit): %1$s"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:262
+#, c-format
+msgid "# Coins"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:263
+#, c-format
+msgid "Value"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:264
+#, fuzzy, c-format
+msgid "Withdraw Fee"
+msgstr "Abheben bei %1$s"
+
+#: src/webex/renderHtml.tsx:265
+#, c-format
+msgid "Refresh Fee"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:266
+#, c-format
+msgid "Deposit Fee"
+msgstr ""
+
+#, fuzzy, c-format
+#~ msgid "Bank requested reserve (%1$s) for %2$s."
+#~ msgstr "Bank bestätig anlegen der Reserve (%1$s) bei %2$s"
+
+#, fuzzy, c-format
+#~ msgid "Started to withdraw %1$s from %2$s (%3$s)."
+#~ msgstr "Reserve (%1$s) mit %2$s bei %3$s erzeugt"
+
+#, fuzzy, c-format
+#~ msgid "Merchant %1$s offered contract %2$s."
+#~ msgstr ""
+#~ "%1$s\n"
+#~ " möchte einen Vertrag über %2$s\n"
+#~ " mit Ihnen abschließen."
+
+#, fuzzy, c-format
+#~ msgid "Withdrew %1$s from %2$s ( %3$s)."
+#~ msgstr "Reserve (%1$s) mit %2$s bei %3$s erzeugt"
+
+#, fuzzy, c-format
+#~ msgid "Paid %1$s to merchant %2$s.%3$s( %4$s)"
+#~ msgstr "Reserve (%1$s) mit %2$s bei %3$s erzeugt"
+
+#, fuzzy, c-format
+#~ msgid "Merchant %1$s gave a refund over %2$s."
+#~ msgstr ""
+#~ "%1$s\n"
+#~ " möchte einen Vertrag über %2$s\n"
+#~ " mit Ihnen abschließen."
+
+#, fuzzy, c-format
+#~ msgid "Merchant %1$s gave a %2$s of %3$s."
+#~ msgstr ""
+#~ "%1$s\n"
+#~ " möchte einen Vertrag über %2$s\n"
+#~ " mit Ihnen abschließen."
+
+#, fuzzy, c-format
+#~ msgid "Submitting payment"
+#~ msgstr "Bezahlung bestätigen"
+
+#, fuzzy, c-format
+#~ msgid "Aborting payment ..."
+#~ msgstr "Bezahlung bestätigen"
+
+#, fuzzy, c-format
+#~ msgid "Retry Payment"
+#~ msgstr "Bezahlung bestätigen"
+
+#, fuzzy, c-format
+#~ msgid "Abort Payment"
+#~ msgstr "Bezahlung bestätigen"
+
+#, fuzzy
+#~ msgid "You are about to purchase:"
+#~ msgstr "Sie sind dabei, Folgendes zu kaufen:"
+
+#, fuzzy
+#~ msgid "Withdrawal fees: %1$s"
+#~ msgstr "Abheben bei %1$s"
+
+#~ msgid "Wallet depleted reserve (%1$s) at %2$s"
+#~ msgstr "Geldbörse hat die Reserve (%1$s) erschöpft"
+
+#~ msgid "Please enter a URL"
+#~ msgstr "Bitte eine URL eingeben"
+
+#~ msgid "The URL you've entered is not valid (must be absolute)"
+#~ msgstr "Die eingegebene URL ist nicht gültig (muss absolut sein)"
+
+#~ msgid "The bank wants to create a reserve over %1$s."
+#~ msgstr "Die Bank möchte eine Reserve über %1$s anlegen."
diff --git a/packages/taler-wallet-core/src/i18n/en-US.po b/packages/taler-wallet-core/src/i18n/en-US.po
new file mode 100644
index 000000000..4fe38d5e9
--- /dev/null
+++ b/packages/taler-wallet-core/src/i18n/en-US.po
@@ -0,0 +1,294 @@
+# 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/>
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Wallet\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2016-11-23 00:00+0100\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: src/util/wire.ts:37
+#, c-format
+msgid "Invalid Wire"
+msgstr ""
+
+#: src/util/wire.ts:42 src/util/wire.ts:45
+#, c-format
+msgid "Invalid Test Wire Detail"
+msgstr ""
+
+#: src/util/wire.ts:47
+#, c-format
+msgid "Test Wire Acct #%1$s on %2$s"
+msgstr ""
+
+#: src/util/wire.ts:49
+#, c-format
+msgid "Unknown Wire Detail"
+msgstr ""
+
+#: src/webex/pages/benchmark.tsx:52
+#, c-format
+msgid "Operation"
+msgstr ""
+
+#: src/webex/pages/benchmark.tsx:53
+#, c-format
+msgid "time (ms/op)"
+msgstr ""
+
+#: src/webex/pages/pay.tsx:130
+#, c-format
+msgid "The merchant %1$s offers you to purchase:"
+msgstr ""
+
+#: src/webex/pages/pay.tsx:136
+#, c-format
+msgid "The total price is %1$s (plus %2$s fees)."
+msgstr ""
+
+#: src/webex/pages/pay.tsx:141
+#, c-format
+msgid "The total price is %1$s."
+msgstr ""
+
+#: src/webex/pages/pay.tsx:163
+#, c-format
+msgid "Retry"
+msgstr ""
+
+#: src/webex/pages/pay.tsx:173
+#, c-format
+msgid "Confirm payment"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:153
+#, c-format
+msgid "Balance"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:154
+#, c-format
+msgid "History"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:155
+#, c-format
+msgid "Debug"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:175
+#, c-format
+msgid "You have no balance to show. Need some %1$s getting started?"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:238
+#, c-format
+msgid "%1$s incoming"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:250
+#, c-format
+msgid "%1$s being spent"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:281
+#, c-format
+msgid "Error: could not retrieve balance information."
+msgstr ""
+
+#: src/webex/pages/popup.tsx:390
+#, c-format
+msgid "Invalid "
+msgstr ""
+
+#: src/webex/pages/popup.tsx:396
+#, c-format
+msgid "Fees "
+msgstr ""
+
+#: src/webex/pages/popup.tsx:434
+#, c-format
+msgid "Refresh sessions has completed"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:451
+#, c-format
+msgid "Order Refused"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:465
+#, c-format
+msgid "Order redirected"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:482
+#, c-format
+msgid "Payment aborted"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:512
+#, c-format
+msgid "Payment Sent"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:536
+#, c-format
+msgid "Order accepted"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:547
+#, c-format
+msgid "Reserve balance updated"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:559
+#, c-format
+msgid "Payment refund"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:584
+#, c-format
+msgid "Withdrawn"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:596
+#, c-format
+msgid "Tip Accepted"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:606
+#, c-format
+msgid "Tip Declined"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:615
+#, c-format
+msgid "%1$s"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:707
+#, c-format
+msgid "Your wallet has no events recorded."
+msgstr ""
+
+#: src/webex/pages/return-coins.tsx:124
+#, c-format
+msgid "Wire to bank account"
+msgstr ""
+
+#: src/webex/pages/return-coins.tsx:206
+#, c-format
+msgid "Confirm"
+msgstr ""
+
+#: src/webex/pages/return-coins.tsx:209
+#, c-format
+msgid "Cancel"
+msgstr ""
+
+#: src/webex/pages/withdraw.tsx:73
+#, c-format
+msgid "Could not get details for withdraw operation:"
+msgstr ""
+
+#: src/webex/pages/withdraw.tsx:89 src/webex/pages/withdraw.tsx:183
+#, c-format
+msgid "Chose different exchange provider"
+msgstr ""
+
+#: src/webex/pages/withdraw.tsx:109
+#, c-format
+msgid ""
+"Please select an exchange. You can review the details before after your "
+"selection."
+msgstr ""
+
+#: src/webex/pages/withdraw.tsx:121
+#, c-format
+msgid "Select %1$s"
+msgstr ""
+
+#: src/webex/pages/withdraw.tsx:143
+#, c-format
+msgid "Select custom exchange"
+msgstr ""
+
+#: src/webex/pages/withdraw.tsx:163
+#, c-format
+msgid "You are about to withdraw %1$s from your bank account into your wallet."
+msgstr ""
+
+#: src/webex/pages/withdraw.tsx:174
+#, c-format
+msgid "Accept fees and withdraw"
+msgstr ""
+
+#: src/webex/pages/withdraw.tsx:192
+#, c-format
+msgid "Cancel withdraw operation"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:249
+#, c-format
+msgid "Withdrawal fees:"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:252
+#, c-format
+msgid "Rounding loss:"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:254
+#, c-format
+msgid "Earliest expiration (for deposit): %1$s"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:262
+#, c-format
+msgid "# Coins"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:263
+#, c-format
+msgid "Value"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:264
+#, c-format
+msgid "Withdraw Fee"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:265
+#, c-format
+msgid "Refresh Fee"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:266
+#, c-format
+msgid "Deposit Fee"
+msgstr ""
+
+#, fuzzy
+#~ msgid "DEBUG: Your balance on %1$s is %2$s KUDO. Get more at %3$s"
+#~ msgstr "DEBUG: Your balance is %2$s KUDO on %1$s. Get more at %3$s"
diff --git a/packages/taler-wallet-core/src/i18n/fr.po b/packages/taler-wallet-core/src/i18n/fr.po
new file mode 100644
index 000000000..67b09de1a
--- /dev/null
+++ b/packages/taler-wallet-core/src/i18n/fr.po
@@ -0,0 +1,290 @@
+# 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/>
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Wallet\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2016-11-23 00:00+0100\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: src/util/wire.ts:37
+#, c-format
+msgid "Invalid Wire"
+msgstr ""
+
+#: src/util/wire.ts:42 src/util/wire.ts:45
+#, c-format
+msgid "Invalid Test Wire Detail"
+msgstr ""
+
+#: src/util/wire.ts:47
+#, c-format
+msgid "Test Wire Acct #%1$s on %2$s"
+msgstr ""
+
+#: src/util/wire.ts:49
+#, c-format
+msgid "Unknown Wire Detail"
+msgstr ""
+
+#: src/webex/pages/benchmark.tsx:52
+#, c-format
+msgid "Operation"
+msgstr ""
+
+#: src/webex/pages/benchmark.tsx:53
+#, c-format
+msgid "time (ms/op)"
+msgstr ""
+
+#: src/webex/pages/pay.tsx:130
+#, c-format
+msgid "The merchant %1$s offers you to purchase:"
+msgstr ""
+
+#: src/webex/pages/pay.tsx:136
+#, c-format
+msgid "The total price is %1$s (plus %2$s fees)."
+msgstr ""
+
+#: src/webex/pages/pay.tsx:141
+#, c-format
+msgid "The total price is %1$s."
+msgstr ""
+
+#: src/webex/pages/pay.tsx:163
+#, c-format
+msgid "Retry"
+msgstr ""
+
+#: src/webex/pages/pay.tsx:173
+#, c-format
+msgid "Confirm payment"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:153
+#, c-format
+msgid "Balance"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:154
+#, c-format
+msgid "History"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:155
+#, c-format
+msgid "Debug"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:175
+#, c-format
+msgid "You have no balance to show. Need some %1$s getting started?"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:238
+#, c-format
+msgid "%1$s incoming"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:250
+#, c-format
+msgid "%1$s being spent"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:281
+#, c-format
+msgid "Error: could not retrieve balance information."
+msgstr ""
+
+#: src/webex/pages/popup.tsx:390
+#, c-format
+msgid "Invalid "
+msgstr ""
+
+#: src/webex/pages/popup.tsx:396
+#, c-format
+msgid "Fees "
+msgstr ""
+
+#: src/webex/pages/popup.tsx:434
+#, c-format
+msgid "Refresh sessions has completed"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:451
+#, c-format
+msgid "Order Refused"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:465
+#, c-format
+msgid "Order redirected"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:482
+#, c-format
+msgid "Payment aborted"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:512
+#, c-format
+msgid "Payment Sent"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:536
+#, c-format
+msgid "Order accepted"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:547
+#, c-format
+msgid "Reserve balance updated"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:559
+#, c-format
+msgid "Payment refund"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:584
+#, c-format
+msgid "Withdrawn"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:596
+#, c-format
+msgid "Tip Accepted"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:606
+#, c-format
+msgid "Tip Declined"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:615
+#, c-format
+msgid "%1$s"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:707
+#, c-format
+msgid "Your wallet has no events recorded."
+msgstr ""
+
+#: src/webex/pages/return-coins.tsx:124
+#, c-format
+msgid "Wire to bank account"
+msgstr ""
+
+#: src/webex/pages/return-coins.tsx:206
+#, c-format
+msgid "Confirm"
+msgstr ""
+
+#: src/webex/pages/return-coins.tsx:209
+#, c-format
+msgid "Cancel"
+msgstr ""
+
+#: src/webex/pages/withdraw.tsx:73
+#, c-format
+msgid "Could not get details for withdraw operation:"
+msgstr ""
+
+#: src/webex/pages/withdraw.tsx:89 src/webex/pages/withdraw.tsx:183
+#, c-format
+msgid "Chose different exchange provider"
+msgstr ""
+
+#: src/webex/pages/withdraw.tsx:109
+#, c-format
+msgid ""
+"Please select an exchange. You can review the details before after your "
+"selection."
+msgstr ""
+
+#: src/webex/pages/withdraw.tsx:121
+#, c-format
+msgid "Select %1$s"
+msgstr ""
+
+#: src/webex/pages/withdraw.tsx:143
+#, c-format
+msgid "Select custom exchange"
+msgstr ""
+
+#: src/webex/pages/withdraw.tsx:163
+#, c-format
+msgid "You are about to withdraw %1$s from your bank account into your wallet."
+msgstr ""
+
+#: src/webex/pages/withdraw.tsx:174
+#, c-format
+msgid "Accept fees and withdraw"
+msgstr ""
+
+#: src/webex/pages/withdraw.tsx:192
+#, c-format
+msgid "Cancel withdraw operation"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:249
+#, c-format
+msgid "Withdrawal fees:"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:252
+#, c-format
+msgid "Rounding loss:"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:254
+#, c-format
+msgid "Earliest expiration (for deposit): %1$s"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:262
+#, c-format
+msgid "# Coins"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:263
+#, c-format
+msgid "Value"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:264
+#, c-format
+msgid "Withdraw Fee"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:265
+#, c-format
+msgid "Refresh Fee"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:266
+#, c-format
+msgid "Deposit Fee"
+msgstr ""
diff --git a/packages/taler-wallet-core/src/i18n/index.ts b/packages/taler-wallet-core/src/i18n/index.ts
new file mode 100644
index 000000000..b248d2666
--- /dev/null
+++ b/packages/taler-wallet-core/src/i18n/index.ts
@@ -0,0 +1,78 @@
+/*
+ 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/>
+ */
+
+/**
+ * Translation helpers for React components and template literals.
+ */
+
+/**
+ * Imports.
+ */
+import { strings } from "./strings";
+export { strings } from "./strings";
+
+// @ts-ignore: no type decl for this library
+import * as jedLib from "jed";
+
+export let jed: any = undefined;
+
+/**
+ * Set up jed library for internationalization,
+ * based on browser language settings.
+ */
+export function setupI18n(lang: string): any {
+ lang = lang.replace("_", "-");
+
+ if (!strings[lang]) {
+ lang = "en-US";
+ console.log(`language ${lang} not found, defaulting to english`);
+ }
+ jed = new jedLib.Jed(strings[lang]);
+}
+
+/**
+ * Use different translations for testing. Should not be used outside
+ * of test cases.
+ */
+export function internalSetStrings(langStrings: any): void {
+ jed = new jedLib.Jed(langStrings);
+}
+
+/**
+ * Convert template strings to a msgid
+ */
+function toI18nString(stringSeq: ReadonlyArray<string>): string {
+ let s = "";
+ for (let i = 0; i < stringSeq.length; i++) {
+ s += stringSeq[i];
+ if (i < stringSeq.length - 1) {
+ s += `%${i + 1}$s`;
+ }
+ }
+ return s;
+}
+
+/**
+ * Internationalize a string template with arbitrary serialized values.
+ */
+export function str(stringSeq: TemplateStringsArray, ...values: any[]): string {
+ const s = toI18nString(stringSeq);
+ const tr = jed
+ .translate(s)
+ .ifPlural(1, s)
+ .fetch(...values);
+ return tr;
+}
diff --git a/packages/taler-wallet-core/src/i18n/it.po b/packages/taler-wallet-core/src/i18n/it.po
new file mode 100644
index 000000000..67b09de1a
--- /dev/null
+++ b/packages/taler-wallet-core/src/i18n/it.po
@@ -0,0 +1,290 @@
+# 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/>
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Wallet\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2016-11-23 00:00+0100\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: src/util/wire.ts:37
+#, c-format
+msgid "Invalid Wire"
+msgstr ""
+
+#: src/util/wire.ts:42 src/util/wire.ts:45
+#, c-format
+msgid "Invalid Test Wire Detail"
+msgstr ""
+
+#: src/util/wire.ts:47
+#, c-format
+msgid "Test Wire Acct #%1$s on %2$s"
+msgstr ""
+
+#: src/util/wire.ts:49
+#, c-format
+msgid "Unknown Wire Detail"
+msgstr ""
+
+#: src/webex/pages/benchmark.tsx:52
+#, c-format
+msgid "Operation"
+msgstr ""
+
+#: src/webex/pages/benchmark.tsx:53
+#, c-format
+msgid "time (ms/op)"
+msgstr ""
+
+#: src/webex/pages/pay.tsx:130
+#, c-format
+msgid "The merchant %1$s offers you to purchase:"
+msgstr ""
+
+#: src/webex/pages/pay.tsx:136
+#, c-format
+msgid "The total price is %1$s (plus %2$s fees)."
+msgstr ""
+
+#: src/webex/pages/pay.tsx:141
+#, c-format
+msgid "The total price is %1$s."
+msgstr ""
+
+#: src/webex/pages/pay.tsx:163
+#, c-format
+msgid "Retry"
+msgstr ""
+
+#: src/webex/pages/pay.tsx:173
+#, c-format
+msgid "Confirm payment"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:153
+#, c-format
+msgid "Balance"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:154
+#, c-format
+msgid "History"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:155
+#, c-format
+msgid "Debug"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:175
+#, c-format
+msgid "You have no balance to show. Need some %1$s getting started?"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:238
+#, c-format
+msgid "%1$s incoming"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:250
+#, c-format
+msgid "%1$s being spent"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:281
+#, c-format
+msgid "Error: could not retrieve balance information."
+msgstr ""
+
+#: src/webex/pages/popup.tsx:390
+#, c-format
+msgid "Invalid "
+msgstr ""
+
+#: src/webex/pages/popup.tsx:396
+#, c-format
+msgid "Fees "
+msgstr ""
+
+#: src/webex/pages/popup.tsx:434
+#, c-format
+msgid "Refresh sessions has completed"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:451
+#, c-format
+msgid "Order Refused"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:465
+#, c-format
+msgid "Order redirected"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:482
+#, c-format
+msgid "Payment aborted"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:512
+#, c-format
+msgid "Payment Sent"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:536
+#, c-format
+msgid "Order accepted"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:547
+#, c-format
+msgid "Reserve balance updated"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:559
+#, c-format
+msgid "Payment refund"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:584
+#, c-format
+msgid "Withdrawn"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:596
+#, c-format
+msgid "Tip Accepted"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:606
+#, c-format
+msgid "Tip Declined"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:615
+#, c-format
+msgid "%1$s"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:707
+#, c-format
+msgid "Your wallet has no events recorded."
+msgstr ""
+
+#: src/webex/pages/return-coins.tsx:124
+#, c-format
+msgid "Wire to bank account"
+msgstr ""
+
+#: src/webex/pages/return-coins.tsx:206
+#, c-format
+msgid "Confirm"
+msgstr ""
+
+#: src/webex/pages/return-coins.tsx:209
+#, c-format
+msgid "Cancel"
+msgstr ""
+
+#: src/webex/pages/withdraw.tsx:73
+#, c-format
+msgid "Could not get details for withdraw operation:"
+msgstr ""
+
+#: src/webex/pages/withdraw.tsx:89 src/webex/pages/withdraw.tsx:183
+#, c-format
+msgid "Chose different exchange provider"
+msgstr ""
+
+#: src/webex/pages/withdraw.tsx:109
+#, c-format
+msgid ""
+"Please select an exchange. You can review the details before after your "
+"selection."
+msgstr ""
+
+#: src/webex/pages/withdraw.tsx:121
+#, c-format
+msgid "Select %1$s"
+msgstr ""
+
+#: src/webex/pages/withdraw.tsx:143
+#, c-format
+msgid "Select custom exchange"
+msgstr ""
+
+#: src/webex/pages/withdraw.tsx:163
+#, c-format
+msgid "You are about to withdraw %1$s from your bank account into your wallet."
+msgstr ""
+
+#: src/webex/pages/withdraw.tsx:174
+#, c-format
+msgid "Accept fees and withdraw"
+msgstr ""
+
+#: src/webex/pages/withdraw.tsx:192
+#, c-format
+msgid "Cancel withdraw operation"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:249
+#, c-format
+msgid "Withdrawal fees:"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:252
+#, c-format
+msgid "Rounding loss:"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:254
+#, c-format
+msgid "Earliest expiration (for deposit): %1$s"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:262
+#, c-format
+msgid "# Coins"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:263
+#, c-format
+msgid "Value"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:264
+#, c-format
+msgid "Withdraw Fee"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:265
+#, c-format
+msgid "Refresh Fee"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:266
+#, c-format
+msgid "Deposit Fee"
+msgstr ""
diff --git a/packages/taler-wallet-core/src/i18n/poheader b/packages/taler-wallet-core/src/i18n/poheader
new file mode 100644
index 000000000..3ec704932
--- /dev/null
+++ b/packages/taler-wallet-core/src/i18n/poheader
@@ -0,0 +1,26 @@
+# 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/>
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Wallet\n"
+"Report-Msgid-Bugs-To: taler@gnu.org\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
diff --git a/packages/taler-wallet-core/src/i18n/strings-prelude b/packages/taler-wallet-core/src/i18n/strings-prelude
new file mode 100644
index 000000000..aa6602bd4
--- /dev/null
+++ b/packages/taler-wallet-core/src/i18n/strings-prelude
@@ -0,0 +1,17 @@
+/*
+ This file is part of TALER
+ (C) 2016 Inria
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+export const strings: {[s: string]: any} = {};
diff --git a/packages/taler-wallet-core/src/i18n/strings.ts b/packages/taler-wallet-core/src/i18n/strings.ts
new file mode 100644
index 000000000..748b9656a
--- /dev/null
+++ b/packages/taler-wallet-core/src/i18n/strings.ts
@@ -0,0 +1,373 @@
+/*
+ This file is part of TALER
+ (C) 2016 Inria
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+export const strings: { [s: string]: any } = {};
+strings["de"] = {
+ domain: "messages",
+ locale_data: {
+ messages: {
+ "": {
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=(n != 1);",
+ lang: "",
+ },
+ "Invalid Wire": [""],
+ "Invalid Test Wire Detail": [""],
+ "Test Wire Acct #%1$s on %2$s": [""],
+ "Unknown Wire Detail": [""],
+ Operation: [""],
+ "time (ms/op)": [""],
+ "The merchant %1$s offers you to purchase:": [
+ "Der Händler %1$s möchte einen Vertrag über %2$s mit Ihnen abschließen.",
+ ],
+ "The total price is %1$s (plus %2$s fees).": [""],
+ "The total price is %1$s.": [""],
+ Retry: [""],
+ "Confirm payment": ["Bezahlung bestätigen"],
+ Balance: ["Saldo"],
+ History: ["Verlauf"],
+ Debug: ["Debug"],
+ "You have no balance to show. Need some %1$s getting started?": [
+ "Sie haben kein Digitalgeld. Wollen Sie %1$s? abheben?",
+ ],
+ "%1$s incoming": [""],
+ "%1$s being spent": [""],
+ "Error: could not retrieve balance information.": [""],
+ "Invalid ": [""],
+ "Fees ": [""],
+ "Refresh sessions has completed": [""],
+ "Order Refused": [""],
+ "Order redirected": [""],
+ "Payment aborted": [""],
+ "Payment Sent": [""],
+ "Order accepted": [""],
+ "Reserve balance updated": [""],
+ "Payment refund": [""],
+ Withdrawn: ["Abheben bei %1$s"],
+ "Tip Accepted": [""],
+ "Tip Declined": [""],
+ "%1$s": [""],
+ "Your wallet has no events recorded.": [
+ "Ihre Geldbörse verzeichnet keine Vorkommnisse.",
+ ],
+ "Wire to bank account": [""],
+ Confirm: ["Bezahlung bestätigen"],
+ Cancel: ["Saldo"],
+ "Could not get details for withdraw operation:": [""],
+ "Chose different exchange provider": [""],
+ "Please select an exchange. You can review the details before after your selection.": [
+ "",
+ ],
+ "Select %1$s": [""],
+ "Select custom exchange": [""],
+ "You are about to withdraw %1$s from your bank account into your wallet.": [
+ "",
+ ],
+ "Accept fees and withdraw": [""],
+ "Cancel withdraw operation": [""],
+ "Withdrawal fees:": ["Abheben bei"],
+ "Rounding loss:": [""],
+ "Earliest expiration (for deposit): %1$s": [""],
+ "# Coins": [""],
+ Value: [""],
+ "Withdraw Fee": ["Abheben bei %1$s"],
+ "Refresh Fee": [""],
+ "Deposit Fee": [""],
+ },
+ },
+};
+
+strings["en-US"] = {
+ domain: "messages",
+ locale_data: {
+ messages: {
+ "": {
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=(n != 1);",
+ lang: "",
+ },
+ "Invalid Wire": [""],
+ "Invalid Test Wire Detail": [""],
+ "Test Wire Acct #%1$s on %2$s": [""],
+ "Unknown Wire Detail": [""],
+ Operation: [""],
+ "time (ms/op)": [""],
+ "The merchant %1$s offers you to purchase:": [""],
+ "The total price is %1$s (plus %2$s fees).": [""],
+ "The total price is %1$s.": [""],
+ Retry: [""],
+ "Confirm payment": [""],
+ Balance: [""],
+ History: [""],
+ Debug: [""],
+ "You have no balance to show. Need some %1$s getting started?": [""],
+ "%1$s incoming": [""],
+ "%1$s being spent": [""],
+ "Error: could not retrieve balance information.": [""],
+ "Invalid ": [""],
+ "Fees ": [""],
+ "Refresh sessions has completed": [""],
+ "Order Refused": [""],
+ "Order redirected": [""],
+ "Payment aborted": [""],
+ "Payment Sent": [""],
+ "Order accepted": [""],
+ "Reserve balance updated": [""],
+ "Payment refund": [""],
+ Withdrawn: [""],
+ "Tip Accepted": [""],
+ "Tip Declined": [""],
+ "%1$s": [""],
+ "Your wallet has no events recorded.": [""],
+ "Wire to bank account": [""],
+ Confirm: [""],
+ Cancel: [""],
+ "Could not get details for withdraw operation:": [""],
+ "Chose different exchange provider": [""],
+ "Please select an exchange. You can review the details before after your selection.": [
+ "",
+ ],
+ "Select %1$s": [""],
+ "Select custom exchange": [""],
+ "You are about to withdraw %1$s from your bank account into your wallet.": [
+ "",
+ ],
+ "Accept fees and withdraw": [""],
+ "Cancel withdraw operation": [""],
+ "Withdrawal fees:": [""],
+ "Rounding loss:": [""],
+ "Earliest expiration (for deposit): %1$s": [""],
+ "# Coins": [""],
+ Value: [""],
+ "Withdraw Fee": [""],
+ "Refresh Fee": [""],
+ "Deposit Fee": [""],
+ },
+ },
+};
+
+strings["fr"] = {
+ domain: "messages",
+ locale_data: {
+ messages: {
+ "": {
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=(n != 1);",
+ lang: "",
+ },
+ "Invalid Wire": [""],
+ "Invalid Test Wire Detail": [""],
+ "Test Wire Acct #%1$s on %2$s": [""],
+ "Unknown Wire Detail": [""],
+ Operation: [""],
+ "time (ms/op)": [""],
+ "The merchant %1$s offers you to purchase:": [""],
+ "The total price is %1$s (plus %2$s fees).": [""],
+ "The total price is %1$s.": [""],
+ Retry: [""],
+ "Confirm payment": [""],
+ Balance: [""],
+ History: [""],
+ Debug: [""],
+ "You have no balance to show. Need some %1$s getting started?": [""],
+ "%1$s incoming": [""],
+ "%1$s being spent": [""],
+ "Error: could not retrieve balance information.": [""],
+ "Invalid ": [""],
+ "Fees ": [""],
+ "Refresh sessions has completed": [""],
+ "Order Refused": [""],
+ "Order redirected": [""],
+ "Payment aborted": [""],
+ "Payment Sent": [""],
+ "Order accepted": [""],
+ "Reserve balance updated": [""],
+ "Payment refund": [""],
+ Withdrawn: [""],
+ "Tip Accepted": [""],
+ "Tip Declined": [""],
+ "%1$s": [""],
+ "Your wallet has no events recorded.": [""],
+ "Wire to bank account": [""],
+ Confirm: [""],
+ Cancel: [""],
+ "Could not get details for withdraw operation:": [""],
+ "Chose different exchange provider": [""],
+ "Please select an exchange. You can review the details before after your selection.": [
+ "",
+ ],
+ "Select %1$s": [""],
+ "Select custom exchange": [""],
+ "You are about to withdraw %1$s from your bank account into your wallet.": [
+ "",
+ ],
+ "Accept fees and withdraw": [""],
+ "Cancel withdraw operation": [""],
+ "Withdrawal fees:": [""],
+ "Rounding loss:": [""],
+ "Earliest expiration (for deposit): %1$s": [""],
+ "# Coins": [""],
+ Value: [""],
+ "Withdraw Fee": [""],
+ "Refresh Fee": [""],
+ "Deposit Fee": [""],
+ },
+ },
+};
+
+strings["it"] = {
+ domain: "messages",
+ locale_data: {
+ messages: {
+ "": {
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=(n != 1);",
+ lang: "",
+ },
+ "Invalid Wire": [""],
+ "Invalid Test Wire Detail": [""],
+ "Test Wire Acct #%1$s on %2$s": [""],
+ "Unknown Wire Detail": [""],
+ Operation: [""],
+ "time (ms/op)": [""],
+ "The merchant %1$s offers you to purchase:": [""],
+ "The total price is %1$s (plus %2$s fees).": [""],
+ "The total price is %1$s.": [""],
+ Retry: [""],
+ "Confirm payment": [""],
+ Balance: [""],
+ History: [""],
+ Debug: [""],
+ "You have no balance to show. Need some %1$s getting started?": [""],
+ "%1$s incoming": [""],
+ "%1$s being spent": [""],
+ "Error: could not retrieve balance information.": [""],
+ "Invalid ": [""],
+ "Fees ": [""],
+ "Refresh sessions has completed": [""],
+ "Order Refused": [""],
+ "Order redirected": [""],
+ "Payment aborted": [""],
+ "Payment Sent": [""],
+ "Order accepted": [""],
+ "Reserve balance updated": [""],
+ "Payment refund": [""],
+ Withdrawn: [""],
+ "Tip Accepted": [""],
+ "Tip Declined": [""],
+ "%1$s": [""],
+ "Your wallet has no events recorded.": [""],
+ "Wire to bank account": [""],
+ Confirm: [""],
+ Cancel: [""],
+ "Could not get details for withdraw operation:": [""],
+ "Chose different exchange provider": [""],
+ "Please select an exchange. You can review the details before after your selection.": [
+ "",
+ ],
+ "Select %1$s": [""],
+ "Select custom exchange": [""],
+ "You are about to withdraw %1$s from your bank account into your wallet.": [
+ "",
+ ],
+ "Accept fees and withdraw": [""],
+ "Cancel withdraw operation": [""],
+ "Withdrawal fees:": [""],
+ "Rounding loss:": [""],
+ "Earliest expiration (for deposit): %1$s": [""],
+ "# Coins": [""],
+ Value: [""],
+ "Withdraw Fee": [""],
+ "Refresh Fee": [""],
+ "Deposit Fee": [""],
+ },
+ },
+};
+
+strings["sv"] = {
+ domain: "messages",
+ locale_data: {
+ messages: {
+ "": {
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=(n != 1);",
+ lang: "",
+ },
+ "Invalid Wire": [""],
+ "Invalid Test Wire Detail": [""],
+ "Test Wire Acct #%1$s on %2$s": [""],
+ "Unknown Wire Detail": ["visa mer"],
+ Operation: [""],
+ "time (ms/op)": [""],
+ "The merchant %1$s offers you to purchase:": [
+ "Säljaren %1$s erbjuder följande:",
+ ],
+ "The total price is %1$s (plus %2$s fees).": [
+ "Det totala priset är %1$s (plus %2$s avgifter).\n",
+ ],
+ "The total price is %1$s.": ["Det totala priset är %1$s."],
+ Retry: [""],
+ "Confirm payment": ["Godkän betalning"],
+ Balance: ["Balans"],
+ History: ["Historia"],
+ Debug: [""],
+ "You have no balance to show. Need some %1$s getting started?": [
+ "Du har ingen balans att visa. Behöver du\n %1$s att börja?\n",
+ ],
+ "%1$s incoming": ["%1$s inkommande"],
+ "%1$s being spent": [""],
+ "Error: could not retrieve balance information.": [""],
+ "Invalid ": [""],
+ "Fees ": [""],
+ "Refresh sessions has completed": [""],
+ "Order Refused": [""],
+ "Order redirected": [""],
+ "Payment aborted": [""],
+ "Payment Sent": [""],
+ "Order accepted": [""],
+ "Reserve balance updated": [""],
+ "Payment refund": [""],
+ Withdrawn: ["Utbetalnings avgift"],
+ "Tip Accepted": [""],
+ "Tip Declined": [""],
+ "%1$s": [""],
+ "Your wallet has no events recorded.": ["plånboken"],
+ "Wire to bank account": ["Övervisa till bank konto"],
+ Confirm: ["Bekräfta"],
+ Cancel: ["Avbryt"],
+ "Could not get details for withdraw operation:": [""],
+ "Chose different exchange provider": ["Ändra tjänsteleverantörer"],
+ "Please select an exchange. You can review the details before after your selection.": [
+ "",
+ ],
+ "Select %1$s": ["Välj %1$s"],
+ "Select custom exchange": [""],
+ "You are about to withdraw %1$s from your bank account into your wallet.": [
+ "Du är på väg att ta ut\n %1$s från ditt bankkonto till din plånbok.\n",
+ ],
+ "Accept fees and withdraw": ["Acceptera avgifter och utbetala"],
+ "Cancel withdraw operation": [""],
+ "Withdrawal fees:": ["Utbetalnings avgifter:"],
+ "Rounding loss:": [""],
+ "Earliest expiration (for deposit): %1$s": [""],
+ "# Coins": ["# Mynt"],
+ Value: ["Värde"],
+ "Withdraw Fee": ["Utbetalnings avgift"],
+ "Refresh Fee": ["Återhämtnings avgift"],
+ "Deposit Fee": ["Depostitions avgift"],
+ },
+ },
+};
diff --git a/packages/taler-wallet-core/src/i18n/sv.po b/packages/taler-wallet-core/src/i18n/sv.po
new file mode 100644
index 000000000..c6a739789
--- /dev/null
+++ b/packages/taler-wallet-core/src/i18n/sv.po
@@ -0,0 +1,388 @@
+# 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/>
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Wallet\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2016-11-23 00:00+0100\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: Flo Reitz <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: src/util/wire.ts:37
+#, c-format
+msgid "Invalid Wire"
+msgstr ""
+
+#: src/util/wire.ts:42 src/util/wire.ts:45
+#, c-format
+msgid "Invalid Test Wire Detail"
+msgstr ""
+
+#: src/util/wire.ts:47
+#, c-format
+msgid "Test Wire Acct #%1$s on %2$s"
+msgstr ""
+
+#: src/util/wire.ts:49
+#, fuzzy, c-format
+msgid "Unknown Wire Detail"
+msgstr "visa mer"
+
+#: src/webex/pages/benchmark.tsx:52
+#, c-format
+msgid "Operation"
+msgstr ""
+
+#: src/webex/pages/benchmark.tsx:53
+#, c-format
+msgid "time (ms/op)"
+msgstr ""
+
+#: src/webex/pages/pay.tsx:130
+#, fuzzy, c-format
+msgid "The merchant %1$s offers you to purchase:"
+msgstr "Säljaren %1$s erbjuder följande:"
+
+#: src/webex/pages/pay.tsx:136
+#, fuzzy, c-format
+msgid "The total price is %1$s (plus %2$s fees)."
+msgstr "Det totala priset är %1$s (plus %2$s avgifter).\n"
+
+#: src/webex/pages/pay.tsx:141
+#, fuzzy, c-format
+msgid "The total price is %1$s."
+msgstr "Det totala priset är %1$s."
+
+#: src/webex/pages/pay.tsx:163
+#, c-format
+msgid "Retry"
+msgstr ""
+
+#: src/webex/pages/pay.tsx:173
+#, c-format
+msgid "Confirm payment"
+msgstr "Godkän betalning"
+
+#: src/webex/pages/popup.tsx:153
+#, c-format
+msgid "Balance"
+msgstr "Balans"
+
+#: src/webex/pages/popup.tsx:154
+#, c-format
+msgid "History"
+msgstr "Historia"
+
+#: src/webex/pages/popup.tsx:155
+#, c-format
+msgid "Debug"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:175
+#, fuzzy, c-format
+msgid "You have no balance to show. Need some %1$s getting started?"
+msgstr ""
+"Du har ingen balans att visa. Behöver du\n"
+" %1$s att börja?\n"
+
+#: src/webex/pages/popup.tsx:238
+#, fuzzy, c-format
+msgid "%1$s incoming"
+msgstr "%1$s inkommande"
+
+#: src/webex/pages/popup.tsx:250
+#, c-format
+msgid "%1$s being spent"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:281
+#, c-format
+msgid "Error: could not retrieve balance information."
+msgstr ""
+
+#: src/webex/pages/popup.tsx:390
+#, c-format
+msgid "Invalid "
+msgstr ""
+
+#: src/webex/pages/popup.tsx:396
+#, c-format
+msgid "Fees "
+msgstr ""
+
+#: src/webex/pages/popup.tsx:434
+#, c-format
+msgid "Refresh sessions has completed"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:451
+#, c-format
+msgid "Order Refused"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:465
+#, c-format
+msgid "Order redirected"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:482
+#, c-format
+msgid "Payment aborted"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:512
+#, c-format
+msgid "Payment Sent"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:536
+#, c-format
+msgid "Order accepted"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:547
+#, c-format
+msgid "Reserve balance updated"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:559
+#, c-format
+msgid "Payment refund"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:584
+#, fuzzy, c-format
+msgid "Withdrawn"
+msgstr "Utbetalnings avgift"
+
+#: src/webex/pages/popup.tsx:596
+#, c-format
+msgid "Tip Accepted"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:606
+#, c-format
+msgid "Tip Declined"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:615
+#, c-format
+msgid "%1$s"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:707
+#, c-format
+msgid "Your wallet has no events recorded."
+msgstr "plånboken"
+
+#: src/webex/pages/return-coins.tsx:124
+#, c-format
+msgid "Wire to bank account"
+msgstr "Övervisa till bank konto"
+
+#: src/webex/pages/return-coins.tsx:206
+#, c-format
+msgid "Confirm"
+msgstr "Bekräfta"
+
+#: src/webex/pages/return-coins.tsx:209
+#, c-format
+msgid "Cancel"
+msgstr "Avbryt"
+
+#: src/webex/pages/withdraw.tsx:73
+#, c-format
+msgid "Could not get details for withdraw operation:"
+msgstr ""
+
+#: src/webex/pages/withdraw.tsx:89 src/webex/pages/withdraw.tsx:183
+#, fuzzy, c-format
+msgid "Chose different exchange provider"
+msgstr "Ändra tjänsteleverantörer"
+
+#: src/webex/pages/withdraw.tsx:109
+#, c-format
+msgid ""
+"Please select an exchange. You can review the details before after your "
+"selection."
+msgstr ""
+
+#: src/webex/pages/withdraw.tsx:121
+#, fuzzy, c-format
+msgid "Select %1$s"
+msgstr "Välj %1$s"
+
+#: src/webex/pages/withdraw.tsx:143
+#, c-format
+msgid "Select custom exchange"
+msgstr ""
+
+#: src/webex/pages/withdraw.tsx:163
+#, fuzzy, c-format
+msgid "You are about to withdraw %1$s from your bank account into your wallet."
+msgstr ""
+"Du är på väg att ta ut\n"
+" %1$s från ditt bankkonto till din plånbok.\n"
+
+#: src/webex/pages/withdraw.tsx:174
+#, c-format
+msgid "Accept fees and withdraw"
+msgstr "Acceptera avgifter och utbetala"
+
+#: src/webex/pages/withdraw.tsx:192
+#, c-format
+msgid "Cancel withdraw operation"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:249
+#, c-format
+msgid "Withdrawal fees:"
+msgstr "Utbetalnings avgifter:"
+
+#: src/webex/renderHtml.tsx:252
+#, c-format
+msgid "Rounding loss:"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:254
+#, c-format
+msgid "Earliest expiration (for deposit): %1$s"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:262
+#, c-format
+msgid "# Coins"
+msgstr "# Mynt"
+
+#: src/webex/renderHtml.tsx:263
+#, c-format
+msgid "Value"
+msgstr "Värde"
+
+#: src/webex/renderHtml.tsx:264
+#, c-format
+msgid "Withdraw Fee"
+msgstr "Utbetalnings avgift"
+
+#: src/webex/renderHtml.tsx:265
+#, c-format
+msgid "Refresh Fee"
+msgstr "Återhämtnings avgift"
+
+#: src/webex/renderHtml.tsx:266
+#, c-format
+msgid "Deposit Fee"
+msgstr "Depostitions avgift"
+
+#, fuzzy, c-format
+#~ msgid "Merchant %1$s offered contract %2$s."
+#~ msgstr "Säljaren %1$s erbjöd kontrakt %2$s.\n"
+
+#, fuzzy, c-format
+#~ msgid "Merchant %1$s gave a refund over %2$s."
+#~ msgstr "Säljaren %1$sgav en återbetalning på %2$s.\n"
+
+#, fuzzy, c-format
+#~ msgid "Merchant %1$s gave a %2$s of %3$s."
+#~ msgstr "Säljaren %1$sgav en återbetalning på %2$s.\n"
+
+#, c-format
+#~ msgid "help"
+#~ msgstr "hjälp"
+
+#, c-format
+#~ msgid "Payback"
+#~ msgstr "Återbetalning"
+
+#, c-format
+#~ msgid "Return Electronic Cash to Bank Account"
+#~ msgstr "Återlämna elektroniska pengar till bank konto"
+
+#, fuzzy, c-format
+#~ msgid "show more details"
+#~ msgstr "visa mer"
+
+#, c-format
+#~ msgid "Accepted exchanges:"
+#~ msgstr "Accepterade tjänsteleverantörer:"
+
+#, c-format
+#~ msgid "Exchanges in the wallet:"
+#~ msgstr "Tjänsteleverantörer i plånboken:"
+
+#, c-format
+#~ msgid ""
+#~ "You have insufficient funds of the requested currency in your wallet."
+#~ msgstr "plånboken"
+
+#, c-format
+#~ msgid ""
+#~ "You do not have any funds from an exchange that is accepted by this "
+#~ "merchant. None of the exchanges accepted by the merchant is known to your "
+#~ "wallet."
+#~ msgstr "plånboken"
+
+#, c-format
+#~ msgid "Submitting payment"
+#~ msgstr "Bekräftar betalning"
+
+#, c-format
+#~ msgid ""
+#~ "You already paid for this, clicking \"Confirm payment\" will not cost "
+#~ "money again."
+#~ msgstr ""
+#~ "Du har redan betalat för det här, om du trycker \"Godkän betalning\" "
+#~ "debiteras du inte igen"
+
+#, fuzzy, c-format
+#~ msgid "Aborting payment ..."
+#~ msgstr "Bekräftar betalning"
+
+#, fuzzy, c-format
+#~ msgid "Abort Payment"
+#~ msgstr "Godkän betalning"
+
+#, c-format
+#~ msgid "Select"
+#~ msgstr "Välj"
+
+#, fuzzy, c-format
+#~ msgid "The exchange is trusted by the wallet."
+#~ msgstr "Tjänsteleverantörer i plånboken:"
+
+#, fuzzy, c-format
+#~ msgid ""
+#~ "Your wallet (protocol version %1$s) might be outdated.%2$s The exchange "
+#~ "has a higher, incompatible protocol version (%3$s)."
+#~ msgstr "tjänsteleverantörer plånboken"
+
+#, fuzzy, c-format
+#~ msgid ""
+#~ "The chosen exchange (protocol version %1$s might be outdated.%2$s The "
+#~ "exchange has a lower, incompatible protocol version than your wallet "
+#~ "(protocol version %3$s)."
+#~ msgstr "tjänsteleverantörer plånboken"
+
+#, fuzzy, c-format
+#~ msgid ""
+#~ "Oops, something went wrong. The wallet responded with error status (%1$s)."
+#~ msgstr "plånboken"
diff --git a/packages/taler-wallet-core/src/i18n/taler-wallet-webex.pot b/packages/taler-wallet-core/src/i18n/taler-wallet-webex.pot
new file mode 100644
index 000000000..67b09de1a
--- /dev/null
+++ b/packages/taler-wallet-core/src/i18n/taler-wallet-webex.pot
@@ -0,0 +1,290 @@
+# 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/>
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: Taler Wallet\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2016-11-23 00:00+0100\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: src/util/wire.ts:37
+#, c-format
+msgid "Invalid Wire"
+msgstr ""
+
+#: src/util/wire.ts:42 src/util/wire.ts:45
+#, c-format
+msgid "Invalid Test Wire Detail"
+msgstr ""
+
+#: src/util/wire.ts:47
+#, c-format
+msgid "Test Wire Acct #%1$s on %2$s"
+msgstr ""
+
+#: src/util/wire.ts:49
+#, c-format
+msgid "Unknown Wire Detail"
+msgstr ""
+
+#: src/webex/pages/benchmark.tsx:52
+#, c-format
+msgid "Operation"
+msgstr ""
+
+#: src/webex/pages/benchmark.tsx:53
+#, c-format
+msgid "time (ms/op)"
+msgstr ""
+
+#: src/webex/pages/pay.tsx:130
+#, c-format
+msgid "The merchant %1$s offers you to purchase:"
+msgstr ""
+
+#: src/webex/pages/pay.tsx:136
+#, c-format
+msgid "The total price is %1$s (plus %2$s fees)."
+msgstr ""
+
+#: src/webex/pages/pay.tsx:141
+#, c-format
+msgid "The total price is %1$s."
+msgstr ""
+
+#: src/webex/pages/pay.tsx:163
+#, c-format
+msgid "Retry"
+msgstr ""
+
+#: src/webex/pages/pay.tsx:173
+#, c-format
+msgid "Confirm payment"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:153
+#, c-format
+msgid "Balance"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:154
+#, c-format
+msgid "History"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:155
+#, c-format
+msgid "Debug"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:175
+#, c-format
+msgid "You have no balance to show. Need some %1$s getting started?"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:238
+#, c-format
+msgid "%1$s incoming"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:250
+#, c-format
+msgid "%1$s being spent"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:281
+#, c-format
+msgid "Error: could not retrieve balance information."
+msgstr ""
+
+#: src/webex/pages/popup.tsx:390
+#, c-format
+msgid "Invalid "
+msgstr ""
+
+#: src/webex/pages/popup.tsx:396
+#, c-format
+msgid "Fees "
+msgstr ""
+
+#: src/webex/pages/popup.tsx:434
+#, c-format
+msgid "Refresh sessions has completed"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:451
+#, c-format
+msgid "Order Refused"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:465
+#, c-format
+msgid "Order redirected"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:482
+#, c-format
+msgid "Payment aborted"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:512
+#, c-format
+msgid "Payment Sent"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:536
+#, c-format
+msgid "Order accepted"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:547
+#, c-format
+msgid "Reserve balance updated"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:559
+#, c-format
+msgid "Payment refund"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:584
+#, c-format
+msgid "Withdrawn"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:596
+#, c-format
+msgid "Tip Accepted"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:606
+#, c-format
+msgid "Tip Declined"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:615
+#, c-format
+msgid "%1$s"
+msgstr ""
+
+#: src/webex/pages/popup.tsx:707
+#, c-format
+msgid "Your wallet has no events recorded."
+msgstr ""
+
+#: src/webex/pages/return-coins.tsx:124
+#, c-format
+msgid "Wire to bank account"
+msgstr ""
+
+#: src/webex/pages/return-coins.tsx:206
+#, c-format
+msgid "Confirm"
+msgstr ""
+
+#: src/webex/pages/return-coins.tsx:209
+#, c-format
+msgid "Cancel"
+msgstr ""
+
+#: src/webex/pages/withdraw.tsx:73
+#, c-format
+msgid "Could not get details for withdraw operation:"
+msgstr ""
+
+#: src/webex/pages/withdraw.tsx:89 src/webex/pages/withdraw.tsx:183
+#, c-format
+msgid "Chose different exchange provider"
+msgstr ""
+
+#: src/webex/pages/withdraw.tsx:109
+#, c-format
+msgid ""
+"Please select an exchange. You can review the details before after your "
+"selection."
+msgstr ""
+
+#: src/webex/pages/withdraw.tsx:121
+#, c-format
+msgid "Select %1$s"
+msgstr ""
+
+#: src/webex/pages/withdraw.tsx:143
+#, c-format
+msgid "Select custom exchange"
+msgstr ""
+
+#: src/webex/pages/withdraw.tsx:163
+#, c-format
+msgid "You are about to withdraw %1$s from your bank account into your wallet."
+msgstr ""
+
+#: src/webex/pages/withdraw.tsx:174
+#, c-format
+msgid "Accept fees and withdraw"
+msgstr ""
+
+#: src/webex/pages/withdraw.tsx:192
+#, c-format
+msgid "Cancel withdraw operation"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:249
+#, c-format
+msgid "Withdrawal fees:"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:252
+#, c-format
+msgid "Rounding loss:"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:254
+#, c-format
+msgid "Earliest expiration (for deposit): %1$s"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:262
+#, c-format
+msgid "# Coins"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:263
+#, c-format
+msgid "Value"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:264
+#, c-format
+msgid "Withdraw Fee"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:265
+#, c-format
+msgid "Refresh Fee"
+msgstr ""
+
+#: src/webex/renderHtml.tsx:266
+#, c-format
+msgid "Deposit Fee"
+msgstr ""
diff --git a/packages/taler-wallet-core/src/index.ts b/packages/taler-wallet-core/src/index.ts
new file mode 100644
index 000000000..e70fc44f6
--- /dev/null
+++ b/packages/taler-wallet-core/src/index.ts
@@ -0,0 +1,75 @@
+/*
+ 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/>
+ */
+
+/**
+ * Module entry point for the wallet when used as a node module.
+ */
+
+export { Wallet } from "./wallet";
+export {
+ getDefaultNodeWallet,
+ DefaultNodeWalletArgs,
+} from "./headless/helpers";
+export { Amounts, AmountJson } from "./util/amounts";
+export { Logger } from "./util/logging";
+
+export * as talerCrypto from "./crypto/talerCrypto";
+export {
+ OperationFailedAndReportedError,
+ OperationFailedError,
+ makeErrorDetails,
+} from "./operations/errors";
+
+export * as walletTypes from "./types/walletTypes";
+
+export * as talerTypes from "./types/talerTypes";
+
+export * as walletCoreApi from "./walletCoreApiHandler";
+
+export * as taleruri from "./util/taleruri";
+
+export * as time from "./util/time";
+
+export * as codec from "./util/codec";
+
+export { NodeHttpLib } from "./headless/NodeHttpLib";
+
+export * as payto from "./util/payto";
+
+export * as testvectors from "./util/testvectors";
+
+export * as versions from "./operations/versions";
+
+export type { CryptoWorker } from "./crypto/workers/cryptoWorker";
+export type { CryptoWorkerFactory } from "./crypto/workers/cryptoApi";
+
+export * as httpLib from "./util/http";
+
+export { TalerErrorCode } from "./TalerErrorCode";
+
+export * as queryLib from "./util/query";
+
+export { CryptoImplementation } from "./crypto/workers/cryptoImplementation";
+
+export * as db from "./db";
+
+export * as promiseUtil from "./util/promiseUtils";
+
+export * as i18n from "./i18n";
+
+export * as nodeThreadWorker from "./crypto/workers/nodeThreadWorker";
+
+export * as walletNotifications from "./types/notifications";
diff --git a/packages/taler-wallet-core/src/operations/balance.d.ts.map b/packages/taler-wallet-core/src/operations/balance.d.ts.map
new file mode 100644
index 000000000..264d3139b
--- /dev/null
+++ b/packages/taler-wallet-core/src/operations/balance.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"balance.d.ts","sourceRoot":"","sources":["balance.ts"],"names":[],"mappings":"AAgBA;;GAEG;AACH,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAc9C;;GAEG;AACH,wBAAsB,4BAA4B,CAChD,EAAE,EAAE,mBAAmB,EACvB,EAAE,EAAE,iBAAiB,GACpB,OAAO,CAAC,gBAAgB,CAAC,CAqF3B;AAED;;GAEG;AACH,wBAAsB,WAAW,CAC/B,EAAE,EAAE,mBAAmB,GACtB,OAAO,CAAC,gBAAgB,CAAC,CAmB3B"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/operations/balance.ts b/packages/taler-wallet-core/src/operations/balance.ts
new file mode 100644
index 000000000..26f0aaeee
--- /dev/null
+++ b/packages/taler-wallet-core/src/operations/balance.ts
@@ -0,0 +1,153 @@
+/*
+ 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/>
+ */
+
+/**
+ * Imports.
+ */
+import { BalancesResponse } from "../types/walletTypes";
+import { TransactionHandle } from "../util/query";
+import { InternalWalletState } from "./state";
+import { Stores, CoinStatus } from "../types/dbTypes";
+import * as Amounts from "../util/amounts";
+import { AmountJson } from "../util/amounts";
+import { Logger } from "../util/logging";
+
+const logger = new Logger("withdraw.ts");
+
+interface WalletBalance {
+ available: AmountJson;
+ pendingIncoming: AmountJson;
+ pendingOutgoing: AmountJson;
+}
+
+/**
+ * Get balance information.
+ */
+export async function getBalancesInsideTransaction(
+ ws: InternalWalletState,
+ tx: TransactionHandle,
+): Promise<BalancesResponse> {
+ const balanceStore: Record<string, WalletBalance> = {};
+
+ /**
+ * Add amount to a balance field, both for
+ * the slicing by exchange and currency.
+ */
+ const initBalance = (currency: string): WalletBalance => {
+ const b = balanceStore[currency];
+ if (!b) {
+ balanceStore[currency] = {
+ available: Amounts.getZero(currency),
+ pendingIncoming: Amounts.getZero(currency),
+ pendingOutgoing: Amounts.getZero(currency),
+ };
+ }
+ return balanceStore[currency];
+ };
+
+ // Initialize balance to zero, even if we didn't start withdrawing yet.
+ await tx.iter(Stores.reserves).forEach((r) => {
+ const b = initBalance(r.currency);
+ if (!r.initialWithdrawalStarted) {
+ b.pendingIncoming = Amounts.add(
+ b.pendingIncoming,
+ r.initialDenomSel.totalCoinValue,
+ ).amount;
+ }
+ });
+
+ await tx.iter(Stores.coins).forEach((c) => {
+ // Only count fresh coins, as dormant coins will
+ // already be in a refresh session.
+ if (c.status === CoinStatus.Fresh) {
+ const b = initBalance(c.currentAmount.currency);
+ b.available = Amounts.add(b.available, c.currentAmount).amount;
+ }
+ });
+
+ await tx.iter(Stores.refreshGroups).forEach((r) => {
+ // Don't count finished refreshes, since the refresh already resulted
+ // in coins being added to the wallet.
+ if (r.timestampFinished) {
+ return;
+ }
+ for (let i = 0; i < r.oldCoinPubs.length; i++) {
+ const session = r.refreshSessionPerCoin[i];
+ if (session) {
+ const b = initBalance(session.amountRefreshOutput.currency);
+ // We are always assuming the refresh will succeed, thus we
+ // report the output as available balance.
+ b.available = Amounts.add(session.amountRefreshOutput).amount;
+ }
+ }
+ });
+
+ await tx.iter(Stores.withdrawalGroups).forEach((wds) => {
+ if (wds.timestampFinish) {
+ return;
+ }
+ const b = initBalance(wds.denomsSel.totalWithdrawCost.currency);
+ b.pendingIncoming = Amounts.add(
+ b.pendingIncoming,
+ wds.denomsSel.totalCoinValue,
+ ).amount;
+ });
+
+ const balancesResponse: BalancesResponse = {
+ balances: [],
+ };
+
+ Object.keys(balanceStore)
+ .sort()
+ .forEach((c) => {
+ const v = balanceStore[c];
+ balancesResponse.balances.push({
+ available: Amounts.stringify(v.available),
+ pendingIncoming: Amounts.stringify(v.pendingIncoming),
+ pendingOutgoing: Amounts.stringify(v.pendingOutgoing),
+ hasPendingTransactions: false,
+ requiresUserInput: false,
+ });
+ });
+
+ return balancesResponse;
+}
+
+/**
+ * Get detailed balance information, sliced by exchange and by currency.
+ */
+export async function getBalances(
+ ws: InternalWalletState,
+): Promise<BalancesResponse> {
+ logger.trace("starting to compute balance");
+
+ const wbal = await ws.db.runWithReadTransaction(
+ [
+ Stores.coins,
+ Stores.refreshGroups,
+ Stores.reserves,
+ Stores.purchases,
+ Stores.withdrawalGroups,
+ ],
+ async (tx) => {
+ return getBalancesInsideTransaction(ws, tx);
+ },
+ );
+
+ logger.trace("finished computing wallet balance");
+
+ return wbal;
+}
diff --git a/packages/taler-wallet-core/src/operations/errors.d.ts.map b/packages/taler-wallet-core/src/operations/errors.d.ts.map
new file mode 100644
index 000000000..e5763f31a
--- /dev/null
+++ b/packages/taler-wallet-core/src/operations/errors.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["errors.ts"],"names":[],"mappings":"AAgBA;;;;GAIG;AAEH;;GAEG;AACH,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAEnD;;;GAGG;AACH,qBAAa,+BAAgC,SAAQ,KAAK;IACrC,cAAc,EAAE,qBAAqB;gBAArC,cAAc,EAAE,qBAAqB;CAMzD;AAED;;;GAGG;AACH,qBAAa,oBAAqB,SAAQ,KAAK;IAS1B,cAAc,EAAE,qBAAqB;IARxD,MAAM,CAAC,QAAQ,CACb,EAAE,EAAE,cAAc,EAClB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC/B,oBAAoB;gBAIJ,cAAc,EAAE,qBAAqB;CAMzD;AAED,wBAAgB,gBAAgB,CAC9B,EAAE,EAAE,cAAc,EAClB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC/B,qBAAqB,CAOvB;AAED;;;;GAIG;AACH,wBAAsB,uBAAuB,CAAC,CAAC,EAC7C,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,SAAS,EAAE,CAAC,CAAC,EAAE,qBAAqB,KAAK,OAAO,CAAC,IAAI,CAAC,GACrD,OAAO,CAAC,CAAC,CAAC,CAqCZ"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/operations/errors.ts b/packages/taler-wallet-core/src/operations/errors.ts
new file mode 100644
index 000000000..198d3f8c5
--- /dev/null
+++ b/packages/taler-wallet-core/src/operations/errors.ts
@@ -0,0 +1,121 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019-2020 Taler Systems SA
+
+ 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/>
+ */
+
+/**
+ * Classes and helpers for error handling specific to wallet operations.
+ *
+ * @author Florian Dold <dold@taler.net>
+ */
+
+/**
+ * Imports.
+ */
+import { OperationErrorDetails } from "../types/walletTypes";
+import { TalerErrorCode } from "../TalerErrorCode";
+
+/**
+ * This exception is there to let the caller know that an error happened,
+ * but the error has already been reported by writing it to the database.
+ */
+export class OperationFailedAndReportedError extends Error {
+ constructor(public operationError: OperationErrorDetails) {
+ super(operationError.message);
+
+ // Set the prototype explicitly.
+ Object.setPrototypeOf(this, OperationFailedAndReportedError.prototype);
+ }
+}
+
+/**
+ * This exception is thrown when an error occured and the caller is
+ * responsible for recording the failure in the database.
+ */
+export class OperationFailedError extends Error {
+ static fromCode(
+ ec: TalerErrorCode,
+ message: string,
+ details: Record<string, unknown>,
+ ): OperationFailedError {
+ return new OperationFailedError(makeErrorDetails(ec, message, details));
+ }
+
+ constructor(public operationError: OperationErrorDetails) {
+ super(operationError.message);
+
+ // Set the prototype explicitly.
+ Object.setPrototypeOf(this, OperationFailedError.prototype);
+ }
+}
+
+export function makeErrorDetails(
+ ec: TalerErrorCode,
+ message: string,
+ details: Record<string, unknown>,
+): OperationErrorDetails {
+ return {
+ talerErrorCode: ec,
+ talerErrorHint: `Error: ${TalerErrorCode[ec]}`,
+ details: details,
+ message,
+ };
+}
+
+/**
+ * Run an operation and call the onOpError callback
+ * when there was an exception or operation error that must be reported.
+ * The cause will be re-thrown to the caller.
+ */
+export async function guardOperationException<T>(
+ op: () => Promise<T>,
+ onOpError: (e: OperationErrorDetails) => Promise<void>,
+): Promise<T> {
+ try {
+ return await op();
+ } catch (e) {
+ if (e instanceof OperationFailedAndReportedError) {
+ throw e;
+ }
+ if (e instanceof OperationFailedError) {
+ await onOpError(e.operationError);
+ throw new OperationFailedAndReportedError(e.operationError);
+ }
+ if (e instanceof Error) {
+ const opErr = makeErrorDetails(
+ TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
+ `unexpected exception (message: ${e.message})`,
+ {},
+ );
+ await onOpError(opErr);
+ throw new OperationFailedAndReportedError(opErr);
+ }
+ // Something was thrown that is not even an exception!
+ // Try to stringify it.
+ let excString: string;
+ try {
+ excString = e.toString();
+ } catch (e) {
+ // Something went horribly wrong.
+ excString = "can't stringify exception";
+ }
+ const opErr = makeErrorDetails(
+ TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
+ `unexpected exception (not an exception, ${excString})`,
+ {},
+ );
+ await onOpError(opErr);
+ throw new OperationFailedAndReportedError(opErr);
+ }
+}
diff --git a/packages/taler-wallet-core/src/operations/exchanges.d.ts.map b/packages/taler-wallet-core/src/operations/exchanges.d.ts.map
new file mode 100644
index 000000000..963a271fd
--- /dev/null
+++ b/packages/taler-wallet-core/src/operations/exchanges.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"exchanges.d.ts","sourceRoot":"","sources":["exchanges.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAO9C,OAAO,EACL,cAAc,EAQf,MAAM,kBAAkB,CAAC;AA0R1B,wBAAsB,4BAA4B,CAChD,EAAE,EAAE,mBAAmB,EACvB,eAAe,EAAE,MAAM,EACvB,IAAI,EAAE,MAAM,GAAG,SAAS,GACvB,OAAO,CAAC,IAAI,CAAC,CAUf;AAsFD,wBAAsB,qBAAqB,CACzC,EAAE,EAAE,mBAAmB,EACvB,OAAO,EAAE,MAAM,EACf,QAAQ,UAAQ,GACf,OAAO,CAAC,cAAc,CAAC,CAOzB;AAoED;;GAEG;AACH,wBAAsB,gBAAgB,CACpC,EAAE,EAAE,mBAAmB,EACvB,YAAY,EAAE,cAAc,GAC3B,OAAO,CAAC;IAAE,SAAS,EAAE,OAAO,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,CAAC,CA4BrD;AAED,wBAAsB,mBAAmB,CACvC,EAAE,EAAE,mBAAmB,EACvB,eAAe,EAAE,MAAM,EACvB,oBAAoB,EAAE,MAAM,EAAE,GAC7B,OAAO,CAAC,MAAM,CAAC,CAqBjB"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/operations/exchanges.ts b/packages/taler-wallet-core/src/operations/exchanges.ts
new file mode 100644
index 000000000..ee49fddb5
--- /dev/null
+++ b/packages/taler-wallet-core/src/operations/exchanges.ts
@@ -0,0 +1,555 @@
+/*
+ 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 { InternalWalletState } from "./state";
+import {
+ Denomination,
+ codecForExchangeKeysJson,
+ codecForExchangeWireJson,
+} from "../types/talerTypes";
+import { OperationErrorDetails } from "../types/walletTypes";
+import {
+ ExchangeRecord,
+ ExchangeUpdateStatus,
+ Stores,
+ DenominationRecord,
+ DenominationStatus,
+ WireFee,
+ ExchangeUpdateReason,
+ ExchangeUpdatedEventRecord,
+} from "../types/dbTypes";
+import { canonicalizeBaseUrl } from "../util/helpers";
+import * as Amounts from "../util/amounts";
+import { parsePaytoUri } from "../util/payto";
+import {
+ OperationFailedAndReportedError,
+ guardOperationException,
+ makeErrorDetails,
+} from "./errors";
+import {
+ WALLET_CACHE_BREAKER_CLIENT_VERSION,
+ WALLET_EXCHANGE_PROTOCOL_VERSION,
+} from "./versions";
+import { getTimestampNow } from "../util/time";
+import { compare } from "../util/libtoolVersion";
+import { createRecoupGroup, processRecoupGroup } from "./recoup";
+import { TalerErrorCode } from "../TalerErrorCode";
+import {
+ readSuccessResponseJsonOrThrow,
+ readSuccessResponseTextOrThrow,
+} from "../util/http";
+import { Logger } from "../util/logging";
+import { URL } from "../util/url";
+
+const logger = new Logger("exchanges.ts");
+
+async function denominationRecordFromKeys(
+ ws: InternalWalletState,
+ exchangeBaseUrl: string,
+ denomIn: Denomination,
+): Promise<DenominationRecord> {
+ const denomPubHash = await ws.cryptoApi.hashEncoded(denomIn.denom_pub);
+ const d: DenominationRecord = {
+ denomPub: denomIn.denom_pub,
+ denomPubHash,
+ exchangeBaseUrl,
+ feeDeposit: Amounts.parseOrThrow(denomIn.fee_deposit),
+ feeRefresh: Amounts.parseOrThrow(denomIn.fee_refresh),
+ feeRefund: Amounts.parseOrThrow(denomIn.fee_refund),
+ feeWithdraw: Amounts.parseOrThrow(denomIn.fee_withdraw),
+ isOffered: true,
+ isRevoked: false,
+ masterSig: denomIn.master_sig,
+ stampExpireDeposit: denomIn.stamp_expire_deposit,
+ stampExpireLegal: denomIn.stamp_expire_legal,
+ stampExpireWithdraw: denomIn.stamp_expire_withdraw,
+ stampStart: denomIn.stamp_start,
+ status: DenominationStatus.Unverified,
+ value: Amounts.parseOrThrow(denomIn.value),
+ };
+ return d;
+}
+
+async function setExchangeError(
+ ws: InternalWalletState,
+ baseUrl: string,
+ err: OperationErrorDetails,
+): Promise<void> {
+ console.log(`last error for exchange ${baseUrl}:`, err);
+ const mut = (exchange: ExchangeRecord): ExchangeRecord => {
+ exchange.lastError = err;
+ return exchange;
+ };
+ await ws.db.mutate(Stores.exchanges, baseUrl, mut);
+}
+
+/**
+ * Fetch the exchange's /keys and update our database accordingly.
+ *
+ * Exceptions thrown in this method must be caught and reported
+ * in the pending operations.
+ */
+async function updateExchangeWithKeys(
+ ws: InternalWalletState,
+ baseUrl: string,
+): Promise<void> {
+ const existingExchangeRecord = await ws.db.get(Stores.exchanges, baseUrl);
+
+ if (existingExchangeRecord?.updateStatus != ExchangeUpdateStatus.FetchKeys) {
+ return;
+ }
+
+ const keysUrl = new URL("keys", baseUrl);
+ keysUrl.searchParams.set("cacheBreaker", WALLET_CACHE_BREAKER_CLIENT_VERSION);
+
+ const resp = await ws.http.get(keysUrl.href);
+ const exchangeKeysJson = await readSuccessResponseJsonOrThrow(
+ resp,
+ codecForExchangeKeysJson(),
+ );
+
+ if (exchangeKeysJson.denoms.length === 0) {
+ const opErr = makeErrorDetails(
+ TalerErrorCode.WALLET_EXCHANGE_DENOMINATIONS_INSUFFICIENT,
+ "exchange doesn't offer any denominations",
+ {
+ exchangeBaseUrl: baseUrl,
+ },
+ );
+ await setExchangeError(ws, baseUrl, opErr);
+ throw new OperationFailedAndReportedError(opErr);
+ }
+
+ const protocolVersion = exchangeKeysJson.version;
+
+ const versionRes = compare(WALLET_EXCHANGE_PROTOCOL_VERSION, protocolVersion);
+ if (versionRes?.compatible != true) {
+ const opErr = makeErrorDetails(
+ TalerErrorCode.WALLET_EXCHANGE_PROTOCOL_VERSION_INCOMPATIBLE,
+ "exchange protocol version not compatible with wallet",
+ {
+ exchangeProtocolVersion: protocolVersion,
+ walletProtocolVersion: WALLET_EXCHANGE_PROTOCOL_VERSION,
+ },
+ );
+ await setExchangeError(ws, baseUrl, opErr);
+ throw new OperationFailedAndReportedError(opErr);
+ }
+
+ const currency = Amounts.parseOrThrow(exchangeKeysJson.denoms[0].value)
+ .currency;
+
+ const newDenominations = await Promise.all(
+ exchangeKeysJson.denoms.map((d) =>
+ denominationRecordFromKeys(ws, baseUrl, d),
+ ),
+ );
+
+ const lastUpdateTimestamp = getTimestampNow();
+
+ const recoupGroupId: string | undefined = undefined;
+
+ await ws.db.runWithWriteTransaction(
+ [Stores.exchanges, Stores.denominations, Stores.recoupGroups, Stores.coins],
+ async (tx) => {
+ const r = await tx.get(Stores.exchanges, baseUrl);
+ if (!r) {
+ console.warn(`exchange ${baseUrl} no longer present`);
+ return;
+ }
+ if (r.details) {
+ // FIXME: We need to do some consistency checks!
+ }
+ // FIXME: validate signing keys and merge with old set
+ r.details = {
+ auditors: exchangeKeysJson.auditors,
+ currency: currency,
+ lastUpdateTime: lastUpdateTimestamp,
+ masterPublicKey: exchangeKeysJson.master_public_key,
+ protocolVersion: protocolVersion,
+ signingKeys: exchangeKeysJson.signkeys,
+ };
+ r.updateStatus = ExchangeUpdateStatus.FetchWire;
+ r.lastError = undefined;
+ await tx.put(Stores.exchanges, r);
+
+ for (const newDenom of newDenominations) {
+ const oldDenom = await tx.get(Stores.denominations, [
+ baseUrl,
+ newDenom.denomPub,
+ ]);
+ if (oldDenom) {
+ // FIXME: Do consistency check
+ } else {
+ await tx.put(Stores.denominations, newDenom);
+ }
+ }
+
+ // Handle recoup
+ const recoupDenomList = exchangeKeysJson.recoup ?? [];
+ const newlyRevokedCoinPubs: string[] = [];
+ logger.trace("recoup list from exchange", recoupDenomList);
+ for (const recoupInfo of recoupDenomList) {
+ const oldDenom = await tx.getIndexed(
+ Stores.denominations.denomPubHashIndex,
+ recoupInfo.h_denom_pub,
+ );
+ if (!oldDenom) {
+ // We never even knew about the revoked denomination, all good.
+ continue;
+ }
+ if (oldDenom.isRevoked) {
+ // We already marked the denomination as revoked,
+ // this implies we revoked all coins
+ console.log("denom already revoked");
+ continue;
+ }
+ console.log("revoking denom", recoupInfo.h_denom_pub);
+ oldDenom.isRevoked = true;
+ await tx.put(Stores.denominations, oldDenom);
+ const affectedCoins = await tx
+ .iterIndexed(Stores.coins.denomPubHashIndex, recoupInfo.h_denom_pub)
+ .toArray();
+ for (const ac of affectedCoins) {
+ newlyRevokedCoinPubs.push(ac.coinPub);
+ }
+ }
+ if (newlyRevokedCoinPubs.length != 0) {
+ console.log("recouping coins", newlyRevokedCoinPubs);
+ await createRecoupGroup(ws, tx, newlyRevokedCoinPubs);
+ }
+ },
+ );
+
+ if (recoupGroupId) {
+ // Asynchronously start recoup. This doesn't need to finish
+ // for the exchange update to be considered finished.
+ processRecoupGroup(ws, recoupGroupId).catch((e) => {
+ console.log("error while recouping coins:", e);
+ });
+ }
+}
+
+async function updateExchangeFinalize(
+ ws: InternalWalletState,
+ exchangeBaseUrl: string,
+): Promise<void> {
+ const exchange = await ws.db.get(Stores.exchanges, exchangeBaseUrl);
+ if (!exchange) {
+ return;
+ }
+ if (exchange.updateStatus != ExchangeUpdateStatus.FinalizeUpdate) {
+ return;
+ }
+ await ws.db.runWithWriteTransaction(
+ [Stores.exchanges, Stores.exchangeUpdatedEvents],
+ async (tx) => {
+ const r = await tx.get(Stores.exchanges, exchangeBaseUrl);
+ if (!r) {
+ return;
+ }
+ if (r.updateStatus != ExchangeUpdateStatus.FinalizeUpdate) {
+ return;
+ }
+ r.addComplete = true;
+ r.updateStatus = ExchangeUpdateStatus.Finished;
+ await tx.put(Stores.exchanges, r);
+ const updateEvent: ExchangeUpdatedEventRecord = {
+ exchangeBaseUrl: exchange.baseUrl,
+ timestamp: getTimestampNow(),
+ };
+ await tx.put(Stores.exchangeUpdatedEvents, updateEvent);
+ },
+ );
+}
+
+async function updateExchangeWithTermsOfService(
+ ws: InternalWalletState,
+ exchangeBaseUrl: string,
+): Promise<void> {
+ const exchange = await ws.db.get(Stores.exchanges, exchangeBaseUrl);
+ if (!exchange) {
+ return;
+ }
+ if (exchange.updateStatus != ExchangeUpdateStatus.FetchTerms) {
+ return;
+ }
+ const reqUrl = new URL("terms", exchangeBaseUrl);
+ reqUrl.searchParams.set("cacheBreaker", WALLET_CACHE_BREAKER_CLIENT_VERSION);
+ const headers = {
+ Accept: "text/plain",
+ };
+
+ const resp = await ws.http.get(reqUrl.href, { headers });
+ const tosText = await readSuccessResponseTextOrThrow(resp);
+ const tosEtag = resp.headers.get("etag") || undefined;
+
+ await ws.db.runWithWriteTransaction([Stores.exchanges], async (tx) => {
+ const r = await tx.get(Stores.exchanges, exchangeBaseUrl);
+ if (!r) {
+ return;
+ }
+ if (r.updateStatus != ExchangeUpdateStatus.FetchTerms) {
+ return;
+ }
+ r.termsOfServiceText = tosText;
+ r.termsOfServiceLastEtag = tosEtag;
+ r.updateStatus = ExchangeUpdateStatus.FinalizeUpdate;
+ await tx.put(Stores.exchanges, r);
+ });
+}
+
+export async function acceptExchangeTermsOfService(
+ ws: InternalWalletState,
+ exchangeBaseUrl: string,
+ etag: string | undefined,
+): Promise<void> {
+ await ws.db.runWithWriteTransaction([Stores.exchanges], async (tx) => {
+ const r = await tx.get(Stores.exchanges, exchangeBaseUrl);
+ if (!r) {
+ return;
+ }
+ r.termsOfServiceAcceptedEtag = etag;
+ r.termsOfServiceAcceptedTimestamp = getTimestampNow();
+ await tx.put(Stores.exchanges, r);
+ });
+}
+
+/**
+ * Fetch wire information for an exchange and store it in the database.
+ *
+ * @param exchangeBaseUrl Exchange base URL, assumed to be already normalized.
+ */
+async function updateExchangeWithWireInfo(
+ ws: InternalWalletState,
+ exchangeBaseUrl: string,
+): Promise<void> {
+ const exchange = await ws.db.get(Stores.exchanges, exchangeBaseUrl);
+ if (!exchange) {
+ return;
+ }
+ if (exchange.updateStatus != ExchangeUpdateStatus.FetchWire) {
+ return;
+ }
+ const details = exchange.details;
+ if (!details) {
+ throw Error("invalid exchange state");
+ }
+ const reqUrl = new URL("wire", exchangeBaseUrl);
+ reqUrl.searchParams.set("cacheBreaker", WALLET_CACHE_BREAKER_CLIENT_VERSION);
+
+ const resp = await ws.http.get(reqUrl.href);
+ const wireInfo = await readSuccessResponseJsonOrThrow(
+ resp,
+ codecForExchangeWireJson(),
+ );
+
+ for (const a of wireInfo.accounts) {
+ logger.trace("validating exchange acct");
+ const isValid = await ws.cryptoApi.isValidWireAccount(
+ a.payto_uri,
+ a.master_sig,
+ details.masterPublicKey,
+ );
+ if (!isValid) {
+ throw Error("exchange acct signature invalid");
+ }
+ }
+ const feesForType: { [wireMethod: string]: WireFee[] } = {};
+ for (const wireMethod of Object.keys(wireInfo.fees)) {
+ const feeList: WireFee[] = [];
+ for (const x of wireInfo.fees[wireMethod]) {
+ const startStamp = x.start_date;
+ const endStamp = x.end_date;
+ const fee: WireFee = {
+ closingFee: Amounts.parseOrThrow(x.closing_fee),
+ endStamp,
+ sig: x.sig,
+ startStamp,
+ wireFee: Amounts.parseOrThrow(x.wire_fee),
+ };
+ const isValid = await ws.cryptoApi.isValidWireFee(
+ wireMethod,
+ fee,
+ details.masterPublicKey,
+ );
+ if (!isValid) {
+ throw Error("exchange wire fee signature invalid");
+ }
+ feeList.push(fee);
+ }
+ feesForType[wireMethod] = feeList;
+ }
+
+ await ws.db.runWithWriteTransaction([Stores.exchanges], async (tx) => {
+ const r = await tx.get(Stores.exchanges, exchangeBaseUrl);
+ if (!r) {
+ return;
+ }
+ if (r.updateStatus != ExchangeUpdateStatus.FetchWire) {
+ return;
+ }
+ r.wireInfo = {
+ accounts: wireInfo.accounts,
+ feesForType: feesForType,
+ };
+ r.updateStatus = ExchangeUpdateStatus.FetchTerms;
+ r.lastError = undefined;
+ await tx.put(Stores.exchanges, r);
+ });
+}
+
+export async function updateExchangeFromUrl(
+ ws: InternalWalletState,
+ baseUrl: string,
+ forceNow = false,
+): Promise<ExchangeRecord> {
+ const onOpErr = (e: OperationErrorDetails): Promise<void> =>
+ setExchangeError(ws, baseUrl, e);
+ return await guardOperationException(
+ () => updateExchangeFromUrlImpl(ws, baseUrl, forceNow),
+ onOpErr,
+ );
+}
+
+/**
+ * Update or add exchange DB entry by fetching the /keys and /wire information.
+ * Optionally link the reserve entry to the new or existing
+ * exchange entry in then DB.
+ */
+async function updateExchangeFromUrlImpl(
+ ws: InternalWalletState,
+ baseUrl: string,
+ forceNow = false,
+): Promise<ExchangeRecord> {
+ const now = getTimestampNow();
+ baseUrl = canonicalizeBaseUrl(baseUrl);
+
+ const r = await ws.db.get(Stores.exchanges, baseUrl);
+ if (!r) {
+ const newExchangeRecord: ExchangeRecord = {
+ builtIn: false,
+ addComplete: false,
+ permanent: true,
+ baseUrl: baseUrl,
+ details: undefined,
+ wireInfo: undefined,
+ updateStatus: ExchangeUpdateStatus.FetchKeys,
+ updateStarted: now,
+ updateReason: ExchangeUpdateReason.Initial,
+ timestampAdded: getTimestampNow(),
+ termsOfServiceAcceptedEtag: undefined,
+ termsOfServiceAcceptedTimestamp: undefined,
+ termsOfServiceLastEtag: undefined,
+ termsOfServiceText: undefined,
+ updateDiff: undefined,
+ };
+ await ws.db.put(Stores.exchanges, newExchangeRecord);
+ } else {
+ await ws.db.runWithWriteTransaction([Stores.exchanges], async (t) => {
+ const rec = await t.get(Stores.exchanges, baseUrl);
+ if (!rec) {
+ return;
+ }
+ if (rec.updateStatus != ExchangeUpdateStatus.FetchKeys && !forceNow) {
+ return;
+ }
+ if (rec.updateStatus != ExchangeUpdateStatus.FetchKeys && forceNow) {
+ rec.updateReason = ExchangeUpdateReason.Forced;
+ }
+ rec.updateStarted = now;
+ rec.updateStatus = ExchangeUpdateStatus.FetchKeys;
+ rec.lastError = undefined;
+ t.put(Stores.exchanges, rec);
+ });
+ }
+
+ await updateExchangeWithKeys(ws, baseUrl);
+ await updateExchangeWithWireInfo(ws, baseUrl);
+ await updateExchangeWithTermsOfService(ws, baseUrl);
+ await updateExchangeFinalize(ws, baseUrl);
+
+ const updatedExchange = await ws.db.get(Stores.exchanges, baseUrl);
+
+ if (!updatedExchange) {
+ // This should practically never happen
+ throw Error("exchange not found");
+ }
+ return updatedExchange;
+}
+
+/**
+ * Check if and how an exchange is trusted and/or audited.
+ */
+export async function getExchangeTrust(
+ ws: InternalWalletState,
+ exchangeInfo: ExchangeRecord,
+): Promise<{ isTrusted: boolean; isAudited: boolean }> {
+ let isTrusted = false;
+ let isAudited = false;
+ const exchangeDetails = exchangeInfo.details;
+ if (!exchangeDetails) {
+ throw Error(`exchange ${exchangeInfo.baseUrl} details not available`);
+ }
+ const currencyRecord = await ws.db.get(
+ Stores.currencies,
+ exchangeDetails.currency,
+ );
+ if (currencyRecord) {
+ for (const trustedExchange of currencyRecord.exchanges) {
+ if (trustedExchange.exchangePub === exchangeDetails.masterPublicKey) {
+ isTrusted = true;
+ break;
+ }
+ }
+ for (const trustedAuditor of currencyRecord.auditors) {
+ for (const exchangeAuditor of exchangeDetails.auditors) {
+ if (trustedAuditor.auditorPub === exchangeAuditor.auditor_pub) {
+ isAudited = true;
+ break;
+ }
+ }
+ }
+ }
+ return { isTrusted, isAudited };
+}
+
+export async function getExchangePaytoUri(
+ ws: InternalWalletState,
+ exchangeBaseUrl: string,
+ supportedTargetTypes: string[],
+): Promise<string> {
+ // We do the update here, since the exchange might not even exist
+ // yet in our database.
+ const exchangeRecord = await updateExchangeFromUrl(ws, exchangeBaseUrl);
+ if (!exchangeRecord) {
+ throw Error(`Exchange '${exchangeBaseUrl}' not found.`);
+ }
+ const exchangeWireInfo = exchangeRecord.wireInfo;
+ if (!exchangeWireInfo) {
+ throw Error(`Exchange wire info for '${exchangeBaseUrl}' not found.`);
+ }
+ for (const account of exchangeWireInfo.accounts) {
+ const res = parsePaytoUri(account.payto_uri);
+ if (!res) {
+ continue;
+ }
+ if (supportedTargetTypes.includes(res.targetType)) {
+ return account.payto_uri;
+ }
+ }
+ throw Error("no matching exchange account found");
+}
diff --git a/packages/taler-wallet-core/src/operations/pay.d.ts.map b/packages/taler-wallet-core/src/operations/pay.d.ts.map
new file mode 100644
index 000000000..7ab4d7be6
--- /dev/null
+++ b/packages/taler-wallet-core/src/operations/pay.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"pay.d.ts","sourceRoot":"","sources":["pay.ts"],"names":[],"mappings":"AA6CA,OAAO,EACL,gBAAgB,EAEhB,gBAAgB,EAGjB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAK7C,OAAO,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAY9C;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;OAEG;IACH,aAAa,EAAE,UAAU,CAAC;IAE1B;;OAEG;IACH,QAAQ,EAAE,MAAM,EAAE,CAAC;IAEnB;;OAEG;IACH,iBAAiB,EAAE,UAAU,EAAE,CAAC;IAEhC;;OAEG;IACH,gBAAgB,EAAE,UAAU,CAAC;IAE7B;;OAEG;IACH,mBAAmB,EAAE,UAAU,CAAC;CACjC;AAED;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC;;OAEG;IACH,OAAO,EAAE,MAAM,CAAC;IAEhB;;OAEG;IACH,QAAQ,EAAE,MAAM,CAAC;IAEjB;;;OAGG;IACH,eAAe,EAAE,UAAU,CAAC;IAE5B;;OAEG;IACH,UAAU,EAAE,UAAU,CAAC;CACxB;AAED,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,UAAU,CAAC;CACvB;AAED;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CACvC,EAAE,EAAE,mBAAmB,EACvB,GAAG,EAAE,gBAAgB,GACpB,OAAO,CAAC,WAAW,CAAC,CA+BtB;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,iBAAiB,EAAE,EACzB,mBAAmB,EAAE,UAAU,EAC/B,gBAAgB,EAAE,UAAU,EAC5B,eAAe,EAAE,UAAU,GAC1B,gBAAgB,GAAG,SAAS,CAsF9B;AA+RD,wBAAsB,uBAAuB,CAC3C,EAAE,EAAE,mBAAmB,EACvB,UAAU,EAAE,MAAM,EAClB,QAAQ,UAAQ,GACf,OAAO,CAAC,IAAI,CAAC,CAOf;AAwMD,wBAAsB,SAAS,CAC7B,EAAE,EAAE,mBAAmB,EACvB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,gBAAgB,CAAC,CAmF3B;AAED;;;;;GAKG;AACH,wBAAsB,gBAAgB,CACpC,EAAE,EAAE,mBAAmB,EACvB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,gBAAgB,CAAC,CAgH3B;AAED;;GAEG;AACH,wBAAsB,UAAU,CAC9B,EAAE,EAAE,mBAAmB,EACvB,UAAU,EAAE,MAAM,EAClB,iBAAiB,EAAE,MAAM,GAAG,SAAS,GACpC,OAAO,CAAC,gBAAgB,CAAC,CAwF3B;AAED,wBAAsB,kBAAkB,CACtC,EAAE,EAAE,mBAAmB,EACvB,UAAU,EAAE,MAAM,EAClB,QAAQ,UAAQ,GACf,OAAO,CAAC,IAAI,CAAC,CAOf;AAiCD,wBAAsB,cAAc,CAClC,EAAE,EAAE,mBAAmB,EACvB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,IAAI,CAAC,CAsBf"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/operations/pay.ts b/packages/taler-wallet-core/src/operations/pay.ts
new file mode 100644
index 000000000..f23e326f8
--- /dev/null
+++ b/packages/taler-wallet-core/src/operations/pay.ts
@@ -0,0 +1,1148 @@
+/*
+ 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/>
+ */
+
+/**
+ * Implementation of the payment operation, including downloading and
+ * claiming of proposals.
+ *
+ * @author Florian Dold
+ */
+
+/**
+ * Imports.
+ */
+import { encodeCrock, getRandomBytes } from "../crypto/talerCrypto";
+import {
+ CoinStatus,
+ initRetryInfo,
+ ProposalRecord,
+ ProposalStatus,
+ PurchaseRecord,
+ Stores,
+ updateRetryInfoTimeout,
+ PayEventRecord,
+ WalletContractData,
+} from "../types/dbTypes";
+import { NotificationType } from "../types/notifications";
+import {
+ codecForProposal,
+ codecForContractTerms,
+ CoinDepositPermission,
+ codecForMerchantPayResponse,
+} from "../types/talerTypes";
+import {
+ ConfirmPayResult,
+ OperationErrorDetails,
+ PreparePayResult,
+ RefreshReason,
+ PreparePayResultType,
+} from "../types/walletTypes";
+import * as Amounts from "../util/amounts";
+import { AmountJson } from "../util/amounts";
+import { Logger } from "../util/logging";
+import { parsePayUri } from "../util/taleruri";
+import { guardOperationException, OperationFailedError } from "./errors";
+import { createRefreshGroup, getTotalRefreshCost } from "./refresh";
+import { InternalWalletState } from "./state";
+import { getTimestampNow, timestampAddDuration } from "../util/time";
+import { strcmp, canonicalJson } from "../util/helpers";
+import { readSuccessResponseJsonOrThrow } from "../util/http";
+import { TalerErrorCode } from "../TalerErrorCode";
+import { URL } from "../util/url";
+
+/**
+ * Logger.
+ */
+const logger = new Logger("pay.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;
+}
+
+export interface PayCostInfo {
+ totalCost: AmountJson;
+}
+
+/**
+ * Compute the total cost of a payment to the customer.
+ *
+ * This includes the amount taken by the merchant, fees (wire/deposit) contributed
+ * by the customer, refreshing fees, fees for withdraw-after-refresh and "trimmings"
+ * of coins that are too small to spend.
+ */
+export async function getTotalPaymentCost(
+ ws: InternalWalletState,
+ pcs: PayCoinSelection,
+): Promise<PayCostInfo> {
+ const costs = [];
+ for (let i = 0; i < pcs.coinPubs.length; i++) {
+ const coin = await ws.db.get(Stores.coins, pcs.coinPubs[i]);
+ if (!coin) {
+ throw Error("can't calculate payment cost, coin not found");
+ }
+ const denom = await ws.db.get(Stores.denominations, [
+ coin.exchangeBaseUrl,
+ coin.denomPub,
+ ]);
+ if (!denom) {
+ throw Error(
+ "can't calculate payment cost, denomination for coin not found",
+ );
+ }
+ const allDenoms = await ws.db
+ .iterIndex(
+ Stores.denominations.exchangeBaseUrlIndex,
+ coin.exchangeBaseUrl,
+ )
+ .toArray();
+ const amountLeft = Amounts.sub(denom.value, pcs.coinContributions[i])
+ .amount;
+ const refreshCost = getTotalRefreshCost(allDenoms, denom, amountLeft);
+ costs.push(pcs.coinContributions[i]);
+ costs.push(refreshCost);
+ }
+ return {
+ totalCost: Amounts.sum(costs).amount,
+ };
+}
+
+/**
+ * Given a list of available coins, select coins to spend under the merchant's
+ * constraints.
+ *
+ * This function is only exported for the sake of unit tests.
+ */
+export function selectPayCoins(
+ acis: AvailableCoinInfo[],
+ contractTermsAmount: AmountJson,
+ customerWireFees: AmountJson,
+ depositFeeLimit: AmountJson,
+): PayCoinSelection | undefined {
+ if (acis.length === 0) {
+ return undefined;
+ }
+ const coinPubs: string[] = [];
+ const coinContributions: AmountJson[] = [];
+ // Sort by available amount (descending), deposit fee (ascending) and
+ // denomPub (ascending) if deposit fee is the same
+ // (to guarantee deterministic results)
+ acis.sort(
+ (o1, o2) =>
+ -Amounts.cmp(o1.availableAmount, o2.availableAmount) ||
+ Amounts.cmp(o1.feeDeposit, o2.feeDeposit) ||
+ strcmp(o1.denomPub, o2.denomPub),
+ );
+ const paymentAmount = Amounts.add(contractTermsAmount, customerWireFees)
+ .amount;
+ const currency = paymentAmount.currency;
+ let amountPayRemaining = paymentAmount;
+ let amountDepositFeeLimitRemaining = depositFeeLimit;
+ const customerDepositFees = Amounts.getZero(currency);
+ for (const aci of acis) {
+ // 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 (amountPayRemaining.value === 0 && amountPayRemaining.fraction === 0) {
+ // We have spent enough!
+ break;
+ }
+
+ // How much does the user spend on deposit fees for this coin?
+ const depositFeeSpend = Amounts.sub(
+ aci.feeDeposit,
+ amountDepositFeeLimitRemaining,
+ ).amount;
+
+ if (Amounts.isZero(depositFeeSpend)) {
+ // Fees are still covered by the merchant.
+ amountDepositFeeLimitRemaining = Amounts.sub(
+ amountDepositFeeLimitRemaining,
+ aci.feeDeposit,
+ ).amount;
+ } else {
+ amountDepositFeeLimitRemaining = Amounts.getZero(currency);
+ }
+
+ let coinSpend: AmountJson;
+ const amountActualAvailable = Amounts.sub(
+ aci.availableAmount,
+ depositFeeSpend,
+ ).amount;
+
+ if (Amounts.cmp(amountActualAvailable, amountPayRemaining) > 0) {
+ // Partial spending, as the coin is worth more than the remaining
+ // amount to pay.
+ coinSpend = Amounts.add(amountPayRemaining, depositFeeSpend).amount;
+ // Make sure we contribute at least the deposit fee, otherwise
+ // contributing this coin would cause a loss for the merchant.
+ if (Amounts.cmp(coinSpend, aci.feeDeposit) < 0) {
+ coinSpend = aci.feeDeposit;
+ }
+ amountPayRemaining = Amounts.getZero(currency);
+ } else {
+ // Spend the full remaining amount on the coin
+ coinSpend = aci.availableAmount;
+ amountPayRemaining = Amounts.add(amountPayRemaining, depositFeeSpend)
+ .amount;
+ amountPayRemaining = Amounts.sub(amountPayRemaining, aci.availableAmount)
+ .amount;
+ }
+
+ coinPubs.push(aci.coinPub);
+ coinContributions.push(coinSpend);
+ }
+ if (Amounts.isZero(amountPayRemaining)) {
+ return {
+ paymentAmount: contractTermsAmount,
+ coinContributions,
+ coinPubs,
+ customerDepositFees,
+ customerWireFees,
+ };
+ }
+ return undefined;
+}
+
+/**
+ * Select coins from the wallet's database that can be used
+ * to pay for the given contract.
+ *
+ * If payment is impossible, undefined is returned.
+ */
+async function getCoinsForPayment(
+ ws: InternalWalletState,
+ contractData: WalletContractData,
+): Promise<PayCoinSelection | undefined> {
+ const remainingAmount = contractData.amount;
+
+ const exchanges = await ws.db.iter(Stores.exchanges).toArray();
+
+ for (const exchange of exchanges) {
+ let isOkay = false;
+ const exchangeDetails = exchange.details;
+ if (!exchangeDetails) {
+ continue;
+ }
+ const exchangeFees = exchange.wireInfo;
+ if (!exchangeFees) {
+ continue;
+ }
+
+ // is the exchange explicitly allowed?
+ for (const allowedExchange of contractData.allowedExchanges) {
+ if (allowedExchange.exchangePub === exchangeDetails.masterPublicKey) {
+ isOkay = true;
+ break;
+ }
+ }
+
+ // is the exchange allowed because of one of its auditors?
+ if (!isOkay) {
+ for (const allowedAuditor of contractData.allowedAuditors) {
+ for (const auditor of exchangeDetails.auditors) {
+ if (auditor.auditor_pub === allowedAuditor.auditorPub) {
+ isOkay = true;
+ break;
+ }
+ }
+ if (isOkay) {
+ break;
+ }
+ }
+ }
+
+ if (!isOkay) {
+ continue;
+ }
+
+ const coins = await ws.db
+ .iterIndex(Stores.coins.exchangeBaseUrlIndex, exchange.baseUrl)
+ .toArray();
+
+ if (!coins || coins.length === 0) {
+ continue;
+ }
+
+ // Denomination of the first coin, we assume that all other
+ // coins have the same currency
+ const firstDenom = await ws.db.get(Stores.denominations, [
+ exchange.baseUrl,
+ coins[0].denomPub,
+ ]);
+ if (!firstDenom) {
+ throw Error("db inconsistent");
+ }
+ const currency = firstDenom.value.currency;
+ const acis: AvailableCoinInfo[] = [];
+ for (const coin of coins) {
+ const denom = await ws.db.get(Stores.denominations, [
+ exchange.baseUrl,
+ coin.denomPub,
+ ]);
+ if (!denom) {
+ throw Error("db inconsistent");
+ }
+ if (denom.value.currency !== currency) {
+ console.warn(
+ `same pubkey for different currencies at exchange ${exchange.baseUrl}`,
+ );
+ continue;
+ }
+ if (coin.suspended) {
+ continue;
+ }
+ if (coin.status !== CoinStatus.Fresh) {
+ continue;
+ }
+ acis.push({
+ availableAmount: coin.currentAmount,
+ coinPub: coin.coinPub,
+ denomPub: coin.denomPub,
+ feeDeposit: denom.feeDeposit,
+ });
+ }
+
+ let wireFee: AmountJson | undefined;
+ for (const fee of exchangeFees.feesForType[contractData.wireMethod] || []) {
+ if (
+ fee.startStamp <= contractData.timestamp &&
+ fee.endStamp >= contractData.timestamp
+ ) {
+ wireFee = fee.wireFee;
+ break;
+ }
+ }
+
+ let customerWireFee: AmountJson;
+
+ if (wireFee) {
+ const amortizedWireFee = Amounts.divide(
+ wireFee,
+ contractData.wireFeeAmortization,
+ );
+ if (Amounts.cmp(contractData.maxWireFee, amortizedWireFee) < 0) {
+ customerWireFee = amortizedWireFee;
+ } else {
+ customerWireFee = Amounts.getZero(currency);
+ }
+ } else {
+ customerWireFee = Amounts.getZero(currency);
+ }
+
+ // Try if paying using this exchange works
+ const res = selectPayCoins(
+ acis,
+ remainingAmount,
+ customerWireFee,
+ contractData.maxDepositFee,
+ );
+ if (res) {
+ return res;
+ }
+ }
+ return undefined;
+}
+
+/**
+ * Record all information that is necessary to
+ * pay for a proposal in the wallet's database.
+ */
+async function recordConfirmPay(
+ ws: InternalWalletState,
+ proposal: ProposalRecord,
+ coinSelection: PayCoinSelection,
+ coinDepositPermissions: CoinDepositPermission[],
+ sessionIdOverride: string | undefined,
+): Promise<PurchaseRecord> {
+ const d = proposal.download;
+ if (!d) {
+ throw Error("proposal is in invalid state");
+ }
+ let sessionId;
+ if (sessionIdOverride) {
+ sessionId = sessionIdOverride;
+ } else {
+ sessionId = proposal.downloadSessionId;
+ }
+ logger.trace(`recording payment with session ID ${sessionId}`);
+ const payCostInfo = await getTotalPaymentCost(ws, coinSelection);
+ const t: PurchaseRecord = {
+ abortDone: false,
+ abortRequested: false,
+ contractTermsRaw: d.contractTermsRaw,
+ contractData: d.contractData,
+ lastSessionId: sessionId,
+ payCoinSelection: coinSelection,
+ payCostInfo,
+ coinDepositPermissions,
+ timestampAccept: getTimestampNow(),
+ timestampLastRefundStatus: undefined,
+ proposalId: proposal.proposalId,
+ lastPayError: undefined,
+ lastRefundStatusError: undefined,
+ payRetryInfo: initRetryInfo(),
+ refundStatusRetryInfo: initRetryInfo(),
+ refundStatusRequested: false,
+ timestampFirstSuccessfulPay: undefined,
+ autoRefundDeadline: undefined,
+ paymentSubmitPending: true,
+ refunds: {},
+ };
+
+ await ws.db.runWithWriteTransaction(
+ [Stores.coins, Stores.purchases, Stores.proposals, Stores.refreshGroups],
+ async (tx) => {
+ const p = await tx.get(Stores.proposals, proposal.proposalId);
+ if (p) {
+ p.proposalStatus = ProposalStatus.ACCEPTED;
+ p.lastError = undefined;
+ p.retryInfo = initRetryInfo(false);
+ await tx.put(Stores.proposals, p);
+ }
+ await tx.put(Stores.purchases, t);
+ for (let i = 0; i < coinSelection.coinPubs.length; i++) {
+ const coin = await tx.get(Stores.coins, coinSelection.coinPubs[i]);
+ if (!coin) {
+ throw Error("coin allocated for payment doesn't exist anymore");
+ }
+ coin.status = CoinStatus.Dormant;
+ const remaining = Amounts.sub(
+ coin.currentAmount,
+ coinSelection.coinContributions[i],
+ );
+ if (remaining.saturated) {
+ throw Error("not enough remaining balance on coin for payment");
+ }
+ coin.currentAmount = remaining.amount;
+ await tx.put(Stores.coins, coin);
+ }
+ const refreshCoinPubs = coinSelection.coinPubs.map((x) => ({
+ coinPub: x,
+ }));
+ await createRefreshGroup(ws, tx, refreshCoinPubs, RefreshReason.Pay);
+ },
+ );
+
+ ws.notify({
+ type: NotificationType.ProposalAccepted,
+ proposalId: proposal.proposalId,
+ });
+ return t;
+}
+
+function getNextUrl(contractData: WalletContractData): string {
+ const f = contractData.fulfillmentUrl;
+ if (f.startsWith("http://") || f.startsWith("https://")) {
+ const fu = new URL(contractData.fulfillmentUrl);
+ fu.searchParams.set("order_id", contractData.orderId);
+ return fu.href;
+ } else {
+ return f;
+ }
+}
+
+async function incrementProposalRetry(
+ ws: InternalWalletState,
+ proposalId: string,
+ err: OperationErrorDetails | undefined,
+): Promise<void> {
+ await ws.db.runWithWriteTransaction([Stores.proposals], async (tx) => {
+ const pr = await tx.get(Stores.proposals, proposalId);
+ if (!pr) {
+ return;
+ }
+ if (!pr.retryInfo) {
+ return;
+ }
+ pr.retryInfo.retryCounter++;
+ updateRetryInfoTimeout(pr.retryInfo);
+ pr.lastError = err;
+ await tx.put(Stores.proposals, pr);
+ });
+ if (err) {
+ ws.notify({ type: NotificationType.ProposalOperationError, error: err });
+ }
+}
+
+async function incrementPurchasePayRetry(
+ ws: InternalWalletState,
+ proposalId: string,
+ err: OperationErrorDetails | undefined,
+): Promise<void> {
+ console.log("incrementing purchase pay retry with error", err);
+ await ws.db.runWithWriteTransaction([Stores.purchases], async (tx) => {
+ const pr = await tx.get(Stores.purchases, proposalId);
+ if (!pr) {
+ return;
+ }
+ if (!pr.payRetryInfo) {
+ return;
+ }
+ pr.payRetryInfo.retryCounter++;
+ updateRetryInfoTimeout(pr.payRetryInfo);
+ pr.lastPayError = err;
+ await tx.put(Stores.purchases, pr);
+ });
+ if (err) {
+ ws.notify({ type: NotificationType.PayOperationError, error: err });
+ }
+}
+
+export async function processDownloadProposal(
+ ws: InternalWalletState,
+ proposalId: string,
+ forceNow = false,
+): Promise<void> {
+ const onOpErr = (err: OperationErrorDetails): Promise<void> =>
+ incrementProposalRetry(ws, proposalId, err);
+ await guardOperationException(
+ () => processDownloadProposalImpl(ws, proposalId, forceNow),
+ onOpErr,
+ );
+}
+
+async function resetDownloadProposalRetry(
+ ws: InternalWalletState,
+ proposalId: string,
+): Promise<void> {
+ await ws.db.mutate(Stores.proposals, proposalId, (x) => {
+ if (x.retryInfo.active) {
+ x.retryInfo = initRetryInfo();
+ }
+ return x;
+ });
+}
+
+async function processDownloadProposalImpl(
+ ws: InternalWalletState,
+ proposalId: string,
+ forceNow: boolean,
+): Promise<void> {
+ if (forceNow) {
+ await resetDownloadProposalRetry(ws, proposalId);
+ }
+ const proposal = await ws.db.get(Stores.proposals, proposalId);
+ if (!proposal) {
+ return;
+ }
+ if (proposal.proposalStatus != ProposalStatus.DOWNLOADING) {
+ return;
+ }
+
+ const orderClaimUrl = new URL(
+ `orders/${proposal.orderId}/claim`,
+ proposal.merchantBaseUrl,
+ ).href;
+ logger.trace("downloading contract from '" + orderClaimUrl + "'");
+
+ const requestBody: {
+ nonce: string;
+ token?: string;
+ } = {
+ nonce: proposal.noncePub,
+ };
+ if (proposal.claimToken) {
+ requestBody.token = proposal.claimToken;
+ }
+
+ const resp = await ws.http.postJson(orderClaimUrl, requestBody);
+ const proposalResp = await readSuccessResponseJsonOrThrow(
+ resp,
+ codecForProposal(),
+ );
+
+ // The proposalResp contains the contract terms as raw JSON,
+ // as the coded to parse them doesn't necessarily round-trip.
+ // We need this raw JSON to compute the contract terms hash.
+
+ const contractTermsHash = await ws.cryptoApi.hashString(
+ canonicalJson(proposalResp.contract_terms),
+ );
+
+ const parsedContractTerms = codecForContractTerms().decode(
+ proposalResp.contract_terms,
+ );
+ const fulfillmentUrl = parsedContractTerms.fulfillment_url;
+
+ await ws.db.runWithWriteTransaction(
+ [Stores.proposals, Stores.purchases],
+ async (tx) => {
+ const p = await tx.get(Stores.proposals, proposalId);
+ if (!p) {
+ return;
+ }
+ if (p.proposalStatus !== ProposalStatus.DOWNLOADING) {
+ return;
+ }
+ const amount = Amounts.parseOrThrow(parsedContractTerms.amount);
+ let maxWireFee: AmountJson;
+ if (parsedContractTerms.max_wire_fee) {
+ maxWireFee = Amounts.parseOrThrow(parsedContractTerms.max_wire_fee);
+ } else {
+ maxWireFee = Amounts.getZero(amount.currency);
+ }
+ p.download = {
+ contractData: {
+ amount,
+ contractTermsHash: contractTermsHash,
+ fulfillmentUrl: parsedContractTerms.fulfillment_url,
+ merchantBaseUrl: parsedContractTerms.merchant_base_url,
+ merchantPub: parsedContractTerms.merchant_pub,
+ merchantSig: proposalResp.sig,
+ orderId: parsedContractTerms.order_id,
+ summary: parsedContractTerms.summary,
+ autoRefund: parsedContractTerms.auto_refund,
+ maxWireFee,
+ payDeadline: parsedContractTerms.pay_deadline,
+ refundDeadline: parsedContractTerms.refund_deadline,
+ wireFeeAmortization: parsedContractTerms.wire_fee_amortization || 1,
+ allowedAuditors: parsedContractTerms.auditors.map((x) => ({
+ auditorBaseUrl: x.url,
+ auditorPub: x.master_pub,
+ })),
+ allowedExchanges: parsedContractTerms.exchanges.map((x) => ({
+ exchangeBaseUrl: x.url,
+ exchangePub: x.master_pub,
+ })),
+ timestamp: parsedContractTerms.timestamp,
+ wireMethod: parsedContractTerms.wire_method,
+ wireInfoHash: parsedContractTerms.h_wire,
+ maxDepositFee: Amounts.parseOrThrow(parsedContractTerms.max_fee),
+ merchant: parsedContractTerms.merchant,
+ products: parsedContractTerms.products,
+ summaryI18n: parsedContractTerms.summary_i18n,
+ },
+ contractTermsRaw: JSON.stringify(proposalResp.contract_terms),
+ };
+ if (
+ fulfillmentUrl.startsWith("http://") ||
+ fulfillmentUrl.startsWith("https://")
+ ) {
+ const differentPurchase = await tx.getIndexed(
+ Stores.purchases.fulfillmentUrlIndex,
+ fulfillmentUrl,
+ );
+ if (differentPurchase) {
+ console.log("repurchase detected");
+ p.proposalStatus = ProposalStatus.REPURCHASE;
+ p.repurchaseProposalId = differentPurchase.proposalId;
+ await tx.put(Stores.proposals, p);
+ return;
+ }
+ }
+ p.proposalStatus = ProposalStatus.PROPOSED;
+ await tx.put(Stores.proposals, p);
+ },
+ );
+
+ ws.notify({
+ type: NotificationType.ProposalDownloaded,
+ proposalId: proposal.proposalId,
+ });
+}
+
+/**
+ * Download a proposal and store it in the database.
+ * Returns an id for it to retrieve it later.
+ *
+ * @param sessionId Current session ID, if the proposal is being
+ * downloaded in the context of a session ID.
+ */
+async function startDownloadProposal(
+ ws: InternalWalletState,
+ merchantBaseUrl: string,
+ orderId: string,
+ sessionId: string | undefined,
+ claimToken: string | undefined,
+): Promise<string> {
+ const oldProposal = await ws.db.getIndexed(
+ Stores.proposals.urlAndOrderIdIndex,
+ [merchantBaseUrl, orderId],
+ );
+ if (oldProposal) {
+ await processDownloadProposal(ws, oldProposal.proposalId);
+ return oldProposal.proposalId;
+ }
+
+ const { priv, pub } = await ws.cryptoApi.createEddsaKeypair();
+ const proposalId = encodeCrock(getRandomBytes(32));
+
+ const proposalRecord: ProposalRecord = {
+ download: undefined,
+ noncePriv: priv,
+ noncePub: pub,
+ claimToken,
+ timestamp: getTimestampNow(),
+ merchantBaseUrl,
+ orderId,
+ proposalId: proposalId,
+ proposalStatus: ProposalStatus.DOWNLOADING,
+ repurchaseProposalId: undefined,
+ retryInfo: initRetryInfo(),
+ lastError: undefined,
+ downloadSessionId: sessionId,
+ };
+
+ await ws.db.runWithWriteTransaction([Stores.proposals], async (tx) => {
+ const existingRecord = await tx.getIndexed(
+ Stores.proposals.urlAndOrderIdIndex,
+ [merchantBaseUrl, orderId],
+ );
+ if (existingRecord) {
+ // Created concurrently
+ return;
+ }
+ await tx.put(Stores.proposals, proposalRecord);
+ });
+
+ await processDownloadProposal(ws, proposalId);
+ return proposalId;
+}
+
+export async function submitPay(
+ ws: InternalWalletState,
+ proposalId: string,
+): Promise<ConfirmPayResult> {
+ const purchase = await ws.db.get(Stores.purchases, proposalId);
+ if (!purchase) {
+ throw Error("Purchase not found: " + proposalId);
+ }
+ if (purchase.abortRequested) {
+ throw Error("not submitting payment for aborted purchase");
+ }
+ const sessionId = purchase.lastSessionId;
+
+ console.log("paying with session ID", sessionId);
+
+ const payUrl = new URL(
+ `orders/${purchase.contractData.orderId}/pay`,
+ purchase.contractData.merchantBaseUrl,
+ ).href;
+
+ const reqBody = {
+ coins: purchase.coinDepositPermissions,
+ session_id: purchase.lastSessionId,
+ };
+
+ logger.trace("making pay request", JSON.stringify(reqBody, undefined, 2));
+
+ const resp = await ws.http.postJson(payUrl, reqBody);
+
+ const merchantResp = await readSuccessResponseJsonOrThrow(
+ resp,
+ codecForMerchantPayResponse(),
+ );
+
+ logger.trace("got success from pay URL", merchantResp);
+
+ const now = getTimestampNow();
+
+ const merchantPub = purchase.contractData.merchantPub;
+ const valid: boolean = await ws.cryptoApi.isValidPaymentSignature(
+ merchantResp.sig,
+ purchase.contractData.contractTermsHash,
+ merchantPub,
+ );
+ if (!valid) {
+ console.error("merchant payment signature invalid");
+ // FIXME: properly display error
+ throw Error("merchant payment signature invalid");
+ }
+ const isFirst = purchase.timestampFirstSuccessfulPay === undefined;
+ purchase.timestampFirstSuccessfulPay = now;
+ purchase.paymentSubmitPending = false;
+ purchase.lastPayError = undefined;
+ purchase.payRetryInfo = initRetryInfo(false);
+ if (isFirst) {
+ const ar = purchase.contractData.autoRefund;
+ if (ar) {
+ console.log("auto_refund present");
+ purchase.refundStatusRequested = true;
+ purchase.refundStatusRetryInfo = initRetryInfo();
+ purchase.lastRefundStatusError = undefined;
+ purchase.autoRefundDeadline = timestampAddDuration(now, ar);
+ }
+ }
+
+ await ws.db.runWithWriteTransaction(
+ [Stores.purchases, Stores.payEvents],
+ async (tx) => {
+ await tx.put(Stores.purchases, purchase);
+ const payEvent: PayEventRecord = {
+ proposalId,
+ sessionId,
+ timestamp: now,
+ isReplay: !isFirst,
+ };
+ await tx.put(Stores.payEvents, payEvent);
+ },
+ );
+
+ const nextUrl = getNextUrl(purchase.contractData);
+ ws.cachedNextUrl[purchase.contractData.fulfillmentUrl] = {
+ nextUrl,
+ lastSessionId: sessionId,
+ };
+
+ return { nextUrl };
+}
+
+/**
+ * Check if a payment for the given taler://pay/ URI is possible.
+ *
+ * If the payment is possible, the signature are already generated but not
+ * yet send to the merchant.
+ */
+export async function preparePayForUri(
+ ws: InternalWalletState,
+ talerPayUri: string,
+): Promise<PreparePayResult> {
+ const uriResult = parsePayUri(talerPayUri);
+
+ if (!uriResult) {
+ throw OperationFailedError.fromCode(
+ TalerErrorCode.WALLET_INVALID_TALER_PAY_URI,
+ `invalid taler://pay URI (${talerPayUri})`,
+ {
+ talerPayUri,
+ },
+ );
+ }
+
+ let proposalId = await startDownloadProposal(
+ ws,
+ uriResult.merchantBaseUrl,
+ uriResult.orderId,
+ uriResult.sessionId,
+ uriResult.claimToken,
+ );
+
+ let proposal = await ws.db.get(Stores.proposals, proposalId);
+ if (!proposal) {
+ throw Error(`could not get proposal ${proposalId}`);
+ }
+ if (proposal.proposalStatus === ProposalStatus.REPURCHASE) {
+ const existingProposalId = proposal.repurchaseProposalId;
+ if (!existingProposalId) {
+ throw Error("invalid proposal state");
+ }
+ console.log("using existing purchase for same product");
+ proposal = await ws.db.get(Stores.proposals, existingProposalId);
+ if (!proposal) {
+ throw Error("existing proposal is in wrong state");
+ }
+ }
+ const d = proposal.download;
+ if (!d) {
+ console.error("bad proposal", proposal);
+ throw Error("proposal is in invalid state");
+ }
+ const contractData = d.contractData;
+ const merchantSig = d.contractData.merchantSig;
+ if (!merchantSig) {
+ throw Error("BUG: proposal is in invalid state");
+ }
+
+ proposalId = proposal.proposalId;
+
+ // First check if we already payed for it.
+ const purchase = await ws.db.get(Stores.purchases, proposalId);
+
+ if (!purchase) {
+ // If not already paid, check if we could pay for it.
+ const res = await getCoinsForPayment(ws, contractData);
+
+ if (!res) {
+ logger.info("not confirming payment, insufficient coins");
+ return {
+ status: PreparePayResultType.InsufficientBalance,
+ contractTerms: JSON.parse(d.contractTermsRaw),
+ proposalId: proposal.proposalId,
+ };
+ }
+
+ const costInfo = await getTotalPaymentCost(ws, res);
+ logger.trace("costInfo", costInfo);
+ logger.trace("coinsForPayment", res);
+
+ return {
+ status: PreparePayResultType.PaymentPossible,
+ contractTerms: JSON.parse(d.contractTermsRaw),
+ proposalId: proposal.proposalId,
+ amountEffective: Amounts.stringify(costInfo.totalCost),
+ amountRaw: Amounts.stringify(res.paymentAmount),
+ };
+ }
+
+ if (purchase.lastSessionId !== uriResult.sessionId) {
+ logger.trace(
+ "automatically re-submitting payment with different session ID",
+ );
+ await ws.db.runWithWriteTransaction([Stores.purchases], async (tx) => {
+ const p = await tx.get(Stores.purchases, proposalId);
+ if (!p) {
+ return;
+ }
+ p.lastSessionId = uriResult.sessionId;
+ await tx.put(Stores.purchases, p);
+ });
+ const r = await submitPay(ws, proposalId);
+ return {
+ status: PreparePayResultType.AlreadyConfirmed,
+ contractTerms: JSON.parse(purchase.contractTermsRaw),
+ paid: true,
+ nextUrl: r.nextUrl,
+ };
+ } else if (!purchase.timestampFirstSuccessfulPay) {
+ return {
+ status: PreparePayResultType.AlreadyConfirmed,
+ contractTerms: JSON.parse(purchase.contractTermsRaw),
+ paid: false,
+ };
+ } else if (purchase.paymentSubmitPending) {
+ return {
+ status: PreparePayResultType.AlreadyConfirmed,
+ contractTerms: JSON.parse(purchase.contractTermsRaw),
+ paid: false,
+ };
+ }
+ // FIXME: we don't handle aborted payments correctly here.
+ throw Error("BUG: invariant violation (purchase status)");
+}
+
+/**
+ * Add a contract to the wallet and sign coins, and send them.
+ */
+export async function confirmPay(
+ ws: InternalWalletState,
+ proposalId: string,
+ sessionIdOverride: string | undefined,
+): Promise<ConfirmPayResult> {
+ logger.trace(
+ `executing confirmPay with proposalId ${proposalId} and sessionIdOverride ${sessionIdOverride}`,
+ );
+ const proposal = await ws.db.get(Stores.proposals, proposalId);
+
+ if (!proposal) {
+ throw Error(`proposal with id ${proposalId} not found`);
+ }
+
+ const d = proposal.download;
+ if (!d) {
+ throw Error("proposal is in invalid state");
+ }
+
+ let purchase = await ws.db.get(
+ Stores.purchases,
+ d.contractData.contractTermsHash,
+ );
+
+ if (purchase) {
+ if (
+ sessionIdOverride !== undefined &&
+ sessionIdOverride != purchase.lastSessionId
+ ) {
+ logger.trace(`changing session ID to ${sessionIdOverride}`);
+ await ws.db.mutate(Stores.purchases, purchase.proposalId, (x) => {
+ x.lastSessionId = sessionIdOverride;
+ x.paymentSubmitPending = true;
+ return x;
+ });
+ }
+ logger.trace("confirmPay: submitting payment for existing purchase");
+ return submitPay(ws, proposalId);
+ }
+
+ logger.trace("confirmPay: purchase record does not exist yet");
+
+ const res = await getCoinsForPayment(ws, d.contractData);
+
+ logger.trace("coin selection result", res);
+
+ if (!res) {
+ // Should not happen, since checkPay should be called first
+ logger.warn("not confirming payment, insufficient coins");
+ throw Error("insufficient balance");
+ }
+
+ const depositPermissions: CoinDepositPermission[] = [];
+ for (let i = 0; i < res.coinPubs.length; i++) {
+ const coin = await ws.db.get(Stores.coins, res.coinPubs[i]);
+ if (!coin) {
+ throw Error("can't pay, allocated coin not found anymore");
+ }
+ const denom = await ws.db.get(Stores.denominations, [
+ coin.exchangeBaseUrl,
+ coin.denomPub,
+ ]);
+ if (!denom) {
+ throw Error(
+ "can't pay, denomination of allocated coin not found anymore",
+ );
+ }
+ const dp = await ws.cryptoApi.signDepositPermission({
+ coinPriv: coin.coinPriv,
+ coinPub: coin.coinPub,
+ contractTermsHash: d.contractData.contractTermsHash,
+ denomPubHash: coin.denomPubHash,
+ denomSig: coin.denomSig,
+ exchangeBaseUrl: coin.exchangeBaseUrl,
+ feeDeposit: denom.feeDeposit,
+ merchantPub: d.contractData.merchantPub,
+ refundDeadline: d.contractData.refundDeadline,
+ spendAmount: res.coinContributions[i],
+ timestamp: d.contractData.timestamp,
+ wireInfoHash: d.contractData.wireInfoHash,
+ });
+ depositPermissions.push(dp);
+ }
+ purchase = await recordConfirmPay(
+ ws,
+ proposal,
+ res,
+ depositPermissions,
+ sessionIdOverride,
+ );
+
+ return submitPay(ws, proposalId);
+}
+
+export async function processPurchasePay(
+ ws: InternalWalletState,
+ proposalId: string,
+ forceNow = false,
+): Promise<void> {
+ const onOpErr = (e: OperationErrorDetails): Promise<void> =>
+ incrementPurchasePayRetry(ws, proposalId, e);
+ await guardOperationException(
+ () => processPurchasePayImpl(ws, proposalId, forceNow),
+ onOpErr,
+ );
+}
+
+async function resetPurchasePayRetry(
+ ws: InternalWalletState,
+ proposalId: string,
+): Promise<void> {
+ await ws.db.mutate(Stores.purchases, proposalId, (x) => {
+ if (x.payRetryInfo.active) {
+ x.payRetryInfo = initRetryInfo();
+ }
+ return x;
+ });
+}
+
+async function processPurchasePayImpl(
+ ws: InternalWalletState,
+ proposalId: string,
+ forceNow: boolean,
+): Promise<void> {
+ if (forceNow) {
+ await resetPurchasePayRetry(ws, proposalId);
+ }
+ const purchase = await ws.db.get(Stores.purchases, proposalId);
+ if (!purchase) {
+ return;
+ }
+ if (!purchase.paymentSubmitPending) {
+ return;
+ }
+ logger.trace(`processing purchase pay ${proposalId}`);
+ await submitPay(ws, proposalId);
+}
+
+export async function refuseProposal(
+ ws: InternalWalletState,
+ proposalId: string,
+): Promise<void> {
+ const success = await ws.db.runWithWriteTransaction(
+ [Stores.proposals],
+ async (tx) => {
+ const proposal = await tx.get(Stores.proposals, proposalId);
+ if (!proposal) {
+ logger.trace(`proposal ${proposalId} not found, won't refuse proposal`);
+ return false;
+ }
+ if (proposal.proposalStatus !== ProposalStatus.PROPOSED) {
+ return false;
+ }
+ proposal.proposalStatus = ProposalStatus.REFUSED;
+ await tx.put(Stores.proposals, proposal);
+ return true;
+ },
+ );
+ if (success) {
+ ws.notify({
+ type: NotificationType.ProposalRefused,
+ });
+ }
+}
diff --git a/packages/taler-wallet-core/src/operations/pending.d.ts.map b/packages/taler-wallet-core/src/operations/pending.d.ts.map
new file mode 100644
index 000000000..08897f538
--- /dev/null
+++ b/packages/taler-wallet-core/src/operations/pending.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"pending.d.ts","sourceRoot":"","sources":["pending.ts"],"names":[],"mappings":"AAyBA,OAAO,EACL,yBAAyB,EAI1B,MAAM,kBAAkB,CAAC;AAS1B,OAAO,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AA6X9C,wBAAsB,oBAAoB,CACxC,EAAE,EAAE,mBAAmB,EACvB,EAAE,OAAe,EAAE;;CAAK,GACvB,OAAO,CAAC,yBAAyB,CAAC,CAkCpC"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/operations/pending.ts b/packages/taler-wallet-core/src/operations/pending.ts
new file mode 100644
index 000000000..acad5e634
--- /dev/null
+++ b/packages/taler-wallet-core/src/operations/pending.ts
@@ -0,0 +1,458 @@
+/*
+ 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/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ ExchangeUpdateStatus,
+ ProposalStatus,
+ ReserveRecordStatus,
+ Stores,
+} from "../types/dbTypes";
+import {
+ PendingOperationsResponse,
+ PendingOperationType,
+ ExchangeUpdateOperationStage,
+ ReserveType,
+} from "../types/pending";
+import {
+ Duration,
+ getTimestampNow,
+ Timestamp,
+ getDurationRemaining,
+ durationMin,
+} from "../util/time";
+import { TransactionHandle } from "../util/query";
+import { InternalWalletState } from "./state";
+import { getBalancesInsideTransaction } from "./balance";
+
+function updateRetryDelay(
+ oldDelay: Duration,
+ now: Timestamp,
+ retryTimestamp: Timestamp,
+): Duration {
+ const remaining = getDurationRemaining(retryTimestamp, now);
+ const nextDelay = durationMin(oldDelay, remaining);
+ return nextDelay;
+}
+
+async function gatherExchangePending(
+ tx: TransactionHandle,
+ now: Timestamp,
+ resp: PendingOperationsResponse,
+ onlyDue = false,
+): Promise<void> {
+ if (onlyDue) {
+ // FIXME: exchanges should also be updated regularly
+ return;
+ }
+ await tx.iter(Stores.exchanges).forEach((e) => {
+ switch (e.updateStatus) {
+ case ExchangeUpdateStatus.Finished:
+ if (e.lastError) {
+ resp.pendingOperations.push({
+ type: PendingOperationType.Bug,
+ givesLifeness: false,
+ message:
+ "Exchange record is in FINISHED state but has lastError set",
+ details: {
+ exchangeBaseUrl: e.baseUrl,
+ },
+ });
+ }
+ if (!e.details) {
+ resp.pendingOperations.push({
+ type: PendingOperationType.Bug,
+ givesLifeness: false,
+ message:
+ "Exchange record does not have details, but no update in progress.",
+ details: {
+ exchangeBaseUrl: e.baseUrl,
+ },
+ });
+ }
+ if (!e.wireInfo) {
+ resp.pendingOperations.push({
+ type: PendingOperationType.Bug,
+ givesLifeness: false,
+ message:
+ "Exchange record does not have wire info, but no update in progress.",
+ details: {
+ exchangeBaseUrl: e.baseUrl,
+ },
+ });
+ }
+ break;
+ case ExchangeUpdateStatus.FetchKeys:
+ resp.pendingOperations.push({
+ type: PendingOperationType.ExchangeUpdate,
+ givesLifeness: false,
+ stage: ExchangeUpdateOperationStage.FetchKeys,
+ exchangeBaseUrl: e.baseUrl,
+ lastError: e.lastError,
+ reason: e.updateReason || "unknown",
+ });
+ break;
+ case ExchangeUpdateStatus.FetchWire:
+ resp.pendingOperations.push({
+ type: PendingOperationType.ExchangeUpdate,
+ givesLifeness: false,
+ stage: ExchangeUpdateOperationStage.FetchWire,
+ exchangeBaseUrl: e.baseUrl,
+ lastError: e.lastError,
+ reason: e.updateReason || "unknown",
+ });
+ break;
+ case ExchangeUpdateStatus.FinalizeUpdate:
+ resp.pendingOperations.push({
+ type: PendingOperationType.ExchangeUpdate,
+ givesLifeness: false,
+ stage: ExchangeUpdateOperationStage.FinalizeUpdate,
+ exchangeBaseUrl: e.baseUrl,
+ lastError: e.lastError,
+ reason: e.updateReason || "unknown",
+ });
+ break;
+ default:
+ resp.pendingOperations.push({
+ type: PendingOperationType.Bug,
+ givesLifeness: false,
+ message: "Unknown exchangeUpdateStatus",
+ details: {
+ exchangeBaseUrl: e.baseUrl,
+ exchangeUpdateStatus: e.updateStatus,
+ },
+ });
+ break;
+ }
+ });
+}
+
+async function gatherReservePending(
+ tx: TransactionHandle,
+ now: Timestamp,
+ resp: PendingOperationsResponse,
+ onlyDue = false,
+): Promise<void> {
+ // FIXME: this should be optimized by using an index for "onlyDue==true".
+ await tx.iter(Stores.reserves).forEach((reserve) => {
+ const reserveType = reserve.bankInfo
+ ? ReserveType.TalerBankWithdraw
+ : ReserveType.Manual;
+ if (!reserve.retryInfo.active) {
+ return;
+ }
+ switch (reserve.reserveStatus) {
+ case ReserveRecordStatus.DORMANT:
+ // nothing to report as pending
+ break;
+ case ReserveRecordStatus.WAIT_CONFIRM_BANK:
+ case ReserveRecordStatus.WITHDRAWING:
+ case ReserveRecordStatus.QUERYING_STATUS:
+ case ReserveRecordStatus.REGISTERING_BANK:
+ resp.nextRetryDelay = updateRetryDelay(
+ resp.nextRetryDelay,
+ now,
+ reserve.retryInfo.nextRetry,
+ );
+ if (onlyDue && reserve.retryInfo.nextRetry.t_ms > now.t_ms) {
+ return;
+ }
+ resp.pendingOperations.push({
+ type: PendingOperationType.Reserve,
+ givesLifeness: true,
+ stage: reserve.reserveStatus,
+ timestampCreated: reserve.timestampCreated,
+ reserveType,
+ reservePub: reserve.reservePub,
+ retryInfo: reserve.retryInfo,
+ });
+ break;
+ default:
+ resp.pendingOperations.push({
+ type: PendingOperationType.Bug,
+ givesLifeness: false,
+ message: "Unknown reserve record status",
+ details: {
+ reservePub: reserve.reservePub,
+ reserveStatus: reserve.reserveStatus,
+ },
+ });
+ break;
+ }
+ });
+}
+
+async function gatherRefreshPending(
+ tx: TransactionHandle,
+ now: Timestamp,
+ resp: PendingOperationsResponse,
+ onlyDue = false,
+): Promise<void> {
+ await tx.iter(Stores.refreshGroups).forEach((r) => {
+ if (r.timestampFinished) {
+ return;
+ }
+ resp.nextRetryDelay = updateRetryDelay(
+ resp.nextRetryDelay,
+ now,
+ r.retryInfo.nextRetry,
+ );
+ if (onlyDue && r.retryInfo.nextRetry.t_ms > now.t_ms) {
+ return;
+ }
+
+ resp.pendingOperations.push({
+ type: PendingOperationType.Refresh,
+ givesLifeness: true,
+ refreshGroupId: r.refreshGroupId,
+ finishedPerCoin: r.finishedPerCoin,
+ retryInfo: r.retryInfo,
+ });
+ });
+}
+
+async function gatherWithdrawalPending(
+ tx: TransactionHandle,
+ now: Timestamp,
+ resp: PendingOperationsResponse,
+ onlyDue = false,
+): Promise<void> {
+ await tx.iter(Stores.withdrawalGroups).forEachAsync(async (wsr) => {
+ if (wsr.timestampFinish) {
+ return;
+ }
+ resp.nextRetryDelay = updateRetryDelay(
+ resp.nextRetryDelay,
+ now,
+ wsr.retryInfo.nextRetry,
+ );
+ if (onlyDue && wsr.retryInfo.nextRetry.t_ms > now.t_ms) {
+ return;
+ }
+ let numCoinsWithdrawn = 0;
+ let numCoinsTotal = 0;
+ await tx
+ .iterIndexed(Stores.planchets.byGroup, wsr.withdrawalGroupId)
+ .forEach((x) => {
+ numCoinsTotal++;
+ if (x.withdrawalDone) {
+ numCoinsWithdrawn++;
+ }
+ });
+ resp.pendingOperations.push({
+ type: PendingOperationType.Withdraw,
+ givesLifeness: true,
+ numCoinsTotal,
+ numCoinsWithdrawn,
+ source: wsr.source,
+ withdrawalGroupId: wsr.withdrawalGroupId,
+ lastError: wsr.lastError,
+ });
+ });
+}
+
+async function gatherProposalPending(
+ tx: TransactionHandle,
+ now: Timestamp,
+ resp: PendingOperationsResponse,
+ onlyDue = false,
+): Promise<void> {
+ await tx.iter(Stores.proposals).forEach((proposal) => {
+ if (proposal.proposalStatus == ProposalStatus.PROPOSED) {
+ if (onlyDue) {
+ return;
+ }
+ const dl = proposal.download;
+ if (!dl) {
+ resp.pendingOperations.push({
+ type: PendingOperationType.Bug,
+ message: "proposal is in invalid state",
+ details: {},
+ givesLifeness: false,
+ });
+ } else {
+ resp.pendingOperations.push({
+ type: PendingOperationType.ProposalChoice,
+ givesLifeness: false,
+ merchantBaseUrl: dl.contractData.merchantBaseUrl,
+ proposalId: proposal.proposalId,
+ proposalTimestamp: proposal.timestamp,
+ });
+ }
+ } else if (proposal.proposalStatus == ProposalStatus.DOWNLOADING) {
+ resp.nextRetryDelay = updateRetryDelay(
+ resp.nextRetryDelay,
+ now,
+ proposal.retryInfo.nextRetry,
+ );
+ if (onlyDue && proposal.retryInfo.nextRetry.t_ms > now.t_ms) {
+ return;
+ }
+ resp.pendingOperations.push({
+ type: PendingOperationType.ProposalDownload,
+ givesLifeness: true,
+ merchantBaseUrl: proposal.merchantBaseUrl,
+ orderId: proposal.orderId,
+ proposalId: proposal.proposalId,
+ proposalTimestamp: proposal.timestamp,
+ lastError: proposal.lastError,
+ retryInfo: proposal.retryInfo,
+ });
+ }
+ });
+}
+
+async function gatherTipPending(
+ tx: TransactionHandle,
+ now: Timestamp,
+ resp: PendingOperationsResponse,
+ onlyDue = false,
+): Promise<void> {
+ await tx.iter(Stores.tips).forEach((tip) => {
+ if (tip.pickedUp) {
+ return;
+ }
+ resp.nextRetryDelay = updateRetryDelay(
+ resp.nextRetryDelay,
+ now,
+ tip.retryInfo.nextRetry,
+ );
+ if (onlyDue && tip.retryInfo.nextRetry.t_ms > now.t_ms) {
+ return;
+ }
+ if (tip.acceptedTimestamp) {
+ resp.pendingOperations.push({
+ type: PendingOperationType.TipPickup,
+ givesLifeness: true,
+ merchantBaseUrl: tip.merchantBaseUrl,
+ tipId: tip.tipId,
+ merchantTipId: tip.merchantTipId,
+ });
+ }
+ });
+}
+
+async function gatherPurchasePending(
+ tx: TransactionHandle,
+ now: Timestamp,
+ resp: PendingOperationsResponse,
+ onlyDue = false,
+): Promise<void> {
+ await tx.iter(Stores.purchases).forEach((pr) => {
+ if (pr.paymentSubmitPending) {
+ resp.nextRetryDelay = updateRetryDelay(
+ resp.nextRetryDelay,
+ now,
+ pr.payRetryInfo.nextRetry,
+ );
+ if (!onlyDue || pr.payRetryInfo.nextRetry.t_ms <= now.t_ms) {
+ resp.pendingOperations.push({
+ type: PendingOperationType.Pay,
+ givesLifeness: true,
+ isReplay: false,
+ proposalId: pr.proposalId,
+ retryInfo: pr.payRetryInfo,
+ lastError: pr.lastPayError,
+ });
+ }
+ }
+ if (pr.refundStatusRequested) {
+ resp.nextRetryDelay = updateRetryDelay(
+ resp.nextRetryDelay,
+ now,
+ pr.refundStatusRetryInfo.nextRetry,
+ );
+ if (!onlyDue || pr.refundStatusRetryInfo.nextRetry.t_ms <= now.t_ms) {
+ resp.pendingOperations.push({
+ type: PendingOperationType.RefundQuery,
+ givesLifeness: true,
+ proposalId: pr.proposalId,
+ retryInfo: pr.refundStatusRetryInfo,
+ lastError: pr.lastRefundStatusError,
+ });
+ }
+ }
+ });
+}
+
+async function gatherRecoupPending(
+ tx: TransactionHandle,
+ now: Timestamp,
+ resp: PendingOperationsResponse,
+ onlyDue = false,
+): Promise<void> {
+ await tx.iter(Stores.recoupGroups).forEach((rg) => {
+ if (rg.timestampFinished) {
+ return;
+ }
+ resp.nextRetryDelay = updateRetryDelay(
+ resp.nextRetryDelay,
+ now,
+ rg.retryInfo.nextRetry,
+ );
+ if (onlyDue && rg.retryInfo.nextRetry.t_ms > now.t_ms) {
+ return;
+ }
+ resp.pendingOperations.push({
+ type: PendingOperationType.Recoup,
+ givesLifeness: true,
+ recoupGroupId: rg.recoupGroupId,
+ retryInfo: rg.retryInfo,
+ lastError: rg.lastError,
+ });
+ });
+}
+
+export async function getPendingOperations(
+ ws: InternalWalletState,
+ { onlyDue = false } = {},
+): Promise<PendingOperationsResponse> {
+ const now = getTimestampNow();
+ return await ws.db.runWithReadTransaction(
+ [
+ Stores.exchanges,
+ Stores.reserves,
+ Stores.refreshGroups,
+ Stores.coins,
+ Stores.withdrawalGroups,
+ Stores.proposals,
+ Stores.tips,
+ Stores.purchases,
+ Stores.recoupGroups,
+ Stores.planchets,
+ ],
+ async (tx) => {
+ const walletBalance = await getBalancesInsideTransaction(ws, tx);
+ const resp: PendingOperationsResponse = {
+ nextRetryDelay: { d_ms: Number.MAX_SAFE_INTEGER },
+ onlyDue: onlyDue,
+ walletBalance,
+ pendingOperations: [],
+ };
+ await gatherExchangePending(tx, now, resp, onlyDue);
+ await gatherReservePending(tx, now, resp, onlyDue);
+ await gatherRefreshPending(tx, now, resp, onlyDue);
+ await gatherWithdrawalPending(tx, now, resp, onlyDue);
+ await gatherProposalPending(tx, now, resp, onlyDue);
+ await gatherTipPending(tx, now, resp, onlyDue);
+ await gatherPurchasePending(tx, now, resp, onlyDue);
+ await gatherRecoupPending(tx, now, resp, onlyDue);
+ return resp;
+ },
+ );
+}
diff --git a/packages/taler-wallet-core/src/operations/recoup.d.ts.map b/packages/taler-wallet-core/src/operations/recoup.d.ts.map
new file mode 100644
index 000000000..c5c9254d1
--- /dev/null
+++ b/packages/taler-wallet-core/src/operations/recoup.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"recoup.d.ts","sourceRoot":"","sources":["recoup.ts"],"names":[],"mappings":"AAgBA;;;;;GAKG;AAEH;;GAEG;AACH,OAAO,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAqB9C,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAyPlD,wBAAsB,kBAAkB,CACtC,EAAE,EAAE,mBAAmB,EACvB,aAAa,EAAE,MAAM,EACrB,QAAQ,UAAQ,GACf,OAAO,CAAC,IAAI,CAAC,CASf;AA0BD,wBAAsB,iBAAiB,CACrC,EAAE,EAAE,mBAAmB,EACvB,EAAE,EAAE,iBAAiB,EACrB,QAAQ,EAAE,MAAM,EAAE,GACjB,OAAO,CAAC,MAAM,CAAC,CAmCjB"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/operations/recoup.ts b/packages/taler-wallet-core/src/operations/recoup.ts
new file mode 100644
index 000000000..cc91ab0e9
--- /dev/null
+++ b/packages/taler-wallet-core/src/operations/recoup.ts
@@ -0,0 +1,412 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019-2020 Taler Systems SA
+
+ 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/>
+ */
+
+/**
+ * Implementation of the recoup operation, which allows to recover the
+ * value of coins held in a revoked denomination.
+ *
+ * @author Florian Dold <dold@taler.net>
+ */
+
+/**
+ * Imports.
+ */
+import { InternalWalletState } from "./state";
+import {
+ Stores,
+ CoinStatus,
+ CoinSourceType,
+ CoinRecord,
+ WithdrawCoinSource,
+ RefreshCoinSource,
+ ReserveRecordStatus,
+ RecoupGroupRecord,
+ initRetryInfo,
+ updateRetryInfoTimeout,
+} from "../types/dbTypes";
+
+import { codecForRecoupConfirmation } from "../types/talerTypes";
+import { NotificationType } from "../types/notifications";
+import { forceQueryReserve } from "./reserves";
+
+import { Amounts } from "../util/amounts";
+import { createRefreshGroup, processRefreshGroup } from "./refresh";
+import { RefreshReason, OperationErrorDetails } from "../types/walletTypes";
+import { TransactionHandle } from "../util/query";
+import { encodeCrock, getRandomBytes } from "../crypto/talerCrypto";
+import { getTimestampNow } from "../util/time";
+import { guardOperationException } from "./errors";
+import { readSuccessResponseJsonOrThrow } from "../util/http";
+import { URL } from "../util/url";
+
+async function incrementRecoupRetry(
+ ws: InternalWalletState,
+ recoupGroupId: string,
+ err: OperationErrorDetails | undefined,
+): Promise<void> {
+ await ws.db.runWithWriteTransaction([Stores.recoupGroups], async (tx) => {
+ const r = await tx.get(Stores.recoupGroups, recoupGroupId);
+ if (!r) {
+ return;
+ }
+ if (!r.retryInfo) {
+ return;
+ }
+ r.retryInfo.retryCounter++;
+ updateRetryInfoTimeout(r.retryInfo);
+ r.lastError = err;
+ await tx.put(Stores.recoupGroups, r);
+ });
+ if (err) {
+ ws.notify({ type: NotificationType.RecoupOperationError, error: err });
+ }
+}
+
+async function putGroupAsFinished(
+ ws: InternalWalletState,
+ tx: TransactionHandle,
+ recoupGroup: RecoupGroupRecord,
+ coinIdx: number,
+): Promise<void> {
+ if (recoupGroup.timestampFinished) {
+ return;
+ }
+ recoupGroup.recoupFinishedPerCoin[coinIdx] = true;
+ let allFinished = true;
+ for (const b of recoupGroup.recoupFinishedPerCoin) {
+ if (!b) {
+ allFinished = false;
+ }
+ }
+ if (allFinished) {
+ recoupGroup.timestampFinished = getTimestampNow();
+ recoupGroup.retryInfo = initRetryInfo(false);
+ recoupGroup.lastError = undefined;
+ if (recoupGroup.scheduleRefreshCoins.length > 0) {
+ const refreshGroupId = await createRefreshGroup(
+ ws,
+ tx,
+ recoupGroup.scheduleRefreshCoins.map((x) => ({ coinPub: x })),
+ RefreshReason.Recoup,
+ );
+ processRefreshGroup(ws, refreshGroupId.refreshGroupId).then((e) => {
+ console.error("error while refreshing after recoup", e);
+ });
+ }
+ }
+ await tx.put(Stores.recoupGroups, recoupGroup);
+}
+
+async function recoupTipCoin(
+ ws: InternalWalletState,
+ recoupGroupId: string,
+ coinIdx: number,
+ coin: CoinRecord,
+): Promise<void> {
+ // We can't really recoup a coin we got via tipping.
+ // Thus we just put the coin to sleep.
+ // FIXME: somehow report this to the user
+ await ws.db.runWithWriteTransaction([Stores.recoupGroups], async (tx) => {
+ const recoupGroup = await tx.get(Stores.recoupGroups, recoupGroupId);
+ if (!recoupGroup) {
+ return;
+ }
+ if (recoupGroup.recoupFinishedPerCoin[coinIdx]) {
+ return;
+ }
+ await putGroupAsFinished(ws, tx, recoupGroup, coinIdx);
+ });
+}
+
+async function recoupWithdrawCoin(
+ ws: InternalWalletState,
+ recoupGroupId: string,
+ coinIdx: number,
+ coin: CoinRecord,
+ cs: WithdrawCoinSource,
+): Promise<void> {
+ const reservePub = cs.reservePub;
+ const reserve = await ws.db.get(Stores.reserves, reservePub);
+ if (!reserve) {
+ // FIXME: We should at least emit some pending operation / warning for this?
+ return;
+ }
+
+ ws.notify({
+ type: NotificationType.RecoupStarted,
+ });
+
+ const recoupRequest = await ws.cryptoApi.createRecoupRequest(coin);
+ const reqUrl = new URL(`/coins/${coin.coinPub}/recoup`, coin.exchangeBaseUrl);
+ const resp = await ws.http.postJson(reqUrl.href, recoupRequest);
+ const recoupConfirmation = await readSuccessResponseJsonOrThrow(
+ resp,
+ codecForRecoupConfirmation(),
+ );
+
+ if (recoupConfirmation.reserve_pub !== reservePub) {
+ throw Error(`Coin's reserve doesn't match reserve on recoup`);
+ }
+
+ const exchange = await ws.db.get(Stores.exchanges, coin.exchangeBaseUrl);
+ if (!exchange) {
+ // FIXME: report inconsistency?
+ return;
+ }
+ const exchangeDetails = exchange.details;
+ if (!exchangeDetails) {
+ // FIXME: report inconsistency?
+ return;
+ }
+
+ // FIXME: verify that our expectations about the amount match
+
+ await ws.db.runWithWriteTransaction(
+ [Stores.coins, Stores.reserves, Stores.recoupGroups],
+ async (tx) => {
+ const recoupGroup = await tx.get(Stores.recoupGroups, recoupGroupId);
+ if (!recoupGroup) {
+ return;
+ }
+ if (recoupGroup.recoupFinishedPerCoin[coinIdx]) {
+ return;
+ }
+ const updatedCoin = await tx.get(Stores.coins, coin.coinPub);
+ if (!updatedCoin) {
+ return;
+ }
+ const updatedReserve = await tx.get(Stores.reserves, reserve.reservePub);
+ if (!updatedReserve) {
+ return;
+ }
+ updatedCoin.status = CoinStatus.Dormant;
+ const currency = updatedCoin.currentAmount.currency;
+ updatedCoin.currentAmount = Amounts.getZero(currency);
+ updatedReserve.reserveStatus = ReserveRecordStatus.QUERYING_STATUS;
+ await tx.put(Stores.coins, updatedCoin);
+ await tx.put(Stores.reserves, updatedReserve);
+ await putGroupAsFinished(ws, tx, recoupGroup, coinIdx);
+ },
+ );
+
+ ws.notify({
+ type: NotificationType.RecoupFinished,
+ });
+
+ forceQueryReserve(ws, reserve.reservePub).catch((e) => {
+ console.log("re-querying reserve after recoup failed:", e);
+ });
+}
+
+async function recoupRefreshCoin(
+ ws: InternalWalletState,
+ recoupGroupId: string,
+ coinIdx: number,
+ coin: CoinRecord,
+ cs: RefreshCoinSource,
+): Promise<void> {
+ ws.notify({
+ type: NotificationType.RecoupStarted,
+ });
+
+ const recoupRequest = await ws.cryptoApi.createRecoupRequest(coin);
+ const reqUrl = new URL(`/coins/${coin.coinPub}/recoup`, coin.exchangeBaseUrl);
+ console.log("making recoup request");
+
+ const resp = await ws.http.postJson(reqUrl.href, recoupRequest);
+ const recoupConfirmation = await readSuccessResponseJsonOrThrow(
+ resp,
+ codecForRecoupConfirmation(),
+ );
+
+ if (recoupConfirmation.old_coin_pub != cs.oldCoinPub) {
+ throw Error(`Coin's oldCoinPub doesn't match reserve on recoup`);
+ }
+
+ const exchange = await ws.db.get(Stores.exchanges, coin.exchangeBaseUrl);
+ if (!exchange) {
+ // FIXME: report inconsistency?
+ return;
+ }
+ const exchangeDetails = exchange.details;
+ if (!exchangeDetails) {
+ // FIXME: report inconsistency?
+ return;
+ }
+
+ await ws.db.runWithWriteTransaction(
+ [Stores.coins, Stores.reserves, Stores.recoupGroups, Stores.refreshGroups],
+ async (tx) => {
+ const recoupGroup = await tx.get(Stores.recoupGroups, recoupGroupId);
+ if (!recoupGroup) {
+ return;
+ }
+ if (recoupGroup.recoupFinishedPerCoin[coinIdx]) {
+ return;
+ }
+ const oldCoin = await tx.get(Stores.coins, cs.oldCoinPub);
+ const revokedCoin = await tx.get(Stores.coins, coin.coinPub);
+ if (!revokedCoin) {
+ return;
+ }
+ if (!oldCoin) {
+ return;
+ }
+ revokedCoin.status = CoinStatus.Dormant;
+ oldCoin.currentAmount = Amounts.add(
+ oldCoin.currentAmount,
+ recoupGroup.oldAmountPerCoin[coinIdx],
+ ).amount;
+ console.log(
+ "recoup: setting old coin amount to",
+ Amounts.stringify(oldCoin.currentAmount),
+ );
+ recoupGroup.scheduleRefreshCoins.push(oldCoin.coinPub);
+ await tx.put(Stores.coins, revokedCoin);
+ await tx.put(Stores.coins, oldCoin);
+ await putGroupAsFinished(ws, tx, recoupGroup, coinIdx);
+ },
+ );
+}
+
+async function resetRecoupGroupRetry(
+ ws: InternalWalletState,
+ recoupGroupId: string,
+): Promise<void> {
+ await ws.db.mutate(Stores.recoupGroups, recoupGroupId, (x) => {
+ if (x.retryInfo.active) {
+ x.retryInfo = initRetryInfo();
+ }
+ return x;
+ });
+}
+
+export async function processRecoupGroup(
+ ws: InternalWalletState,
+ recoupGroupId: string,
+ forceNow = false,
+): Promise<void> {
+ await ws.memoProcessRecoup.memo(recoupGroupId, async () => {
+ const onOpErr = (e: OperationErrorDetails): Promise<void> =>
+ incrementRecoupRetry(ws, recoupGroupId, e);
+ return await guardOperationException(
+ async () => await processRecoupGroupImpl(ws, recoupGroupId, forceNow),
+ onOpErr,
+ );
+ });
+}
+
+async function processRecoupGroupImpl(
+ ws: InternalWalletState,
+ recoupGroupId: string,
+ forceNow = false,
+): Promise<void> {
+ if (forceNow) {
+ await resetRecoupGroupRetry(ws, recoupGroupId);
+ }
+ console.log("in processRecoupGroupImpl");
+ const recoupGroup = await ws.db.get(Stores.recoupGroups, recoupGroupId);
+ if (!recoupGroup) {
+ return;
+ }
+ console.log(recoupGroup);
+ if (recoupGroup.timestampFinished) {
+ console.log("recoup group finished");
+ return;
+ }
+ const ps = recoupGroup.coinPubs.map((x, i) =>
+ processRecoup(ws, recoupGroupId, i),
+ );
+ await Promise.all(ps);
+}
+
+export async function createRecoupGroup(
+ ws: InternalWalletState,
+ tx: TransactionHandle,
+ coinPubs: string[],
+): Promise<string> {
+ const recoupGroupId = encodeCrock(getRandomBytes(32));
+
+ const recoupGroup: RecoupGroupRecord = {
+ recoupGroupId,
+ coinPubs: coinPubs,
+ lastError: undefined,
+ timestampFinished: undefined,
+ timestampStarted: getTimestampNow(),
+ retryInfo: initRetryInfo(),
+ recoupFinishedPerCoin: coinPubs.map(() => false),
+ // Will be populated later
+ oldAmountPerCoin: [],
+ scheduleRefreshCoins: [],
+ };
+
+ for (let coinIdx = 0; coinIdx < coinPubs.length; coinIdx++) {
+ const coinPub = coinPubs[coinIdx];
+ const coin = await tx.get(Stores.coins, coinPub);
+ if (!coin) {
+ await putGroupAsFinished(ws, tx, recoupGroup, coinIdx);
+ continue;
+ }
+ if (Amounts.isZero(coin.currentAmount)) {
+ await putGroupAsFinished(ws, tx, recoupGroup, coinIdx);
+ continue;
+ }
+ recoupGroup.oldAmountPerCoin[coinIdx] = coin.currentAmount;
+ coin.currentAmount = Amounts.getZero(coin.currentAmount.currency);
+ await tx.put(Stores.coins, coin);
+ }
+
+ await tx.put(Stores.recoupGroups, recoupGroup);
+
+ return recoupGroupId;
+}
+
+async function processRecoup(
+ ws: InternalWalletState,
+ recoupGroupId: string,
+ coinIdx: number,
+): Promise<void> {
+ const recoupGroup = await ws.db.get(Stores.recoupGroups, recoupGroupId);
+ if (!recoupGroup) {
+ return;
+ }
+ if (recoupGroup.timestampFinished) {
+ return;
+ }
+ if (recoupGroup.recoupFinishedPerCoin[coinIdx]) {
+ return;
+ }
+
+ const coinPub = recoupGroup.coinPubs[coinIdx];
+
+ const coin = await ws.db.get(Stores.coins, coinPub);
+ if (!coin) {
+ throw Error(`Coin ${coinPub} not found, can't request payback`);
+ }
+
+ const cs = coin.coinSource;
+
+ switch (cs.type) {
+ case CoinSourceType.Tip:
+ return recoupTipCoin(ws, recoupGroupId, coinIdx, coin);
+ case CoinSourceType.Refresh:
+ return recoupRefreshCoin(ws, recoupGroupId, coinIdx, coin, cs);
+ case CoinSourceType.Withdraw:
+ return recoupWithdrawCoin(ws, recoupGroupId, coinIdx, coin, cs);
+ default:
+ throw Error("unknown coin source type");
+ }
+}
diff --git a/packages/taler-wallet-core/src/operations/refresh.d.ts.map b/packages/taler-wallet-core/src/operations/refresh.d.ts.map
new file mode 100644
index 000000000..01cbe7458
--- /dev/null
+++ b/packages/taler-wallet-core/src/operations/refresh.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"refresh.d.ts","sourceRoot":"","sources":["refresh.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAW,UAAU,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,EACL,kBAAkB,EAUnB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAI9C,OAAO,EAEL,aAAa,EACb,aAAa,EACb,cAAc,EACf,MAAM,sBAAsB,CAAC;AAc9B;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,kBAAkB,EAAE,EAC5B,cAAc,EAAE,kBAAkB,EAClC,UAAU,EAAE,UAAU,GACrB,UAAU,CAiBZ;AA0WD,wBAAsB,mBAAmB,CACvC,EAAE,EAAE,mBAAmB,EACvB,cAAc,EAAE,MAAM,EACtB,QAAQ,UAAQ,GACf,OAAO,CAAC,IAAI,CAAC,CASf;AAyED;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,EAAE,EAAE,mBAAmB,EACvB,EAAE,EAAE,iBAAiB,EACrB,WAAW,EAAE,aAAa,EAAE,EAC5B,MAAM,EAAE,aAAa,GACpB,OAAO,CAAC,cAAc,CAAC,CA8BzB"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/operations/refresh.ts b/packages/taler-wallet-core/src/operations/refresh.ts
new file mode 100644
index 000000000..646bc2edf
--- /dev/null
+++ b/packages/taler-wallet-core/src/operations/refresh.ts
@@ -0,0 +1,573 @@
+/*
+ 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 { Amounts, AmountJson } from "../util/amounts";
+import {
+ DenominationRecord,
+ Stores,
+ CoinStatus,
+ RefreshPlanchetRecord,
+ CoinRecord,
+ RefreshSessionRecord,
+ initRetryInfo,
+ updateRetryInfoTimeout,
+ RefreshGroupRecord,
+ CoinSourceType,
+} from "../types/dbTypes";
+import { amountToPretty } from "../util/helpers";
+import { TransactionHandle } from "../util/query";
+import { InternalWalletState } from "./state";
+import { Logger } from "../util/logging";
+import { getWithdrawDenomList } from "./withdraw";
+import { updateExchangeFromUrl } from "./exchanges";
+import {
+ OperationErrorDetails,
+ CoinPublicKey,
+ RefreshReason,
+ RefreshGroupId,
+} from "../types/walletTypes";
+import { guardOperationException } from "./errors";
+import { NotificationType } from "../types/notifications";
+import { getRandomBytes, encodeCrock } from "../crypto/talerCrypto";
+import { getTimestampNow } from "../util/time";
+import { readSuccessResponseJsonOrThrow } from "../util/http";
+import {
+ codecForExchangeMeltResponse,
+ codecForExchangeRevealResponse,
+} from "../types/talerTypes";
+import { URL } from "../util/url";
+
+const logger = new Logger("refresh.ts");
+
+/**
+ * Get the amount that we lose when refreshing a coin of the given denomination
+ * with a certain amount left.
+ *
+ * If the amount left is zero, then the refresh cost
+ * is also considered to be zero. If a refresh isn't possible (e.g. due to lack of
+ * the right denominations), then the cost is the full amount left.
+ *
+ * Considers refresh fees, withdrawal fees after refresh and amounts too small
+ * to refresh.
+ */
+export function getTotalRefreshCost(
+ denoms: DenominationRecord[],
+ refreshedDenom: DenominationRecord,
+ amountLeft: AmountJson,
+): AmountJson {
+ const withdrawAmount = Amounts.sub(amountLeft, refreshedDenom.feeRefresh)
+ .amount;
+ const withdrawDenoms = getWithdrawDenomList(withdrawAmount, denoms);
+ const resultingAmount = Amounts.add(
+ Amounts.getZero(withdrawAmount.currency),
+ ...withdrawDenoms.selectedDenoms.map(
+ (d) => Amounts.mult(d.denom.value, d.count).amount,
+ ),
+ ).amount;
+ const totalCost = Amounts.sub(amountLeft, resultingAmount).amount;
+ logger.trace(
+ `total refresh cost for ${amountToPretty(amountLeft)} is ${amountToPretty(
+ totalCost,
+ )}`,
+ );
+ return totalCost;
+}
+
+/**
+ * Create a refresh session inside a refresh group.
+ */
+async function refreshCreateSession(
+ ws: InternalWalletState,
+ refreshGroupId: string,
+ coinIndex: number,
+): Promise<void> {
+ logger.trace(
+ `creating refresh session for coin ${coinIndex} in refresh group ${refreshGroupId}`,
+ );
+ const refreshGroup = await ws.db.get(Stores.refreshGroups, refreshGroupId);
+ if (!refreshGroup) {
+ return;
+ }
+ if (refreshGroup.finishedPerCoin[coinIndex]) {
+ return;
+ }
+ const existingRefreshSession = refreshGroup.refreshSessionPerCoin[coinIndex];
+ if (existingRefreshSession) {
+ return;
+ }
+ const oldCoinPub = refreshGroup.oldCoinPubs[coinIndex];
+ const coin = await ws.db.get(Stores.coins, oldCoinPub);
+ if (!coin) {
+ throw Error("Can't refresh, coin not found");
+ }
+
+ const exchange = await updateExchangeFromUrl(ws, coin.exchangeBaseUrl);
+ if (!exchange) {
+ throw Error("db inconsistent: exchange of coin not found");
+ }
+
+ const oldDenom = await ws.db.get(Stores.denominations, [
+ exchange.baseUrl,
+ coin.denomPub,
+ ]);
+
+ if (!oldDenom) {
+ throw Error("db inconsistent: denomination for coin not found");
+ }
+
+ const availableDenoms: DenominationRecord[] = await ws.db
+ .iterIndex(Stores.denominations.exchangeBaseUrlIndex, exchange.baseUrl)
+ .toArray();
+
+ const availableAmount = Amounts.sub(coin.currentAmount, oldDenom.feeRefresh)
+ .amount;
+
+ const newCoinDenoms = getWithdrawDenomList(availableAmount, availableDenoms);
+
+ if (newCoinDenoms.selectedDenoms.length === 0) {
+ logger.trace(
+ `not refreshing, available amount ${amountToPretty(
+ availableAmount,
+ )} too small`,
+ );
+ await ws.db.runWithWriteTransaction(
+ [Stores.coins, Stores.refreshGroups],
+ async (tx) => {
+ const rg = await tx.get(Stores.refreshGroups, refreshGroupId);
+ if (!rg) {
+ return;
+ }
+ rg.finishedPerCoin[coinIndex] = true;
+ let allDone = true;
+ for (const f of rg.finishedPerCoin) {
+ if (!f) {
+ allDone = false;
+ break;
+ }
+ }
+ if (allDone) {
+ rg.timestampFinished = getTimestampNow();
+ rg.retryInfo = initRetryInfo(false);
+ }
+ await tx.put(Stores.refreshGroups, rg);
+ },
+ );
+ ws.notify({ type: NotificationType.RefreshUnwarranted });
+ return;
+ }
+
+ const refreshSession: RefreshSessionRecord = await ws.cryptoApi.createRefreshSession(
+ exchange.baseUrl,
+ 3,
+ coin,
+ newCoinDenoms,
+ oldDenom.feeRefresh,
+ );
+
+ // Store refresh session and subtract refreshed amount from
+ // coin in the same transaction.
+ await ws.db.runWithWriteTransaction(
+ [Stores.refreshGroups, Stores.coins],
+ async (tx) => {
+ const c = await tx.get(Stores.coins, coin.coinPub);
+ if (!c) {
+ throw Error("coin not found, but marked for refresh");
+ }
+ const r = Amounts.sub(c.currentAmount, refreshSession.amountRefreshInput);
+ if (r.saturated) {
+ console.log("can't refresh coin, no amount left");
+ return;
+ }
+ c.currentAmount = r.amount;
+ c.status = CoinStatus.Dormant;
+ const rg = await tx.get(Stores.refreshGroups, refreshGroupId);
+ if (!rg) {
+ return;
+ }
+ if (rg.refreshSessionPerCoin[coinIndex]) {
+ return;
+ }
+ rg.refreshSessionPerCoin[coinIndex] = refreshSession;
+ await tx.put(Stores.refreshGroups, rg);
+ await tx.put(Stores.coins, c);
+ },
+ );
+ logger.info(
+ `created refresh session for coin #${coinIndex} in ${refreshGroupId}`,
+ );
+ ws.notify({ type: NotificationType.RefreshStarted });
+}
+
+async function refreshMelt(
+ ws: InternalWalletState,
+ refreshGroupId: string,
+ coinIndex: number,
+): Promise<void> {
+ const refreshGroup = await ws.db.get(Stores.refreshGroups, refreshGroupId);
+ if (!refreshGroup) {
+ return;
+ }
+ const refreshSession = refreshGroup.refreshSessionPerCoin[coinIndex];
+ if (!refreshSession) {
+ return;
+ }
+ if (refreshSession.norevealIndex !== undefined) {
+ return;
+ }
+
+ const coin = await ws.db.get(Stores.coins, refreshSession.meltCoinPub);
+
+ if (!coin) {
+ console.error("can't melt coin, it does not exist");
+ return;
+ }
+
+ const reqUrl = new URL(
+ `coins/${coin.coinPub}/melt`,
+ refreshSession.exchangeBaseUrl,
+ );
+ const meltReq = {
+ coin_pub: coin.coinPub,
+ confirm_sig: refreshSession.confirmSig,
+ denom_pub_hash: coin.denomPubHash,
+ denom_sig: coin.denomSig,
+ rc: refreshSession.hash,
+ value_with_fee: Amounts.stringify(refreshSession.amountRefreshInput),
+ };
+ logger.trace(`melt request for coin:`, meltReq);
+ const resp = await ws.http.postJson(reqUrl.href, meltReq);
+ const meltResponse = await readSuccessResponseJsonOrThrow(
+ resp,
+ codecForExchangeMeltResponse(),
+ );
+
+ const norevealIndex = meltResponse.noreveal_index;
+
+ refreshSession.norevealIndex = norevealIndex;
+
+ await ws.db.mutate(Stores.refreshGroups, refreshGroupId, (rg) => {
+ const rs = rg.refreshSessionPerCoin[coinIndex];
+ if (!rs) {
+ return;
+ }
+ if (rs.norevealIndex !== undefined) {
+ return;
+ }
+ if (rs.finishedTimestamp) {
+ return;
+ }
+ rs.norevealIndex = norevealIndex;
+ return rg;
+ });
+
+ ws.notify({
+ type: NotificationType.RefreshMelted,
+ });
+}
+
+async function refreshReveal(
+ ws: InternalWalletState,
+ refreshGroupId: string,
+ coinIndex: number,
+): Promise<void> {
+ const refreshGroup = await ws.db.get(Stores.refreshGroups, refreshGroupId);
+ if (!refreshGroup) {
+ return;
+ }
+ const refreshSession = refreshGroup.refreshSessionPerCoin[coinIndex];
+ if (!refreshSession) {
+ return;
+ }
+ const norevealIndex = refreshSession.norevealIndex;
+ if (norevealIndex === undefined) {
+ throw Error("can't reveal without melting first");
+ }
+ const privs = Array.from(refreshSession.transferPrivs);
+ privs.splice(norevealIndex, 1);
+
+ const planchets = refreshSession.planchetsForGammas[norevealIndex];
+ if (!planchets) {
+ throw Error("refresh index error");
+ }
+
+ const meltCoinRecord = await ws.db.get(
+ Stores.coins,
+ refreshSession.meltCoinPub,
+ );
+ if (!meltCoinRecord) {
+ throw Error("inconsistent database");
+ }
+
+ const evs = planchets.map((x: RefreshPlanchetRecord) => x.coinEv);
+
+ const linkSigs: string[] = [];
+ for (let i = 0; i < refreshSession.newDenoms.length; i++) {
+ const linkSig = await ws.cryptoApi.signCoinLink(
+ meltCoinRecord.coinPriv,
+ refreshSession.newDenomHashes[i],
+ refreshSession.meltCoinPub,
+ refreshSession.transferPubs[norevealIndex],
+ planchets[i].coinEv,
+ );
+ linkSigs.push(linkSig);
+ }
+
+ const req = {
+ coin_evs: evs,
+ new_denoms_h: refreshSession.newDenomHashes,
+ rc: refreshSession.hash,
+ transfer_privs: privs,
+ transfer_pub: refreshSession.transferPubs[norevealIndex],
+ link_sigs: linkSigs,
+ };
+
+ const reqUrl = new URL(
+ `refreshes/${refreshSession.hash}/reveal`,
+ refreshSession.exchangeBaseUrl,
+ );
+
+ const resp = await ws.http.postJson(reqUrl.href, req);
+ const reveal = await readSuccessResponseJsonOrThrow(
+ resp,
+ codecForExchangeRevealResponse(),
+ );
+
+ const coins: CoinRecord[] = [];
+
+ for (let i = 0; i < reveal.ev_sigs.length; i++) {
+ const denom = await ws.db.get(Stores.denominations, [
+ refreshSession.exchangeBaseUrl,
+ refreshSession.newDenoms[i],
+ ]);
+ if (!denom) {
+ console.error("denom not found");
+ continue;
+ }
+ const pc = refreshSession.planchetsForGammas[norevealIndex][i];
+ const denomSig = await ws.cryptoApi.rsaUnblind(
+ reveal.ev_sigs[i].ev_sig,
+ pc.blindingKey,
+ denom.denomPub,
+ );
+ const coin: CoinRecord = {
+ blindingKey: pc.blindingKey,
+ coinPriv: pc.privateKey,
+ coinPub: pc.publicKey,
+ currentAmount: denom.value,
+ denomPub: denom.denomPub,
+ denomPubHash: denom.denomPubHash,
+ denomSig,
+ exchangeBaseUrl: refreshSession.exchangeBaseUrl,
+ status: CoinStatus.Fresh,
+ coinSource: {
+ type: CoinSourceType.Refresh,
+ oldCoinPub: refreshSession.meltCoinPub,
+ },
+ suspended: false,
+ };
+
+ coins.push(coin);
+ }
+
+ await ws.db.runWithWriteTransaction(
+ [Stores.coins, Stores.refreshGroups],
+ async (tx) => {
+ const rg = await tx.get(Stores.refreshGroups, refreshGroupId);
+ if (!rg) {
+ console.log("no refresh session found");
+ return;
+ }
+ const rs = rg.refreshSessionPerCoin[coinIndex];
+ if (!rs) {
+ return;
+ }
+ if (rs.finishedTimestamp) {
+ console.log("refresh session already finished");
+ return;
+ }
+ rs.finishedTimestamp = getTimestampNow();
+ rg.finishedPerCoin[coinIndex] = true;
+ let allDone = true;
+ for (const f of rg.finishedPerCoin) {
+ if (!f) {
+ allDone = false;
+ break;
+ }
+ }
+ if (allDone) {
+ rg.timestampFinished = getTimestampNow();
+ rg.retryInfo = initRetryInfo(false);
+ }
+ for (const coin of coins) {
+ await tx.put(Stores.coins, coin);
+ }
+ await tx.put(Stores.refreshGroups, rg);
+ },
+ );
+ console.log("refresh finished (end of reveal)");
+ ws.notify({
+ type: NotificationType.RefreshRevealed,
+ });
+}
+
+async function incrementRefreshRetry(
+ ws: InternalWalletState,
+ refreshGroupId: string,
+ err: OperationErrorDetails | undefined,
+): Promise<void> {
+ await ws.db.runWithWriteTransaction([Stores.refreshGroups], async (tx) => {
+ const r = await tx.get(Stores.refreshGroups, refreshGroupId);
+ if (!r) {
+ return;
+ }
+ if (!r.retryInfo) {
+ return;
+ }
+ r.retryInfo.retryCounter++;
+ updateRetryInfoTimeout(r.retryInfo);
+ r.lastError = err;
+ await tx.put(Stores.refreshGroups, r);
+ });
+ if (err) {
+ ws.notify({ type: NotificationType.RefreshOperationError, error: err });
+ }
+}
+
+export async function processRefreshGroup(
+ ws: InternalWalletState,
+ refreshGroupId: string,
+ forceNow = false,
+): Promise<void> {
+ await ws.memoProcessRefresh.memo(refreshGroupId, async () => {
+ const onOpErr = (e: OperationErrorDetails): Promise<void> =>
+ incrementRefreshRetry(ws, refreshGroupId, e);
+ return await guardOperationException(
+ async () => await processRefreshGroupImpl(ws, refreshGroupId, forceNow),
+ onOpErr,
+ );
+ });
+}
+
+async function resetRefreshGroupRetry(
+ ws: InternalWalletState,
+ refreshSessionId: string,
+): Promise<void> {
+ await ws.db.mutate(Stores.refreshGroups, refreshSessionId, (x) => {
+ if (x.retryInfo.active) {
+ x.retryInfo = initRetryInfo();
+ }
+ return x;
+ });
+}
+
+async function processRefreshGroupImpl(
+ ws: InternalWalletState,
+ refreshGroupId: string,
+ forceNow: boolean,
+): Promise<void> {
+ if (forceNow) {
+ await resetRefreshGroupRetry(ws, refreshGroupId);
+ }
+ const refreshGroup = await ws.db.get(Stores.refreshGroups, refreshGroupId);
+ if (!refreshGroup) {
+ return;
+ }
+ if (refreshGroup.timestampFinished) {
+ return;
+ }
+ const ps = refreshGroup.oldCoinPubs.map((x, i) =>
+ processRefreshSession(ws, refreshGroupId, i),
+ );
+ await Promise.all(ps);
+ logger.trace("refresh finished");
+}
+
+async function processRefreshSession(
+ ws: InternalWalletState,
+ refreshGroupId: string,
+ coinIndex: number,
+): Promise<void> {
+ logger.trace(
+ `processing refresh session for coin ${coinIndex} of group ${refreshGroupId}`,
+ );
+ let refreshGroup = await ws.db.get(Stores.refreshGroups, refreshGroupId);
+ if (!refreshGroup) {
+ return;
+ }
+ if (refreshGroup.finishedPerCoin[coinIndex]) {
+ return;
+ }
+ if (!refreshGroup.refreshSessionPerCoin[coinIndex]) {
+ await refreshCreateSession(ws, refreshGroupId, coinIndex);
+ refreshGroup = await ws.db.get(Stores.refreshGroups, refreshGroupId);
+ if (!refreshGroup) {
+ return;
+ }
+ }
+ const refreshSession = refreshGroup.refreshSessionPerCoin[coinIndex];
+ if (!refreshSession) {
+ if (!refreshGroup.finishedPerCoin[coinIndex]) {
+ throw Error(
+ "BUG: refresh session was not created and coin not marked as finished",
+ );
+ }
+ return;
+ }
+ if (refreshSession.norevealIndex === undefined) {
+ await refreshMelt(ws, refreshGroupId, coinIndex);
+ }
+ await refreshReveal(ws, refreshGroupId, coinIndex);
+}
+
+/**
+ * Create a refresh group for a list of coins.
+ */
+export async function createRefreshGroup(
+ ws: InternalWalletState,
+ tx: TransactionHandle,
+ oldCoinPubs: CoinPublicKey[],
+ reason: RefreshReason,
+): Promise<RefreshGroupId> {
+ const refreshGroupId = encodeCrock(getRandomBytes(32));
+
+ const refreshGroup: RefreshGroupRecord = {
+ timestampFinished: undefined,
+ finishedPerCoin: oldCoinPubs.map((x) => false),
+ lastError: undefined,
+ lastErrorPerCoin: {},
+ oldCoinPubs: oldCoinPubs.map((x) => x.coinPub),
+ reason,
+ refreshGroupId,
+ refreshSessionPerCoin: oldCoinPubs.map((x) => undefined),
+ retryInfo: initRetryInfo(),
+ };
+
+ await tx.put(Stores.refreshGroups, refreshGroup);
+
+ const processAsync = async (): Promise<void> => {
+ try {
+ await processRefreshGroup(ws, refreshGroupId);
+ } catch (e) {
+ logger.trace(`Error during refresh: ${e}`);
+ }
+ };
+
+ processAsync();
+
+ return {
+ refreshGroupId,
+ };
+}
diff --git a/packages/taler-wallet-core/src/operations/refund.d.ts.map b/packages/taler-wallet-core/src/operations/refund.d.ts.map
new file mode 100644
index 000000000..77efa7cae
--- /dev/null
+++ b/packages/taler-wallet-core/src/operations/refund.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"refund.d.ts","sourceRoot":"","sources":["refund.ts"],"names":[],"mappings":"AAgBA;;;;GAIG;AAEH;;GAEG;AACH,OAAO,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AA0S9C;;;GAGG;AACH,wBAAsB,WAAW,CAC/B,EAAE,EAAE,mBAAmB,EACvB,cAAc,EAAE,MAAM,GACrB,OAAO,CAAC;IAAE,iBAAiB,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC,CA2B5D;AAED,wBAAsB,0BAA0B,CAC9C,EAAE,EAAE,mBAAmB,EACvB,UAAU,EAAE,MAAM,EAClB,QAAQ,UAAQ,GACf,OAAO,CAAC,IAAI,CAAC,CAOf"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/operations/refund.ts b/packages/taler-wallet-core/src/operations/refund.ts
new file mode 100644
index 000000000..9792d2268
--- /dev/null
+++ b/packages/taler-wallet-core/src/operations/refund.ts
@@ -0,0 +1,438 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019-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/>
+ */
+
+/**
+ * Implementation of the refund operation.
+ *
+ * @author Florian Dold
+ */
+
+/**
+ * Imports.
+ */
+import { InternalWalletState } from "./state";
+import {
+ OperationErrorDetails,
+ RefreshReason,
+ CoinPublicKey,
+} from "../types/walletTypes";
+import {
+ Stores,
+ updateRetryInfoTimeout,
+ initRetryInfo,
+ CoinStatus,
+ RefundReason,
+ RefundState,
+ PurchaseRecord,
+} from "../types/dbTypes";
+import { NotificationType } from "../types/notifications";
+import { parseRefundUri } from "../util/taleruri";
+import { createRefreshGroup, getTotalRefreshCost } from "./refresh";
+import { Amounts } from "../util/amounts";
+import {
+ MerchantCoinRefundStatus,
+ MerchantCoinRefundSuccessStatus,
+ MerchantCoinRefundFailureStatus,
+ codecForMerchantOrderStatusPaid,
+} from "../types/talerTypes";
+import { guardOperationException } from "./errors";
+import { getTimestampNow } from "../util/time";
+import { Logger } from "../util/logging";
+import { readSuccessResponseJsonOrThrow } from "../util/http";
+import { TransactionHandle } from "../util/query";
+import { URL } from "../util/url";
+
+const logger = new Logger("refund.ts");
+
+/**
+ * Retry querying and applying refunds for an order later.
+ */
+async function incrementPurchaseQueryRefundRetry(
+ ws: InternalWalletState,
+ proposalId: string,
+ err: OperationErrorDetails | undefined,
+): Promise<void> {
+ await ws.db.runWithWriteTransaction([Stores.purchases], async (tx) => {
+ const pr = await tx.get(Stores.purchases, proposalId);
+ if (!pr) {
+ return;
+ }
+ if (!pr.refundStatusRetryInfo) {
+ return;
+ }
+ pr.refundStatusRetryInfo.retryCounter++;
+ updateRetryInfoTimeout(pr.refundStatusRetryInfo);
+ pr.lastRefundStatusError = err;
+ await tx.put(Stores.purchases, pr);
+ });
+ if (err) {
+ ws.notify({
+ type: NotificationType.RefundStatusOperationError,
+ error: err,
+ });
+ }
+}
+
+function getRefundKey(d: MerchantCoinRefundStatus): string {
+ return `${d.coin_pub}-${d.rtransaction_id}`;
+}
+
+async function applySuccessfulRefund(
+ tx: TransactionHandle,
+ p: PurchaseRecord,
+ refreshCoinsMap: Record<string, { coinPub: string }>,
+ r: MerchantCoinRefundSuccessStatus,
+): Promise<void> {
+ // FIXME: check signature before storing it as valid!
+
+ const refundKey = getRefundKey(r);
+ const coin = await tx.get(Stores.coins, r.coin_pub);
+ if (!coin) {
+ console.warn("coin not found, can't apply refund");
+ return;
+ }
+ const denom = await tx.getIndexed(
+ Stores.denominations.denomPubHashIndex,
+ coin.denomPubHash,
+ );
+ if (!denom) {
+ throw Error("inconsistent database");
+ }
+ refreshCoinsMap[coin.coinPub] = { coinPub: coin.coinPub };
+ const refundAmount = Amounts.parseOrThrow(r.refund_amount);
+ const refundFee = denom.feeRefund;
+ coin.status = CoinStatus.Dormant;
+ coin.currentAmount = Amounts.add(coin.currentAmount, refundAmount).amount;
+ coin.currentAmount = Amounts.sub(coin.currentAmount, refundFee).amount;
+ logger.trace(`coin amount after is ${Amounts.stringify(coin.currentAmount)}`);
+ await tx.put(Stores.coins, coin);
+
+ const allDenoms = await tx
+ .iterIndexed(
+ Stores.denominations.exchangeBaseUrlIndex,
+ coin.exchangeBaseUrl,
+ )
+ .toArray();
+
+ const amountLeft = Amounts.sub(
+ Amounts.add(coin.currentAmount, Amounts.parseOrThrow(r.refund_amount))
+ .amount,
+ denom.feeRefund,
+ ).amount;
+
+ const totalRefreshCostBound = getTotalRefreshCost(
+ allDenoms,
+ denom,
+ amountLeft,
+ );
+
+ p.refunds[refundKey] = {
+ type: RefundState.Applied,
+ executionTime: r.execution_time,
+ refundAmount: Amounts.parseOrThrow(r.refund_amount),
+ refundFee: denom.feeRefund,
+ totalRefreshCostBound,
+ };
+}
+
+async function storePendingRefund(
+ tx: TransactionHandle,
+ p: PurchaseRecord,
+ r: MerchantCoinRefundFailureStatus,
+): Promise<void> {
+ const refundKey = getRefundKey(r);
+
+ const coin = await tx.get(Stores.coins, r.coin_pub);
+ if (!coin) {
+ console.warn("coin not found, can't apply refund");
+ return;
+ }
+ const denom = await tx.getIndexed(
+ Stores.denominations.denomPubHashIndex,
+ coin.denomPubHash,
+ );
+
+ if (!denom) {
+ throw Error("inconsistent database");
+ }
+
+ const allDenoms = await tx
+ .iterIndexed(
+ Stores.denominations.exchangeBaseUrlIndex,
+ coin.exchangeBaseUrl,
+ )
+ .toArray();
+
+ const amountLeft = Amounts.sub(
+ Amounts.add(coin.currentAmount, Amounts.parseOrThrow(r.refund_amount))
+ .amount,
+ denom.feeRefund,
+ ).amount;
+
+ const totalRefreshCostBound = getTotalRefreshCost(
+ allDenoms,
+ denom,
+ amountLeft,
+ );
+
+ p.refunds[refundKey] = {
+ type: RefundState.Pending,
+ executionTime: r.execution_time,
+ refundAmount: Amounts.parseOrThrow(r.refund_amount),
+ refundFee: denom.feeRefund,
+ totalRefreshCostBound,
+ };
+}
+
+async function acceptRefunds(
+ ws: InternalWalletState,
+ proposalId: string,
+ refunds: MerchantCoinRefundStatus[],
+ reason: RefundReason,
+): Promise<void> {
+ console.log("handling refunds", refunds);
+ const now = getTimestampNow();
+
+ await ws.db.runWithWriteTransaction(
+ [
+ Stores.purchases,
+ Stores.coins,
+ Stores.denominations,
+ Stores.refreshGroups,
+ Stores.refundEvents,
+ ],
+ async (tx) => {
+ const p = await tx.get(Stores.purchases, proposalId);
+ if (!p) {
+ console.error("purchase not found, not adding refunds");
+ return;
+ }
+
+ const refreshCoinsMap: Record<string, CoinPublicKey> = {};
+
+ for (const refundStatus of refunds) {
+ const refundKey = getRefundKey(refundStatus);
+ const existingRefundInfo = p.refunds[refundKey];
+
+ // Already failed.
+ if (existingRefundInfo?.type === RefundState.Failed) {
+ continue;
+ }
+
+ // Already applied.
+ if (existingRefundInfo?.type === RefundState.Applied) {
+ continue;
+ }
+
+ // Still pending.
+ if (
+ refundStatus.type === "failure" &&
+ existingRefundInfo?.type === RefundState.Pending
+ ) {
+ continue;
+ }
+
+ // Invariant: (!existingRefundInfo) || (existingRefundInfo === Pending)
+
+ if (refundStatus.type === "success") {
+ await applySuccessfulRefund(tx, p, refreshCoinsMap, refundStatus);
+ } else {
+ await storePendingRefund(tx, p, refundStatus);
+ }
+ }
+
+ const refreshCoinsPubs = Object.values(refreshCoinsMap);
+ await createRefreshGroup(ws, tx, refreshCoinsPubs, RefreshReason.Refund);
+
+ // Are we done with querying yet, or do we need to do another round
+ // after a retry delay?
+ let queryDone = true;
+
+ if (p.autoRefundDeadline && p.autoRefundDeadline.t_ms > now.t_ms) {
+ queryDone = false;
+ }
+
+ let numPendingRefunds = 0;
+ for (const ri of Object.values(p.refunds)) {
+ switch (ri.type) {
+ case RefundState.Pending:
+ numPendingRefunds++;
+ break;
+ }
+ }
+
+ if (numPendingRefunds > 0) {
+ queryDone = false;
+ }
+
+ if (queryDone) {
+ p.timestampLastRefundStatus = now;
+ p.lastRefundStatusError = undefined;
+ p.refundStatusRetryInfo = initRetryInfo(false);
+ p.refundStatusRequested = false;
+ logger.trace("refund query done");
+ } else {
+ // No error, but we need to try again!
+ p.timestampLastRefundStatus = now;
+ p.refundStatusRetryInfo.retryCounter++;
+ updateRetryInfoTimeout(p.refundStatusRetryInfo);
+ p.lastRefundStatusError = undefined;
+ logger.trace("refund query not done");
+ }
+
+ await tx.put(Stores.purchases, p);
+ },
+ );
+
+ ws.notify({
+ type: NotificationType.RefundQueried,
+ });
+}
+
+async function startRefundQuery(
+ ws: InternalWalletState,
+ proposalId: string,
+): Promise<void> {
+ const success = await ws.db.runWithWriteTransaction(
+ [Stores.purchases],
+ async (tx) => {
+ const p = await tx.get(Stores.purchases, proposalId);
+ if (!p) {
+ logger.error("no purchase found for refund URL");
+ return false;
+ }
+ p.refundStatusRequested = true;
+ p.lastRefundStatusError = undefined;
+ p.refundStatusRetryInfo = initRetryInfo();
+ await tx.put(Stores.purchases, p);
+ return true;
+ },
+ );
+
+ if (!success) {
+ return;
+ }
+
+ ws.notify({
+ type: NotificationType.RefundStarted,
+ });
+
+ await processPurchaseQueryRefund(ws, proposalId);
+}
+
+/**
+ * Accept a refund, return the contract hash for the contract
+ * that was involved in the refund.
+ */
+export async function applyRefund(
+ ws: InternalWalletState,
+ talerRefundUri: string,
+): Promise<{ contractTermsHash: string; proposalId: string }> {
+ const parseResult = parseRefundUri(talerRefundUri);
+
+ logger.trace("applying refund", parseResult);
+
+ if (!parseResult) {
+ throw Error("invalid refund URI");
+ }
+
+ const purchase = await ws.db.getIndexed(Stores.purchases.orderIdIndex, [
+ parseResult.merchantBaseUrl,
+ parseResult.orderId,
+ ]);
+
+ if (!purchase) {
+ throw Error(
+ `no purchase for the taler://refund/ URI (${talerRefundUri}) was found`,
+ );
+ }
+
+ logger.info("processing purchase for refund");
+ await startRefundQuery(ws, purchase.proposalId);
+
+ return {
+ contractTermsHash: purchase.contractData.contractTermsHash,
+ proposalId: purchase.proposalId,
+ };
+}
+
+export async function processPurchaseQueryRefund(
+ ws: InternalWalletState,
+ proposalId: string,
+ forceNow = false,
+): Promise<void> {
+ const onOpErr = (e: OperationErrorDetails): Promise<void> =>
+ incrementPurchaseQueryRefundRetry(ws, proposalId, e);
+ await guardOperationException(
+ () => processPurchaseQueryRefundImpl(ws, proposalId, forceNow),
+ onOpErr,
+ );
+}
+
+async function resetPurchaseQueryRefundRetry(
+ ws: InternalWalletState,
+ proposalId: string,
+): Promise<void> {
+ await ws.db.mutate(Stores.purchases, proposalId, (x) => {
+ if (x.refundStatusRetryInfo.active) {
+ x.refundStatusRetryInfo = initRetryInfo();
+ }
+ return x;
+ });
+}
+
+async function processPurchaseQueryRefundImpl(
+ ws: InternalWalletState,
+ proposalId: string,
+ forceNow: boolean,
+): Promise<void> {
+ if (forceNow) {
+ await resetPurchaseQueryRefundRetry(ws, proposalId);
+ }
+ const purchase = await ws.db.get(Stores.purchases, proposalId);
+ if (!purchase) {
+ return;
+ }
+
+ if (!purchase.refundStatusRequested) {
+ return;
+ }
+
+ const requestUrl = new URL(
+ `orders/${purchase.contractData.orderId}`,
+ purchase.contractData.merchantBaseUrl,
+ );
+ requestUrl.searchParams.set(
+ "h_contract",
+ purchase.contractData.contractTermsHash,
+ );
+
+ const request = await ws.http.get(requestUrl.href);
+
+ console.log("got json", JSON.stringify(await request.json(), undefined, 2));
+
+ const refundResponse = await readSuccessResponseJsonOrThrow(
+ request,
+ codecForMerchantOrderStatusPaid(),
+ );
+
+ await acceptRefunds(
+ ws,
+ proposalId,
+ refundResponse.refunds,
+ RefundReason.NormalRefund,
+ );
+}
diff --git a/packages/taler-wallet-core/src/operations/reserves.d.ts.map b/packages/taler-wallet-core/src/operations/reserves.d.ts.map
new file mode 100644
index 000000000..33d646ba5
--- /dev/null
+++ b/packages/taler-wallet-core/src/operations/reserves.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"reserves.d.ts","sourceRoot":"","sources":["reserves.ts"],"names":[],"mappings":"AAgBA,OAAO,EACL,oBAAoB,EACpB,qBAAqB,EAErB,wBAAwB,EACzB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AA+C9C,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAyBlD;;;;;GAKG;AACH,wBAAsB,aAAa,CACjC,EAAE,EAAE,mBAAmB,EACvB,GAAG,EAAE,oBAAoB,GACxB,OAAO,CAAC,qBAAqB,CAAC,CA+JhC;AAED;;GAEG;AACH,wBAAsB,iBAAiB,CACrC,EAAE,EAAE,mBAAmB,EACvB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,IAAI,CAAC,CAoBf;AAED;;;;;;GAMG;AACH,wBAAsB,cAAc,CAClC,EAAE,EAAE,mBAAmB,EACvB,UAAU,EAAE,MAAM,EAClB,QAAQ,UAAQ,GACf,OAAO,CAAC,IAAI,CAAC,CASf;AA+CD,wBAAsB,wBAAwB,CAC5C,EAAE,EAAE,mBAAmB,EACvB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,IAAI,CAAC,CAOf;AA8ZD,wBAAsB,0BAA0B,CAC9C,EAAE,EAAE,mBAAmB,EACvB,gBAAgB,EAAE,MAAM,EACxB,gBAAgB,EAAE,MAAM,GACvB,OAAO,CAAC,wBAAwB,CAAC,CAqBnC;AAED;;GAEG;AACH,wBAAsB,mBAAmB,CACvC,EAAE,EAAE,iBAAiB,EACrB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,EAAE,CAAC,CAuBnB"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/operations/reserves.ts b/packages/taler-wallet-core/src/operations/reserves.ts
new file mode 100644
index 000000000..58095affd
--- /dev/null
+++ b/packages/taler-wallet-core/src/operations/reserves.ts
@@ -0,0 +1,841 @@
+/*
+ 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 {
+ CreateReserveRequest,
+ CreateReserveResponse,
+ OperationErrorDetails,
+ AcceptWithdrawalResponse,
+} from "../types/walletTypes";
+import { canonicalizeBaseUrl } from "../util/helpers";
+import { InternalWalletState } from "./state";
+import {
+ ReserveRecordStatus,
+ ReserveRecord,
+ CurrencyRecord,
+ Stores,
+ WithdrawalGroupRecord,
+ initRetryInfo,
+ updateRetryInfoTimeout,
+ ReserveUpdatedEventRecord,
+ WalletReserveHistoryItemType,
+ WithdrawalSourceType,
+ ReserveHistoryRecord,
+ ReserveBankInfo,
+} from "../types/dbTypes";
+import { Logger } from "../util/logging";
+import { Amounts } from "../util/amounts";
+import {
+ updateExchangeFromUrl,
+ getExchangeTrust,
+ getExchangePaytoUri,
+} from "./exchanges";
+import {
+ codecForWithdrawOperationStatusResponse,
+ codecForBankWithdrawalOperationPostResponse,
+} from "../types/talerTypes";
+import { assertUnreachable } from "../util/assertUnreachable";
+import { encodeCrock, getRandomBytes } from "../crypto/talerCrypto";
+import { randomBytes } from "../crypto/primitives/nacl-fast";
+import {
+ selectWithdrawalDenoms,
+ processWithdrawGroup,
+ getBankWithdrawalInfo,
+ denomSelectionInfoToState,
+} from "./withdraw";
+import {
+ guardOperationException,
+ OperationFailedAndReportedError,
+ makeErrorDetails,
+} from "./errors";
+import { NotificationType } from "../types/notifications";
+import { codecForReserveStatus } from "../types/ReserveStatus";
+import { getTimestampNow } from "../util/time";
+import {
+ reconcileReserveHistory,
+ summarizeReserveHistory,
+} from "../util/reserveHistoryUtil";
+import { TransactionHandle } from "../util/query";
+import { addPaytoQueryParams } from "../util/payto";
+import { TalerErrorCode } from "../TalerErrorCode";
+import {
+ readSuccessResponseJsonOrErrorCode,
+ throwUnexpectedRequestError,
+ readSuccessResponseJsonOrThrow,
+} from "../util/http";
+import { codecForAny } from "../util/codec";
+import { URL } from "../util/url";
+
+const logger = new Logger("reserves.ts");
+
+async function resetReserveRetry(
+ ws: InternalWalletState,
+ reservePub: string,
+): Promise<void> {
+ await ws.db.mutate(Stores.reserves, reservePub, (x) => {
+ if (x.retryInfo.active) {
+ x.retryInfo = initRetryInfo();
+ }
+ return x;
+ });
+}
+
+/**
+ * Create a reserve, but do not flag it as confirmed yet.
+ *
+ * Adds the corresponding exchange as a trusted exchange if it is neither
+ * audited nor trusted already.
+ */
+export async function createReserve(
+ ws: InternalWalletState,
+ req: CreateReserveRequest,
+): Promise<CreateReserveResponse> {
+ const keypair = await ws.cryptoApi.createEddsaKeypair();
+ const now = getTimestampNow();
+ const canonExchange = canonicalizeBaseUrl(req.exchange);
+
+ let reserveStatus;
+ if (req.bankWithdrawStatusUrl) {
+ reserveStatus = ReserveRecordStatus.REGISTERING_BANK;
+ } else {
+ reserveStatus = ReserveRecordStatus.QUERYING_STATUS;
+ }
+
+ let bankInfo: ReserveBankInfo | undefined;
+
+ if (req.bankWithdrawStatusUrl) {
+ if (!req.exchangePaytoUri) {
+ throw Error(
+ "Exchange payto URI must be specified for a bank-integrated withdrawal",
+ );
+ }
+ bankInfo = {
+ statusUrl: req.bankWithdrawStatusUrl,
+ exchangePaytoUri: req.exchangePaytoUri,
+ };
+ }
+
+ const initialWithdrawalGroupId = encodeCrock(getRandomBytes(32));
+
+ const denomSelInfo = await selectWithdrawalDenoms(
+ ws,
+ canonExchange,
+ req.amount,
+ );
+ const initialDenomSel = denomSelectionInfoToState(denomSelInfo);
+
+ const reserveRecord: ReserveRecord = {
+ instructedAmount: req.amount,
+ initialWithdrawalGroupId,
+ initialDenomSel,
+ initialWithdrawalStarted: false,
+ timestampCreated: now,
+ exchangeBaseUrl: canonExchange,
+ reservePriv: keypair.priv,
+ reservePub: keypair.pub,
+ senderWire: req.senderWire,
+ timestampBankConfirmed: undefined,
+ timestampReserveInfoPosted: undefined,
+ bankInfo,
+ reserveStatus,
+ lastSuccessfulStatusQuery: undefined,
+ retryInfo: initRetryInfo(),
+ lastError: undefined,
+ currency: req.amount.currency,
+ };
+
+ const reserveHistoryRecord: ReserveHistoryRecord = {
+ reservePub: keypair.pub,
+ reserveTransactions: [],
+ };
+
+ reserveHistoryRecord.reserveTransactions.push({
+ type: WalletReserveHistoryItemType.Credit,
+ expectedAmount: req.amount,
+ });
+
+ const senderWire = req.senderWire;
+ if (senderWire) {
+ const rec = {
+ paytoUri: senderWire,
+ };
+ await ws.db.put(Stores.senderWires, rec);
+ }
+
+ const exchangeInfo = await updateExchangeFromUrl(ws, req.exchange);
+ const exchangeDetails = exchangeInfo.details;
+ if (!exchangeDetails) {
+ console.log(exchangeDetails);
+ throw Error("exchange not updated");
+ }
+ const { isAudited, isTrusted } = await getExchangeTrust(ws, exchangeInfo);
+ let currencyRecord = await ws.db.get(
+ Stores.currencies,
+ exchangeDetails.currency,
+ );
+ if (!currencyRecord) {
+ currencyRecord = {
+ auditors: [],
+ exchanges: [],
+ fractionalDigits: 2,
+ name: exchangeDetails.currency,
+ };
+ }
+
+ if (!isAudited && !isTrusted) {
+ currencyRecord.exchanges.push({
+ baseUrl: req.exchange,
+ exchangePub: exchangeDetails.masterPublicKey,
+ });
+ }
+
+ const cr: CurrencyRecord = currencyRecord;
+
+ const resp = await ws.db.runWithWriteTransaction(
+ [
+ Stores.currencies,
+ Stores.reserves,
+ Stores.reserveHistory,
+ Stores.bankWithdrawUris,
+ ],
+ async (tx) => {
+ // Check if we have already created a reserve for that bankWithdrawStatusUrl
+ if (reserveRecord.bankInfo?.statusUrl) {
+ const bwi = await tx.get(
+ Stores.bankWithdrawUris,
+ reserveRecord.bankInfo.statusUrl,
+ );
+ if (bwi) {
+ const otherReserve = await tx.get(Stores.reserves, bwi.reservePub);
+ if (otherReserve) {
+ logger.trace(
+ "returning existing reserve for bankWithdrawStatusUri",
+ );
+ return {
+ exchange: otherReserve.exchangeBaseUrl,
+ reservePub: otherReserve.reservePub,
+ };
+ }
+ }
+ await tx.put(Stores.bankWithdrawUris, {
+ reservePub: reserveRecord.reservePub,
+ talerWithdrawUri: reserveRecord.bankInfo.statusUrl,
+ });
+ }
+ await tx.put(Stores.currencies, cr);
+ await tx.put(Stores.reserves, reserveRecord);
+ await tx.put(Stores.reserveHistory, reserveHistoryRecord);
+ const r: CreateReserveResponse = {
+ exchange: canonExchange,
+ reservePub: keypair.pub,
+ };
+ return r;
+ },
+ );
+
+ if (reserveRecord.reservePub === resp.reservePub) {
+ // Only emit notification when a new reserve was created.
+ ws.notify({
+ type: NotificationType.ReserveCreated,
+ reservePub: reserveRecord.reservePub,
+ });
+ }
+
+ // Asynchronously process the reserve, but return
+ // to the caller already.
+ processReserve(ws, resp.reservePub, true).catch((e) => {
+ logger.error("Processing reserve (after createReserve) failed:", e);
+ });
+
+ return resp;
+}
+
+/**
+ * Re-query the status of a reserve.
+ */
+export async function forceQueryReserve(
+ ws: InternalWalletState,
+ reservePub: string,
+): Promise<void> {
+ await ws.db.runWithWriteTransaction([Stores.reserves], async (tx) => {
+ const reserve = await tx.get(Stores.reserves, reservePub);
+ if (!reserve) {
+ return;
+ }
+ // Only force status query where it makes sense
+ switch (reserve.reserveStatus) {
+ case ReserveRecordStatus.DORMANT:
+ case ReserveRecordStatus.WITHDRAWING:
+ case ReserveRecordStatus.QUERYING_STATUS:
+ break;
+ default:
+ return;
+ }
+ reserve.reserveStatus = ReserveRecordStatus.QUERYING_STATUS;
+ reserve.retryInfo = initRetryInfo();
+ await tx.put(Stores.reserves, reserve);
+ });
+ await processReserve(ws, reservePub, true);
+}
+
+/**
+ * First fetch information requred to withdraw from the reserve,
+ * then deplete the reserve, withdrawing coins until it is empty.
+ *
+ * The returned promise resolves once the reserve is set to the
+ * state DORMANT.
+ */
+export async function processReserve(
+ ws: InternalWalletState,
+ reservePub: string,
+ forceNow = false,
+): Promise<void> {
+ return ws.memoProcessReserve.memo(reservePub, async () => {
+ const onOpError = (err: OperationErrorDetails): Promise<void> =>
+ incrementReserveRetry(ws, reservePub, err);
+ await guardOperationException(
+ () => processReserveImpl(ws, reservePub, forceNow),
+ onOpError,
+ );
+ });
+}
+
+async function registerReserveWithBank(
+ ws: InternalWalletState,
+ reservePub: string,
+): Promise<void> {
+ const reserve = await ws.db.get(Stores.reserves, reservePub);
+ switch (reserve?.reserveStatus) {
+ case ReserveRecordStatus.WAIT_CONFIRM_BANK:
+ case ReserveRecordStatus.REGISTERING_BANK:
+ break;
+ default:
+ return;
+ }
+ const bankInfo = reserve.bankInfo;
+ if (!bankInfo) {
+ return;
+ }
+ const bankStatusUrl = bankInfo.statusUrl;
+ const httpResp = await ws.http.postJson(bankStatusUrl, {
+ reserve_pub: reservePub,
+ selected_exchange: bankInfo.exchangePaytoUri,
+ });
+ await readSuccessResponseJsonOrThrow(
+ httpResp,
+ codecForBankWithdrawalOperationPostResponse(),
+ );
+ await ws.db.mutate(Stores.reserves, reservePub, (r) => {
+ switch (r.reserveStatus) {
+ case ReserveRecordStatus.REGISTERING_BANK:
+ case ReserveRecordStatus.WAIT_CONFIRM_BANK:
+ break;
+ default:
+ return;
+ }
+ r.timestampReserveInfoPosted = getTimestampNow();
+ r.reserveStatus = ReserveRecordStatus.WAIT_CONFIRM_BANK;
+ if (!r.bankInfo) {
+ throw Error("invariant failed");
+ }
+ r.retryInfo = initRetryInfo();
+ return r;
+ });
+ ws.notify({ type: NotificationType.ReserveRegisteredWithBank });
+ return processReserveBankStatus(ws, reservePub);
+}
+
+export async function processReserveBankStatus(
+ ws: InternalWalletState,
+ reservePub: string,
+): Promise<void> {
+ const onOpError = (err: OperationErrorDetails): Promise<void> =>
+ incrementReserveRetry(ws, reservePub, err);
+ await guardOperationException(
+ () => processReserveBankStatusImpl(ws, reservePub),
+ onOpError,
+ );
+}
+
+async function processReserveBankStatusImpl(
+ ws: InternalWalletState,
+ reservePub: string,
+): Promise<void> {
+ const reserve = await ws.db.get(Stores.reserves, reservePub);
+ switch (reserve?.reserveStatus) {
+ case ReserveRecordStatus.WAIT_CONFIRM_BANK:
+ case ReserveRecordStatus.REGISTERING_BANK:
+ break;
+ default:
+ return;
+ }
+ const bankStatusUrl = reserve.bankInfo?.statusUrl;
+ if (!bankStatusUrl) {
+ return;
+ }
+
+ const statusResp = await ws.http.get(bankStatusUrl);
+ const status = await readSuccessResponseJsonOrThrow(
+ statusResp,
+ codecForWithdrawOperationStatusResponse(),
+ );
+
+ if (status.selection_done) {
+ if (reserve.reserveStatus === ReserveRecordStatus.REGISTERING_BANK) {
+ await registerReserveWithBank(ws, reservePub);
+ return await processReserveBankStatus(ws, reservePub);
+ }
+ } else {
+ await registerReserveWithBank(ws, reservePub);
+ return await processReserveBankStatus(ws, reservePub);
+ }
+
+ if (status.transfer_done) {
+ await ws.db.mutate(Stores.reserves, reservePub, (r) => {
+ switch (r.reserveStatus) {
+ case ReserveRecordStatus.REGISTERING_BANK:
+ case ReserveRecordStatus.WAIT_CONFIRM_BANK:
+ break;
+ default:
+ return;
+ }
+ const now = getTimestampNow();
+ r.timestampBankConfirmed = now;
+ r.reserveStatus = ReserveRecordStatus.QUERYING_STATUS;
+ r.retryInfo = initRetryInfo();
+ return r;
+ });
+ await processReserveImpl(ws, reservePub, true);
+ } else {
+ await ws.db.mutate(Stores.reserves, reservePub, (r) => {
+ switch (r.reserveStatus) {
+ case ReserveRecordStatus.WAIT_CONFIRM_BANK:
+ break;
+ default:
+ return;
+ }
+ if (r.bankInfo) {
+ r.bankInfo.confirmUrl = status.confirm_transfer_url;
+ }
+ return r;
+ });
+ await incrementReserveRetry(ws, reservePub, undefined);
+ }
+}
+
+async function incrementReserveRetry(
+ ws: InternalWalletState,
+ reservePub: string,
+ err: OperationErrorDetails | undefined,
+): Promise<void> {
+ await ws.db.runWithWriteTransaction([Stores.reserves], async (tx) => {
+ const r = await tx.get(Stores.reserves, reservePub);
+ if (!r) {
+ return;
+ }
+ if (!r.retryInfo) {
+ return;
+ }
+ r.retryInfo.retryCounter++;
+ updateRetryInfoTimeout(r.retryInfo);
+ r.lastError = err;
+ await tx.put(Stores.reserves, r);
+ });
+ if (err) {
+ ws.notify({
+ type: NotificationType.ReserveOperationError,
+ error: err,
+ });
+ }
+}
+
+/**
+ * Update the information about a reserve that is stored in the wallet
+ * by quering the reserve's exchange.
+ */
+async function updateReserve(
+ ws: InternalWalletState,
+ reservePub: string,
+): Promise<{ ready: boolean }> {
+ const reserve = await ws.db.get(Stores.reserves, reservePub);
+ if (!reserve) {
+ throw Error("reserve not in db");
+ }
+
+ if (reserve.reserveStatus !== ReserveRecordStatus.QUERYING_STATUS) {
+ return { ready: true };
+ }
+
+ const resp = await ws.http.get(
+ new URL(`reserves/${reservePub}`, reserve.exchangeBaseUrl).href,
+ );
+
+ const result = await readSuccessResponseJsonOrErrorCode(
+ resp,
+ codecForReserveStatus(),
+ );
+ if (result.isError) {
+ if (
+ resp.status === 404 &&
+ result.talerErrorResponse.code === TalerErrorCode.RESERVE_STATUS_UNKNOWN
+ ) {
+ ws.notify({
+ type: NotificationType.ReserveNotYetFound,
+ reservePub,
+ });
+ await incrementReserveRetry(ws, reservePub, undefined);
+ return { ready: false };
+ } else {
+ throwUnexpectedRequestError(resp, result.talerErrorResponse);
+ }
+ }
+
+ const reserveInfo = result.response;
+
+ const balance = Amounts.parseOrThrow(reserveInfo.balance);
+ const currency = balance.currency;
+ await ws.db.runWithWriteTransaction(
+ [Stores.reserves, Stores.reserveUpdatedEvents, Stores.reserveHistory],
+ async (tx) => {
+ const r = await tx.get(Stores.reserves, reservePub);
+ if (!r) {
+ return;
+ }
+ if (r.reserveStatus !== ReserveRecordStatus.QUERYING_STATUS) {
+ return;
+ }
+
+ const hist = await tx.get(Stores.reserveHistory, reservePub);
+ if (!hist) {
+ throw Error("inconsistent database");
+ }
+
+ const newHistoryTransactions = reserveInfo.history.slice(
+ hist.reserveTransactions.length,
+ );
+
+ const reserveUpdateId = encodeCrock(getRandomBytes(32));
+
+ const reconciled = reconcileReserveHistory(
+ hist.reserveTransactions,
+ reserveInfo.history,
+ );
+
+ const summary = summarizeReserveHistory(
+ reconciled.updatedLocalHistory,
+ currency,
+ );
+
+ if (
+ reconciled.newAddedItems.length + reconciled.newMatchedItems.length !=
+ 0
+ ) {
+ const reserveUpdate: ReserveUpdatedEventRecord = {
+ reservePub: r.reservePub,
+ timestamp: getTimestampNow(),
+ amountReserveBalance: Amounts.stringify(balance),
+ amountExpected: Amounts.stringify(summary.awaitedReserveAmount),
+ newHistoryTransactions,
+ reserveUpdateId,
+ };
+ await tx.put(Stores.reserveUpdatedEvents, reserveUpdate);
+ r.reserveStatus = ReserveRecordStatus.WITHDRAWING;
+ r.retryInfo = initRetryInfo();
+ } else {
+ r.reserveStatus = ReserveRecordStatus.DORMANT;
+ r.retryInfo = initRetryInfo(false);
+ }
+ r.lastSuccessfulStatusQuery = getTimestampNow();
+ hist.reserveTransactions = reconciled.updatedLocalHistory;
+ r.lastError = undefined;
+ await tx.put(Stores.reserves, r);
+ await tx.put(Stores.reserveHistory, hist);
+ },
+ );
+ ws.notify({ type: NotificationType.ReserveUpdated });
+ return { ready: true };
+}
+
+async function processReserveImpl(
+ ws: InternalWalletState,
+ reservePub: string,
+ forceNow = false,
+): Promise<void> {
+ const reserve = await ws.db.get(Stores.reserves, reservePub);
+ if (!reserve) {
+ console.log("not processing reserve: reserve does not exist");
+ return;
+ }
+ if (!forceNow) {
+ const now = getTimestampNow();
+ if (reserve.retryInfo.nextRetry.t_ms > now.t_ms) {
+ logger.trace("processReserve retry not due yet");
+ return;
+ }
+ } else {
+ await resetReserveRetry(ws, reservePub);
+ }
+ logger.trace(
+ `Processing reserve ${reservePub} with status ${reserve.reserveStatus}`,
+ );
+ switch (reserve.reserveStatus) {
+ case ReserveRecordStatus.REGISTERING_BANK:
+ await processReserveBankStatus(ws, reservePub);
+ return await processReserveImpl(ws, reservePub, true);
+ case ReserveRecordStatus.QUERYING_STATUS: {
+ const res = await updateReserve(ws, reservePub);
+ if (res.ready) {
+ return await processReserveImpl(ws, reservePub, true);
+ } else {
+ break;
+ }
+ }
+ case ReserveRecordStatus.WITHDRAWING:
+ await depleteReserve(ws, reservePub);
+ break;
+ case ReserveRecordStatus.DORMANT:
+ // nothing to do
+ break;
+ case ReserveRecordStatus.WAIT_CONFIRM_BANK:
+ await processReserveBankStatus(ws, reservePub);
+ break;
+ default:
+ console.warn("unknown reserve record status:", reserve.reserveStatus);
+ assertUnreachable(reserve.reserveStatus);
+ break;
+ }
+}
+
+/**
+ * Withdraw coins from a reserve until it is empty.
+ *
+ * When finished, marks the reserve as depleted by setting
+ * the depleted timestamp.
+ */
+async function depleteReserve(
+ ws: InternalWalletState,
+ reservePub: string,
+): Promise<void> {
+ let reserve: ReserveRecord | undefined;
+ let hist: ReserveHistoryRecord | undefined;
+ await ws.db.runWithReadTransaction(
+ [Stores.reserves, Stores.reserveHistory],
+ async (tx) => {
+ reserve = await tx.get(Stores.reserves, reservePub);
+ hist = await tx.get(Stores.reserveHistory, reservePub);
+ },
+ );
+
+ if (!reserve) {
+ return;
+ }
+ if (!hist) {
+ throw Error("inconsistent database");
+ }
+ if (reserve.reserveStatus !== ReserveRecordStatus.WITHDRAWING) {
+ return;
+ }
+ logger.trace(`depleting reserve ${reservePub}`);
+
+ const summary = summarizeReserveHistory(
+ hist.reserveTransactions,
+ reserve.currency,
+ );
+
+ const withdrawAmount = summary.unclaimedReserveAmount;
+
+ const denomsForWithdraw = await selectWithdrawalDenoms(
+ ws,
+ reserve.exchangeBaseUrl,
+ withdrawAmount,
+ );
+ if (!denomsForWithdraw) {
+ // Only complain about inability to withdraw if we
+ // didn't withdraw before.
+ if (Amounts.isZero(summary.withdrawnAmount)) {
+ const opErr = makeErrorDetails(
+ TalerErrorCode.WALLET_EXCHANGE_DENOMINATIONS_INSUFFICIENT,
+ `Unable to withdraw from reserve, no denominations are available to withdraw.`,
+ {},
+ );
+ await incrementReserveRetry(ws, reserve.reservePub, opErr);
+ throw new OperationFailedAndReportedError(opErr);
+ }
+ return;
+ }
+
+ logger.trace(
+ `Selected coins total cost ${Amounts.stringify(
+ denomsForWithdraw.totalWithdrawCost,
+ )} for withdrawal of ${Amounts.stringify(withdrawAmount)}`,
+ );
+
+ logger.trace("selected denominations");
+
+ const newWithdrawalGroup = await ws.db.runWithWriteTransaction(
+ [
+ Stores.withdrawalGroups,
+ Stores.reserves,
+ Stores.reserveHistory,
+ Stores.planchets,
+ ],
+ async (tx) => {
+ const newReserve = await tx.get(Stores.reserves, reservePub);
+ if (!newReserve) {
+ return false;
+ }
+ if (newReserve.reserveStatus !== ReserveRecordStatus.WITHDRAWING) {
+ return false;
+ }
+ const newHist = await tx.get(Stores.reserveHistory, reservePub);
+ if (!newHist) {
+ throw Error("inconsistent database");
+ }
+ const newSummary = summarizeReserveHistory(
+ newHist.reserveTransactions,
+ newReserve.currency,
+ );
+ if (
+ Amounts.cmp(
+ newSummary.unclaimedReserveAmount,
+ denomsForWithdraw.totalWithdrawCost,
+ ) < 0
+ ) {
+ // Something must have happened concurrently!
+ logger.error(
+ "aborting withdrawal session, likely concurrent withdrawal happened",
+ );
+ logger.error(
+ `unclaimed reserve amount is ${newSummary.unclaimedReserveAmount}`,
+ );
+ logger.error(
+ `withdrawal cost is ${denomsForWithdraw.totalWithdrawCost}`,
+ );
+ return false;
+ }
+ for (let i = 0; i < denomsForWithdraw.selectedDenoms.length; i++) {
+ const sd = denomsForWithdraw.selectedDenoms[i];
+ for (let j = 0; j < sd.count; j++) {
+ const amt = Amounts.add(sd.denom.value, sd.denom.feeWithdraw).amount;
+ newHist.reserveTransactions.push({
+ type: WalletReserveHistoryItemType.Withdraw,
+ expectedAmount: amt,
+ });
+ }
+ }
+ newReserve.reserveStatus = ReserveRecordStatus.DORMANT;
+ newReserve.retryInfo = initRetryInfo(false);
+
+ let withdrawalGroupId: string;
+
+ if (!newReserve.initialWithdrawalStarted) {
+ withdrawalGroupId = newReserve.initialWithdrawalGroupId;
+ newReserve.initialWithdrawalStarted = true;
+ } else {
+ withdrawalGroupId = encodeCrock(randomBytes(32));
+ }
+
+ const withdrawalRecord: WithdrawalGroupRecord = {
+ withdrawalGroupId: withdrawalGroupId,
+ exchangeBaseUrl: newReserve.exchangeBaseUrl,
+ source: {
+ type: WithdrawalSourceType.Reserve,
+ reservePub: newReserve.reservePub,
+ },
+ rawWithdrawalAmount: withdrawAmount,
+ timestampStart: getTimestampNow(),
+ retryInfo: initRetryInfo(),
+ lastErrorPerCoin: {},
+ lastError: undefined,
+ denomsSel: denomSelectionInfoToState(denomsForWithdraw),
+ };
+
+ await tx.put(Stores.reserves, newReserve);
+ await tx.put(Stores.reserveHistory, newHist);
+ await tx.put(Stores.withdrawalGroups, withdrawalRecord);
+ return withdrawalRecord;
+ },
+ );
+
+ if (newWithdrawalGroup) {
+ logger.trace("processing new withdraw group");
+ ws.notify({
+ type: NotificationType.WithdrawGroupCreated,
+ withdrawalGroupId: newWithdrawalGroup.withdrawalGroupId,
+ });
+ await processWithdrawGroup(ws, newWithdrawalGroup.withdrawalGroupId);
+ } else {
+ console.trace("withdraw session already existed");
+ }
+}
+
+export async function createTalerWithdrawReserve(
+ ws: InternalWalletState,
+ talerWithdrawUri: string,
+ selectedExchange: string,
+): Promise<AcceptWithdrawalResponse> {
+ const withdrawInfo = await getBankWithdrawalInfo(ws, talerWithdrawUri);
+ const exchangeWire = await getExchangePaytoUri(
+ ws,
+ selectedExchange,
+ withdrawInfo.wireTypes,
+ );
+ const reserve = await createReserve(ws, {
+ amount: withdrawInfo.amount,
+ bankWithdrawStatusUrl: withdrawInfo.extractedStatusUrl,
+ exchange: selectedExchange,
+ senderWire: withdrawInfo.senderWire,
+ exchangePaytoUri: exchangeWire,
+ });
+ // We do this here, as the reserve should be registered before we return,
+ // so that we can redirect the user to the bank's status page.
+ await processReserveBankStatus(ws, reserve.reservePub);
+ return {
+ reservePub: reserve.reservePub,
+ confirmTransferUrl: withdrawInfo.confirmTransferUrl,
+ };
+}
+
+/**
+ * Get payto URIs needed to fund a reserve.
+ */
+export async function getFundingPaytoUris(
+ tx: TransactionHandle,
+ reservePub: string,
+): Promise<string[]> {
+ const r = await tx.get(Stores.reserves, reservePub);
+ if (!r) {
+ logger.error(`reserve ${reservePub} not found (DB corrupted?)`);
+ return [];
+ }
+ const exchange = await tx.get(Stores.exchanges, r.exchangeBaseUrl);
+ if (!exchange) {
+ logger.error(`exchange ${r.exchangeBaseUrl} not found (DB corrupted?)`);
+ return [];
+ }
+ const plainPaytoUris =
+ exchange.wireInfo?.accounts.map((x) => x.payto_uri) ?? [];
+ if (!plainPaytoUris) {
+ logger.error(`exchange ${r.exchangeBaseUrl} has no wire info`);
+ return [];
+ }
+ return plainPaytoUris.map((x) =>
+ addPaytoQueryParams(x, {
+ amount: Amounts.stringify(r.instructedAmount),
+ message: `Taler Withdrawal ${r.reservePub}`,
+ }),
+ );
+}
diff --git a/packages/taler-wallet-core/src/operations/state.d.ts.map b/packages/taler-wallet-core/src/operations/state.d.ts.map
new file mode 100644
index 000000000..275197839
--- /dev/null
+++ b/packages/taler-wallet-core/src/operations/state.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"state.d.ts","sourceRoot":"","sources":["state.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AACvE,OAAO,EAAE,SAAS,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAC7E,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAEtE,OAAO,EAAE,yBAAyB,EAAE,MAAM,kBAAkB,CAAC;AAC7D,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAC5D,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAEzC,aAAK,oBAAoB,GAAG,CAAC,CAAC,EAAE,kBAAkB,KAAK,IAAI,CAAC;AAI5D,qBAAa,mBAAmB;IAerB,EAAE,EAAE,QAAQ;IACZ,IAAI,EAAE,kBAAkB;IAfjC,aAAa,EAAE;QAAE,CAAC,cAAc,EAAE,MAAM,GAAG,aAAa,CAAA;KAAE,CAAM;IAChE,kBAAkB,EAAE,cAAc,CAAC,IAAI,CAAC,CAAwB;IAChE,gBAAgB,EAAE,cAAc,CAAC,IAAI,CAAC,CAAwB;IAC9D,cAAc,EAAE,iBAAiB,CAC/B,yBAAyB,CAC1B,CAA2B;IAC5B,cAAc,EAAE,iBAAiB,CAAC,gBAAgB,CAAC,CAA2B;IAC9E,kBAAkB,EAAE,cAAc,CAAC,IAAI,CAAC,CAAwB;IAChE,iBAAiB,EAAE,cAAc,CAAC,IAAI,CAAC,CAAwB;IAC/D,SAAS,EAAE,SAAS,CAAC;IAErB,SAAS,EAAE,oBAAoB,EAAE,CAAM;gBAG9B,EAAE,EAAE,QAAQ,EACZ,IAAI,EAAE,kBAAkB,EAC/B,mBAAmB,EAAE,mBAAmB;IAKnC,MAAM,CAAC,CAAC,EAAE,kBAAkB,GAAG,IAAI;IAU1C,uBAAuB,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,kBAAkB,KAAK,IAAI,GAAG,IAAI;CAGlE"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/operations/state.ts b/packages/taler-wallet-core/src/operations/state.ts
new file mode 100644
index 000000000..cfec85d0f
--- /dev/null
+++ b/packages/taler-wallet-core/src/operations/state.ts
@@ -0,0 +1,65 @@
+/*
+ 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 { HttpRequestLibrary } from "../util/http";
+import { NextUrlResult, BalancesResponse } from "../types/walletTypes";
+import { CryptoApi, CryptoWorkerFactory } from "../crypto/workers/cryptoApi";
+import { AsyncOpMemoMap, AsyncOpMemoSingle } from "../util/asyncMemo";
+import { Logger } from "../util/logging";
+import { PendingOperationsResponse } from "../types/pending";
+import { WalletNotification } from "../types/notifications";
+import { Database } from "../util/query";
+
+type NotificationListener = (n: WalletNotification) => void;
+
+const logger = new Logger("state.ts");
+
+export class InternalWalletState {
+ cachedNextUrl: { [fulfillmentUrl: string]: NextUrlResult } = {};
+ memoProcessReserve: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
+ memoMakePlanchet: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
+ memoGetPending: AsyncOpMemoSingle<
+ PendingOperationsResponse
+ > = new AsyncOpMemoSingle();
+ memoGetBalance: AsyncOpMemoSingle<BalancesResponse> = new AsyncOpMemoSingle();
+ memoProcessRefresh: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
+ memoProcessRecoup: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
+ cryptoApi: CryptoApi;
+
+ listeners: NotificationListener[] = [];
+
+ constructor(
+ public db: Database,
+ public http: HttpRequestLibrary,
+ cryptoWorkerFactory: CryptoWorkerFactory,
+ ) {
+ this.cryptoApi = new CryptoApi(cryptoWorkerFactory);
+ }
+
+ public notify(n: WalletNotification): void {
+ logger.trace("Notification", n);
+ for (const l of this.listeners) {
+ const nc = JSON.parse(JSON.stringify(n));
+ setTimeout(() => {
+ l(nc);
+ }, 0);
+ }
+ }
+
+ addNotificationListener(f: (n: WalletNotification) => void): void {
+ this.listeners.push(f);
+ }
+}
diff --git a/packages/taler-wallet-core/src/operations/testing.d.ts.map b/packages/taler-wallet-core/src/operations/testing.d.ts.map
new file mode 100644
index 000000000..d7b3ceaec
--- /dev/null
+++ b/packages/taler-wallet-core/src/operations/testing.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["testing.ts"],"names":[],"mappings":"AAwBA,OAAO,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAuC9C,wBAAsB,mBAAmB,CACvC,EAAE,EAAE,mBAAmB,EACvB,MAAM,SAAiB,EACvB,WAAW,SAAiC,EAC5C,eAAe,SAAqC,GACnD,OAAO,CAAC,IAAI,CAAC,CAuBf"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/operations/testing.ts b/packages/taler-wallet-core/src/operations/testing.ts
new file mode 100644
index 000000000..71cee1f3a
--- /dev/null
+++ b/packages/taler-wallet-core/src/operations/testing.ts
@@ -0,0 +1,156 @@
+/*
+ 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 { Logger } from "../util/logging";
+import {
+ HttpRequestLibrary,
+ readSuccessResponseJsonOrThrow,
+ checkSuccessResponseOrThrow,
+} from "../util/http";
+import { codecForAny } from "../util/codec";
+import { AmountString } from "../types/talerTypes";
+import { InternalWalletState } from "./state";
+import { createTalerWithdrawReserve } from "./reserves";
+import { URL } from "../util/url";
+
+const logger = new Logger("operations/testing.ts");
+
+interface BankUser {
+ username: string;
+ password: string;
+}
+
+interface BankWithdrawalResponse {
+ taler_withdraw_uri: string;
+ withdrawal_id: string;
+}
+
+/**
+ * Generate a random alphanumeric ID. Does *not* use cryptographically
+ * secure randomness.
+ */
+function makeId(length: number): string {
+ let result = "";
+ const characters =
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+ for (let i = 0; i < length; i++) {
+ result += characters.charAt(Math.floor(Math.random() * characters.length));
+ }
+ return result;
+}
+
+/**
+ * Helper function to generate the "Authorization" HTTP header.
+ */
+function makeAuth(username: string, password: string): string {
+ const auth = `${username}:${password}`;
+ const authEncoded: string = Buffer.from(auth).toString("base64");
+ return `Basic ${authEncoded}`;
+}
+
+export async function withdrawTestBalance(
+ ws: InternalWalletState,
+ amount = "TESTKUDOS:10",
+ bankBaseUrl = "https://bank.test.taler.net/",
+ exchangeBaseUrl = "https://exchange.test.taler.net/",
+): Promise<void> {
+ const bankUser = await registerRandomBankUser(ws.http, bankBaseUrl);
+ logger.trace(`Registered bank user ${JSON.stringify(bankUser)}`);
+
+ const wresp = await createBankWithdrawalUri(
+ ws.http,
+ bankBaseUrl,
+ bankUser,
+ amount,
+ );
+
+ await createTalerWithdrawReserve(
+ ws,
+ wresp.taler_withdraw_uri,
+ exchangeBaseUrl,
+ );
+
+ await confirmBankWithdrawalUri(
+ ws.http,
+ bankBaseUrl,
+ bankUser,
+ wresp.withdrawal_id,
+ );
+}
+
+async function createBankWithdrawalUri(
+ http: HttpRequestLibrary,
+ bankBaseUrl: string,
+ bankUser: BankUser,
+ amount: AmountString,
+): Promise<BankWithdrawalResponse> {
+ const reqUrl = new URL(
+ `accounts/${bankUser.username}/withdrawals`,
+ bankBaseUrl,
+ ).href;
+ const resp = await http.postJson(
+ reqUrl,
+ {
+ amount,
+ },
+ {
+ headers: {
+ Authorization: makeAuth(bankUser.username, bankUser.password),
+ },
+ },
+ );
+ const respJson = await readSuccessResponseJsonOrThrow(resp, codecForAny);
+ return respJson;
+}
+
+async function confirmBankWithdrawalUri(
+ http: HttpRequestLibrary,
+ bankBaseUrl: string,
+ bankUser: BankUser,
+ withdrawalId: string,
+): Promise<void> {
+ const reqUrl = new URL(
+ `accounts/${bankUser.username}/withdrawals/${withdrawalId}/confirm`,
+ bankBaseUrl,
+ ).href;
+ const resp = await http.postJson(
+ reqUrl,
+ {},
+ {
+ headers: {
+ Authorization: makeAuth(bankUser.username, bankUser.password),
+ },
+ },
+ );
+ await readSuccessResponseJsonOrThrow(resp, codecForAny);
+ return;
+}
+
+async function registerRandomBankUser(
+ http: HttpRequestLibrary,
+ bankBaseUrl: string,
+): Promise<BankUser> {
+ const reqUrl = new URL("testing/register", bankBaseUrl).href;
+ const randId = makeId(8);
+ const bankUser: BankUser = {
+ username: `testuser-${randId}`,
+ password: `testpw-${randId}`,
+ };
+
+ const resp = await http.postJson(reqUrl, bankUser);
+ await checkSuccessResponseOrThrow(resp);
+ return bankUser;
+}
diff --git a/packages/taler-wallet-core/src/operations/tip.d.ts.map b/packages/taler-wallet-core/src/operations/tip.d.ts.map
new file mode 100644
index 000000000..8d8a72fb8
--- /dev/null
+++ b/packages/taler-wallet-core/src/operations/tip.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"tip.d.ts","sourceRoot":"","sources":["tip.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAE9C,OAAO,EAAE,SAAS,EAAyB,MAAM,sBAAsB,CAAC;AA8BxE,wBAAsB,YAAY,CAChC,EAAE,EAAE,mBAAmB,EACvB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,SAAS,CAAC,CAgFpB;AAuBD,wBAAsB,UAAU,CAC9B,EAAE,EAAE,mBAAmB,EACvB,KAAK,EAAE,MAAM,EACb,QAAQ,UAAQ,GACf,OAAO,CAAC,IAAI,CAAC,CAOf;AAkKD,wBAAsB,SAAS,CAC7B,EAAE,EAAE,mBAAmB,EACvB,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,IAAI,CAAC,CAYf"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/operations/tip.ts b/packages/taler-wallet-core/src/operations/tip.ts
new file mode 100644
index 000000000..d6768bdb6
--- /dev/null
+++ b/packages/taler-wallet-core/src/operations/tip.ts
@@ -0,0 +1,343 @@
+/*
+ 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/>
+ */
+
+import { InternalWalletState } from "./state";
+import { parseTipUri } from "../util/taleruri";
+import { TipStatus, OperationErrorDetails } from "../types/walletTypes";
+import {
+ TipPlanchetDetail,
+ codecForTipPickupGetResponse,
+ codecForTipResponse,
+} from "../types/talerTypes";
+import * as Amounts from "../util/amounts";
+import {
+ Stores,
+ PlanchetRecord,
+ WithdrawalGroupRecord,
+ initRetryInfo,
+ updateRetryInfoTimeout,
+ WithdrawalSourceType,
+ TipPlanchet,
+} from "../types/dbTypes";
+import {
+ getExchangeWithdrawalInfo,
+ selectWithdrawalDenoms,
+ processWithdrawGroup,
+ denomSelectionInfoToState,
+} from "./withdraw";
+import { updateExchangeFromUrl } from "./exchanges";
+import { getRandomBytes, encodeCrock } from "../crypto/talerCrypto";
+import { guardOperationException } from "./errors";
+import { NotificationType } from "../types/notifications";
+import { getTimestampNow } from "../util/time";
+import { readSuccessResponseJsonOrThrow } from "../util/http";
+import { URL } from "../util/url";
+
+export async function getTipStatus(
+ ws: InternalWalletState,
+ talerTipUri: string,
+): Promise<TipStatus> {
+ const res = parseTipUri(talerTipUri);
+ if (!res) {
+ throw Error("invalid taler://tip URI");
+ }
+
+ const tipStatusUrl = new URL("tip-pickup", res.merchantBaseUrl);
+ tipStatusUrl.searchParams.set("tip_id", res.merchantTipId);
+ console.log("checking tip status from", tipStatusUrl.href);
+ const merchantResp = await ws.http.get(tipStatusUrl.href);
+ const tipPickupStatus = await readSuccessResponseJsonOrThrow(
+ merchantResp,
+ codecForTipPickupGetResponse(),
+ );
+ console.log("status", tipPickupStatus);
+
+ const amount = Amounts.parseOrThrow(tipPickupStatus.amount);
+
+ const merchantOrigin = new URL(res.merchantBaseUrl).origin;
+
+ let tipRecord = await ws.db.get(Stores.tips, [
+ res.merchantTipId,
+ merchantOrigin,
+ ]);
+
+ if (!tipRecord) {
+ await updateExchangeFromUrl(ws, tipPickupStatus.exchange_url);
+ const withdrawDetails = await getExchangeWithdrawalInfo(
+ ws,
+ tipPickupStatus.exchange_url,
+ amount,
+ );
+
+ const tipId = encodeCrock(getRandomBytes(32));
+ const selectedDenoms = await selectWithdrawalDenoms(
+ ws,
+ tipPickupStatus.exchange_url,
+ amount,
+ );
+
+ tipRecord = {
+ tipId,
+ acceptedTimestamp: undefined,
+ rejectedTimestamp: undefined,
+ amount,
+ deadline: tipPickupStatus.stamp_expire,
+ exchangeUrl: tipPickupStatus.exchange_url,
+ merchantBaseUrl: res.merchantBaseUrl,
+ nextUrl: undefined,
+ pickedUp: false,
+ planchets: undefined,
+ response: undefined,
+ createdTimestamp: getTimestampNow(),
+ merchantTipId: res.merchantTipId,
+ totalFees: Amounts.add(
+ withdrawDetails.overhead,
+ withdrawDetails.withdrawFee,
+ ).amount,
+ retryInfo: initRetryInfo(),
+ lastError: undefined,
+ denomsSel: denomSelectionInfoToState(selectedDenoms),
+ };
+ await ws.db.put(Stores.tips, tipRecord);
+ }
+
+ const tipStatus: TipStatus = {
+ accepted: !!tipRecord && !!tipRecord.acceptedTimestamp,
+ amount: Amounts.parseOrThrow(tipPickupStatus.amount),
+ amountLeft: Amounts.parseOrThrow(tipPickupStatus.amount_left),
+ exchangeUrl: tipPickupStatus.exchange_url,
+ nextUrl: tipPickupStatus.extra.next_url,
+ merchantOrigin: merchantOrigin,
+ merchantTipId: res.merchantTipId,
+ expirationTimestamp: tipPickupStatus.stamp_expire,
+ timestamp: tipPickupStatus.stamp_created,
+ totalFees: tipRecord.totalFees,
+ tipId: tipRecord.tipId,
+ };
+
+ return tipStatus;
+}
+
+async function incrementTipRetry(
+ ws: InternalWalletState,
+ refreshSessionId: string,
+ err: OperationErrorDetails | undefined,
+): Promise<void> {
+ await ws.db.runWithWriteTransaction([Stores.tips], async (tx) => {
+ const t = await tx.get(Stores.tips, refreshSessionId);
+ if (!t) {
+ return;
+ }
+ if (!t.retryInfo) {
+ return;
+ }
+ t.retryInfo.retryCounter++;
+ updateRetryInfoTimeout(t.retryInfo);
+ t.lastError = err;
+ await tx.put(Stores.tips, t);
+ });
+ ws.notify({ type: NotificationType.TipOperationError });
+}
+
+export async function processTip(
+ ws: InternalWalletState,
+ tipId: string,
+ forceNow = false,
+): Promise<void> {
+ const onOpErr = (e: OperationErrorDetails): Promise<void> =>
+ incrementTipRetry(ws, tipId, e);
+ await guardOperationException(
+ () => processTipImpl(ws, tipId, forceNow),
+ onOpErr,
+ );
+}
+
+async function resetTipRetry(
+ ws: InternalWalletState,
+ tipId: string,
+): Promise<void> {
+ await ws.db.mutate(Stores.tips, tipId, (x) => {
+ if (x.retryInfo.active) {
+ x.retryInfo = initRetryInfo();
+ }
+ return x;
+ });
+}
+
+async function processTipImpl(
+ ws: InternalWalletState,
+ tipId: string,
+ forceNow: boolean,
+): Promise<void> {
+ if (forceNow) {
+ await resetTipRetry(ws, tipId);
+ }
+ let tipRecord = await ws.db.get(Stores.tips, tipId);
+ if (!tipRecord) {
+ return;
+ }
+
+ if (tipRecord.pickedUp) {
+ console.log("tip already picked up");
+ return;
+ }
+
+ const denomsForWithdraw = tipRecord.denomsSel;
+
+ if (!tipRecord.planchets) {
+ const planchets: TipPlanchet[] = [];
+
+ for (const sd of denomsForWithdraw.selectedDenoms) {
+ const denom = await ws.db.getIndexed(
+ Stores.denominations.denomPubHashIndex,
+ sd.denomPubHash,
+ );
+ if (!denom) {
+ throw Error("denom does not exist anymore");
+ }
+ for (let i = 0; i < sd.count; i++) {
+ const r = await ws.cryptoApi.createTipPlanchet(denom);
+ planchets.push(r);
+ }
+ }
+ await ws.db.mutate(Stores.tips, tipId, (r) => {
+ if (!r.planchets) {
+ r.planchets = planchets;
+ }
+ return r;
+ });
+ }
+
+ tipRecord = await ws.db.get(Stores.tips, tipId);
+ if (!tipRecord) {
+ throw Error("tip not in database");
+ }
+
+ if (!tipRecord.planchets) {
+ throw Error("invariant violated");
+ }
+
+ console.log("got planchets for tip!");
+
+ // Planchets in the form that the merchant expects
+ const planchetsDetail: TipPlanchetDetail[] = tipRecord.planchets.map((p) => ({
+ coin_ev: p.coinEv,
+ denom_pub_hash: p.denomPubHash,
+ }));
+
+ let merchantResp;
+
+ const tipStatusUrl = new URL("tip-pickup", tipRecord.merchantBaseUrl);
+
+ try {
+ const req = { planchets: planchetsDetail, tip_id: tipRecord.merchantTipId };
+ merchantResp = await ws.http.postJson(tipStatusUrl.href, req);
+ if (merchantResp.status !== 200) {
+ throw Error(`unexpected status ${merchantResp.status} for tip-pickup`);
+ }
+ console.log("got merchant resp:", merchantResp);
+ } catch (e) {
+ console.log("tipping failed", e);
+ throw e;
+ }
+
+ const response = codecForTipResponse().decode(await merchantResp.json());
+
+ if (response.reserve_sigs.length !== tipRecord.planchets.length) {
+ throw Error("number of tip responses does not match requested planchets");
+ }
+
+ const withdrawalGroupId = encodeCrock(getRandomBytes(32));
+ const planchets: PlanchetRecord[] = [];
+
+ for (let i = 0; i < tipRecord.planchets.length; i++) {
+ const tipPlanchet = tipRecord.planchets[i];
+ const coinEvHash = await ws.cryptoApi.hashEncoded(tipPlanchet.coinEv);
+ const planchet: PlanchetRecord = {
+ blindingKey: tipPlanchet.blindingKey,
+ coinEv: tipPlanchet.coinEv,
+ coinPriv: tipPlanchet.coinPriv,
+ coinPub: tipPlanchet.coinPub,
+ coinValue: tipPlanchet.coinValue,
+ denomPub: tipPlanchet.denomPub,
+ denomPubHash: tipPlanchet.denomPubHash,
+ reservePub: response.reserve_pub,
+ withdrawSig: response.reserve_sigs[i].reserve_sig,
+ isFromTip: true,
+ coinEvHash,
+ coinIdx: i,
+ withdrawalDone: false,
+ withdrawalGroupId: withdrawalGroupId,
+ };
+ planchets.push(planchet);
+ }
+
+ const withdrawalGroup: WithdrawalGroupRecord = {
+ exchangeBaseUrl: tipRecord.exchangeUrl,
+ source: {
+ type: WithdrawalSourceType.Tip,
+ tipId: tipRecord.tipId,
+ },
+ timestampStart: getTimestampNow(),
+ withdrawalGroupId: withdrawalGroupId,
+ rawWithdrawalAmount: tipRecord.amount,
+ lastErrorPerCoin: {},
+ retryInfo: initRetryInfo(),
+ timestampFinish: undefined,
+ lastError: undefined,
+ denomsSel: tipRecord.denomsSel,
+ };
+
+ await ws.db.runWithWriteTransaction(
+ [Stores.tips, Stores.withdrawalGroups],
+ async (tx) => {
+ const tr = await tx.get(Stores.tips, tipId);
+ if (!tr) {
+ return;
+ }
+ if (tr.pickedUp) {
+ return;
+ }
+ tr.pickedUp = true;
+ tr.retryInfo = initRetryInfo(false);
+
+ await tx.put(Stores.tips, tr);
+ await tx.put(Stores.withdrawalGroups, withdrawalGroup);
+ for (const p of planchets) {
+ await tx.put(Stores.planchets, p);
+ }
+ },
+ );
+
+ await processWithdrawGroup(ws, withdrawalGroupId);
+}
+
+export async function acceptTip(
+ ws: InternalWalletState,
+ tipId: string,
+): Promise<void> {
+ const tipRecord = await ws.db.get(Stores.tips, tipId);
+ if (!tipRecord) {
+ console.log("tip not found");
+ return;
+ }
+
+ tipRecord.acceptedTimestamp = getTimestampNow();
+ await ws.db.put(Stores.tips, tipRecord);
+
+ await processTip(ws, tipId);
+ return;
+}
diff --git a/packages/taler-wallet-core/src/operations/transactions.d.ts.map b/packages/taler-wallet-core/src/operations/transactions.d.ts.map
new file mode 100644
index 000000000..5a462e4d6
--- /dev/null
+++ b/packages/taler-wallet-core/src/operations/transactions.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"transactions.d.ts","sourceRoot":"","sources":["transactions.ts"],"names":[],"mappings":"AAgBA;;GAEG;AACH,OAAO,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAO9C,OAAO,EACL,mBAAmB,EACnB,oBAAoB,EAMrB,MAAM,uBAAuB,CAAC;AAoC/B;;GAEG;AACH,wBAAsB,eAAe,CACnC,EAAE,EAAE,mBAAmB,EACvB,mBAAmB,CAAC,EAAE,mBAAmB,GACxC,OAAO,CAAC,oBAAoB,CAAC,CAuN/B"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/operations/transactions.ts b/packages/taler-wallet-core/src/operations/transactions.ts
new file mode 100644
index 000000000..2d66b5e9d
--- /dev/null
+++ b/packages/taler-wallet-core/src/operations/transactions.ts
@@ -0,0 +1,288 @@
+/*
+ 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/>
+ */
+
+/**
+ * Imports.
+ */
+import { InternalWalletState } from "./state";
+import { Stores, WithdrawalSourceType } from "../types/dbTypes";
+import { Amounts, AmountJson } from "../util/amounts";
+import { timestampCmp } from "../util/time";
+import {
+ TransactionsRequest,
+ TransactionsResponse,
+ Transaction,
+ TransactionType,
+ PaymentStatus,
+ WithdrawalType,
+ WithdrawalDetails,
+} from "../types/transactions";
+import { getFundingPaytoUris } from "./reserves";
+
+/**
+ * Create an event ID from the type and the primary key for the event.
+ */
+function makeEventId(type: TransactionType, ...args: string[]): string {
+ return type + ";" + args.map((x) => encodeURIComponent(x)).join(";");
+}
+
+function shouldSkipCurrency(
+ transactionsRequest: TransactionsRequest | undefined,
+ currency: string,
+): boolean {
+ if (!transactionsRequest?.currency) {
+ return false;
+ }
+ return transactionsRequest.currency.toLowerCase() !== currency.toLowerCase();
+}
+
+function shouldSkipSearch(
+ transactionsRequest: TransactionsRequest | undefined,
+ fields: string[],
+): boolean {
+ if (!transactionsRequest?.search) {
+ return false;
+ }
+ const needle = transactionsRequest.search.trim();
+ for (const f of fields) {
+ if (f.indexOf(needle) >= 0) {
+ return false;
+ }
+ }
+ return true;
+}
+
+/**
+ * Retrive the full event history for this wallet.
+ */
+export async function getTransactions(
+ ws: InternalWalletState,
+ transactionsRequest?: TransactionsRequest,
+): Promise<TransactionsResponse> {
+ const transactions: Transaction[] = [];
+
+ await ws.db.runWithReadTransaction(
+ [
+ Stores.currencies,
+ Stores.coins,
+ Stores.denominations,
+ Stores.exchanges,
+ Stores.proposals,
+ Stores.purchases,
+ Stores.refreshGroups,
+ Stores.reserves,
+ Stores.reserveHistory,
+ Stores.tips,
+ Stores.withdrawalGroups,
+ Stores.payEvents,
+ Stores.planchets,
+ Stores.refundEvents,
+ Stores.reserveUpdatedEvents,
+ Stores.recoupGroups,
+ ],
+ // Report withdrawals that are currently in progress.
+ async (tx) => {
+ tx.iter(Stores.withdrawalGroups).forEachAsync(async (wsr) => {
+ if (
+ shouldSkipCurrency(
+ transactionsRequest,
+ wsr.rawWithdrawalAmount.currency,
+ )
+ ) {
+ return;
+ }
+
+ if (shouldSkipSearch(transactionsRequest, [])) {
+ return;
+ }
+
+ switch (wsr.source.type) {
+ case WithdrawalSourceType.Reserve:
+ {
+ const r = await tx.get(Stores.reserves, wsr.source.reservePub);
+ if (!r) {
+ break;
+ }
+ let amountRaw: AmountJson | undefined = undefined;
+ if (wsr.withdrawalGroupId === r.initialWithdrawalGroupId) {
+ amountRaw = r.instructedAmount;
+ } else {
+ amountRaw = wsr.denomsSel.totalWithdrawCost;
+ }
+ let withdrawalDetails: WithdrawalDetails;
+ if (r.bankInfo) {
+ withdrawalDetails = {
+ type: WithdrawalType.TalerBankIntegrationApi,
+ confirmed: true,
+ bankConfirmationUrl: r.bankInfo.confirmUrl,
+ };
+ } else {
+ const exchange = await tx.get(
+ Stores.exchanges,
+ r.exchangeBaseUrl,
+ );
+ if (!exchange) {
+ // FIXME: report somehow
+ break;
+ }
+ withdrawalDetails = {
+ type: WithdrawalType.ManualTransfer,
+ exchangePaytoUris:
+ exchange.wireInfo?.accounts.map((x) => x.payto_uri) ?? [],
+ };
+ }
+ transactions.push({
+ type: TransactionType.Withdrawal,
+ amountEffective: Amounts.stringify(
+ wsr.denomsSel.totalCoinValue,
+ ),
+ amountRaw: Amounts.stringify(amountRaw),
+ withdrawalDetails,
+ exchangeBaseUrl: wsr.exchangeBaseUrl,
+ pending: !wsr.timestampFinish,
+ timestamp: wsr.timestampStart,
+ transactionId: makeEventId(
+ TransactionType.Withdrawal,
+ wsr.withdrawalGroupId,
+ ),
+ });
+ }
+ break;
+ default:
+ // Tips are reported via their own event
+ break;
+ }
+ });
+
+ // Report pending withdrawals based on reserves that
+ // were created, but where the actual withdrawal group has
+ // not started yet.
+ tx.iter(Stores.reserves).forEachAsync(async (r) => {
+ if (shouldSkipCurrency(transactionsRequest, r.currency)) {
+ return;
+ }
+ if (shouldSkipSearch(transactionsRequest, [])) {
+ return;
+ }
+ if (r.initialWithdrawalStarted) {
+ return;
+ }
+ let withdrawalDetails: WithdrawalDetails;
+ if (r.bankInfo) {
+ withdrawalDetails = {
+ type: WithdrawalType.TalerBankIntegrationApi,
+ confirmed: false,
+ bankConfirmationUrl: r.bankInfo.confirmUrl,
+ };
+ } else {
+ withdrawalDetails = {
+ type: WithdrawalType.ManualTransfer,
+ exchangePaytoUris: await getFundingPaytoUris(tx, r.reservePub),
+ };
+ }
+ transactions.push({
+ type: TransactionType.Withdrawal,
+ amountRaw: Amounts.stringify(r.instructedAmount),
+ amountEffective: Amounts.stringify(r.initialDenomSel.totalCoinValue),
+ exchangeBaseUrl: r.exchangeBaseUrl,
+ pending: true,
+ timestamp: r.timestampCreated,
+ withdrawalDetails: withdrawalDetails,
+ transactionId: makeEventId(
+ TransactionType.Withdrawal,
+ r.initialWithdrawalGroupId,
+ ),
+ });
+ });
+
+ tx.iter(Stores.purchases).forEachAsync(async (pr) => {
+ if (
+ shouldSkipCurrency(
+ transactionsRequest,
+ pr.contractData.amount.currency,
+ )
+ ) {
+ return;
+ }
+ if (shouldSkipSearch(transactionsRequest, [pr.contractData.summary])) {
+ return;
+ }
+ const proposal = await tx.get(Stores.proposals, pr.proposalId);
+ if (!proposal) {
+ return;
+ }
+ transactions.push({
+ type: TransactionType.Payment,
+ amountRaw: Amounts.stringify(pr.contractData.amount),
+ amountEffective: Amounts.stringify(pr.payCostInfo.totalCost),
+ status: pr.timestampFirstSuccessfulPay
+ ? PaymentStatus.Paid
+ : PaymentStatus.Accepted,
+ pending: !pr.timestampFirstSuccessfulPay,
+ timestamp: pr.timestampAccept,
+ transactionId: makeEventId(TransactionType.Payment, pr.proposalId),
+ info: {
+ fulfillmentUrl: pr.contractData.fulfillmentUrl,
+ merchant: pr.contractData.merchant,
+ orderId: pr.contractData.orderId,
+ products: pr.contractData.products,
+ summary: pr.contractData.summary,
+ summary_i18n: pr.contractData.summaryI18n,
+ },
+ });
+
+ // for (const rg of pr.refundGroups) {
+ // const pending = Object.keys(pr.refundsPending).length > 0;
+ // const stats = getRefundStats(pr, rg.refundGroupId);
+
+ // transactions.push({
+ // type: TransactionType.Refund,
+ // pending,
+ // info: {
+ // fulfillmentUrl: pr.contractData.fulfillmentUrl,
+ // merchant: pr.contractData.merchant,
+ // orderId: pr.contractData.orderId,
+ // products: pr.contractData.products,
+ // summary: pr.contractData.summary,
+ // summary_i18n: pr.contractData.summaryI18n,
+ // },
+ // timestamp: rg.timestampQueried,
+ // transactionId: makeEventId(
+ // TransactionType.Refund,
+ // pr.proposalId,
+ // `${rg.timestampQueried.t_ms}`,
+ // ),
+ // refundedTransactionId: makeEventId(
+ // TransactionType.Payment,
+ // pr.proposalId,
+ // ),
+ // amountEffective: Amounts.stringify(stats.amountEffective),
+ // amountInvalid: Amounts.stringify(stats.amountInvalid),
+ // amountRaw: Amounts.stringify(stats.amountRaw),
+ // });
+ // }
+ });
+ },
+ );
+
+ const txPending = transactions.filter((x) => x.pending);
+ const txNotPending = transactions.filter((x) => !x.pending);
+
+ txPending.sort((h1, h2) => timestampCmp(h1.timestamp, h2.timestamp));
+ txNotPending.sort((h1, h2) => timestampCmp(h1.timestamp, h2.timestamp));
+
+ return { transactions: [...txPending, ...txNotPending] };
+}
diff --git a/packages/taler-wallet-core/src/operations/versions.d.ts.map b/packages/taler-wallet-core/src/operations/versions.d.ts.map
new file mode 100644
index 000000000..15ba8d27e
--- /dev/null
+++ b/packages/taler-wallet-core/src/operations/versions.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"versions.d.ts","sourceRoot":"","sources":["versions.ts"],"names":[],"mappings":"AAgBA;;;;GAIG;AACH,eAAO,MAAM,gCAAgC,UAAU,CAAC;AAExD;;;;GAIG;AACH,eAAO,MAAM,gCAAgC,UAAU,CAAC;AAExD;;;;;;GAMG;AACH,eAAO,MAAM,mCAAmC,MAAM,CAAC"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/operations/versions.ts b/packages/taler-wallet-core/src/operations/versions.ts
new file mode 100644
index 000000000..31c4921c6
--- /dev/null
+++ b/packages/taler-wallet-core/src/operations/versions.ts
@@ -0,0 +1,38 @@
+/*
+ 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/>
+ */
+
+/**
+ * Protocol version spoken with the exchange.
+ *
+ * Uses libtool's current:revision:age versioning.
+ */
+export const WALLET_EXCHANGE_PROTOCOL_VERSION = "8:0:0";
+
+/**
+ * Protocol version spoken with the merchant.
+ *
+ * Uses libtool's current:revision:age versioning.
+ */
+export const WALLET_MERCHANT_PROTOCOL_VERSION = "1:0:0";
+
+/**
+ * Cache breaker that is appended to queries such as /keys and /wire
+ * to break through caching, if it has been accidentally/badly configured
+ * by the exchange.
+ *
+ * This is only a temporary measure.
+ */
+export const WALLET_CACHE_BREAKER_CLIENT_VERSION = "3";
diff --git a/packages/taler-wallet-core/src/operations/withdraw-test.ts b/packages/taler-wallet-core/src/operations/withdraw-test.ts
new file mode 100644
index 000000000..24cb6f4b1
--- /dev/null
+++ b/packages/taler-wallet-core/src/operations/withdraw-test.ts
@@ -0,0 +1,332 @@
+/*
+ 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 { getWithdrawDenomList } from "./withdraw";
+import { Amounts } from "../util/amounts";
+
+test("withdrawal selection bug repro", (t) => {
+ const amount = {
+ currency: "KUDOS",
+ fraction: 43000000,
+ value: 23,
+ };
+
+ const denoms = [
+ {
+ denomPub:
+ "040000XT67C8KBD6B75TTQ3SK8FWXMNQW4372T3BDDGPAMB9RFCA03638W8T3F71WFEFK9NP32VKYVNFXPYRWQ1N1HDKV5J0DFEKHBPJCYSWCBJDRNWD7G8BN8PT97FA9AMV75MYEK4X54D1HGJ207JSVJBGFCATSPNTEYNHEQF1F220W00TBZR1HNPDQFD56FG0DJQ9KGHM8EC33H6AY9YN9CNX5R3Z4TZ4Q23W47SBHB13H6W74FQJG1F50X38VRSC4SR8RWBAFB7S4K8D2H4NMRFSQT892A3T0BTBW7HM5C0H2CK6FRKG31F7W9WP1S29013K5CXYE55CT8TH6N8J9B780R42Y5S3ZB6J6E9H76XBPSGH4TGYSR2VZRB98J417KCQMZKX1BB67E7W5KVE37TC9SJ904002",
+ denomPubHash:
+ "Q21FQSSG4FXNT96Z14CHXM8N1RZAG9GPHAV8PRWS0PZAAVWH7PBW6R97M2CH19KKP65NNSWXY7B6S53PT3CBM342E357ZXDDJ8RDVW8",
+ exchangeBaseUrl: "https://exchange.demo.taler.net/",
+ feeDeposit: {
+ currency: "KUDOS",
+ fraction: 1000000,
+ value: 0,
+ },
+ feeRefresh: {
+ currency: "KUDOS",
+ fraction: 1000000,
+ value: 0,
+ },
+ feeRefund: {
+ currency: "KUDOS",
+ fraction: 1000000,
+ value: 0,
+ },
+ feeWithdraw: {
+ currency: "KUDOS",
+ fraction: 1000000,
+ value: 0,
+ },
+ isOffered: true,
+ isRevoked: false,
+ masterSig:
+ "4F0P456CNNTTWK8BFJHGM3JTD6FVVNZY8EP077GYAHDJ5Y81S5RQ3SMS925NXMDVG9A88JAAP0E2GDZBC21PP5NHFFVWHAW3AVT8J3R",
+ stampExpireDeposit: {
+ t_ms: 1742909388000,
+ },
+ stampExpireLegal: {
+ t_ms: 1900589388000,
+ },
+ stampExpireWithdraw: {
+ t_ms: 1679837388000,
+ },
+ stampStart: {
+ t_ms: 1585229388000,
+ },
+ status: 0,
+ value: {
+ currency: "KUDOS",
+ fraction: 0,
+ value: 1000,
+ },
+ },
+ {
+ denomPub:
+ "040000Y63CF78QFPKRY77BRK9P557Q1GQWX3NCZ3HSYSK0Z7TT0KGRA7N4SKBKEHSTVHX1Z9DNXMJR4EXSY1TXCKV0GJ3T3YYC6Z0JNMJFVYQAV4FX5J90NZH1N33MZTV8HS9SMNAA9S6K73G4P99GYBB01B0P6M1KXZ5JRDR7VWBR3MEJHHGJ6QBMCJR3NWJRE3WJW9PRY8QPQ2S7KFWTWRESH2DBXCXWBD2SRN6P9YX8GRAEMFEGXC9V5GVJTEMH6ZDGNXFPWZE3JVJ2Q4N9GDYKBCHZCJ7M7M2RJ9ZV4Y64NAN9BT6XDC68215GKKRHTW1BBF1MYY6AR3JCTT9HYAM923RMVQR3TAEB7SDX8J76XRZWYH3AGJCZAQGMN5C8SSH9AHQ9RNQJQ15CN45R37X4YNFJV904002",
+ denomPubHash:
+ "447WA23SCBATMABHA0793F92MYTBYVPYMMQHCPKMKVY5P7RZRFMQ6VRW0Y8HRA7177GTBT0TBT08R21DZD129AJ995H9G09XBFE55G8",
+ exchangeBaseUrl: "https://exchange.demo.taler.net/",
+ feeDeposit: {
+ currency: "KUDOS",
+ fraction: 1000000,
+ value: 0,
+ },
+ feeRefresh: {
+ currency: "KUDOS",
+ fraction: 1000000,
+ value: 0,
+ },
+ feeRefund: {
+ currency: "KUDOS",
+ fraction: 1000000,
+ value: 0,
+ },
+ feeWithdraw: {
+ currency: "KUDOS",
+ fraction: 1000000,
+ value: 0,
+ },
+ isOffered: true,
+ isRevoked: false,
+ masterSig:
+ "P99AW82W46MZ0AKW7Z58VQPXFNTJQM9DVTYPBDF6KVYF38PPVDAZTV7JQ8TY7HGEC7JJJAY4E7AY7J3W1WV10DAZZQHHKTAVTSRAC20",
+ stampExpireDeposit: {
+ t_ms: 1742909388000,
+ },
+ stampExpireLegal: {
+ t_ms: 1900589388000,
+ },
+ stampExpireWithdraw: {
+ t_ms: 1679837388000,
+ },
+ stampStart: {
+ t_ms: 1585229388000,
+ },
+ status: 0,
+ value: {
+ currency: "KUDOS",
+ fraction: 0,
+ value: 10,
+ },
+ },
+ {
+ denomPub:
+ "040000YDESWC2B962DA4WK356SC50MA3N9KV0ZSGY3RC48JCTY258W909C7EEMT5BTC5KZ5T4CERCZ141P9QF87EK2BD1XEEM5GB07MB3H19WE4CQGAS8X84JBWN83PQGQXVMWE5HFA992KMGHC566GT9ZS2QPHZB6X89C4A80Z663PYAAPXP728VHAKATGNNBQ01ZZ2XD1CH9Y38YZBSPJ4K7GB2J76GBCYAVD9ENHDVWXJAXYRPBX4KSS5TXRR3K5NEN9ZV3AJD2V65K7ABRZDF5D5V1FJZZMNJ5XZ4FEREEKEBV9TDFPGJTKDEHEC60K3DN24DAATRESDJ1ZYYSYSRCAT4BT2B62ARGVMJTT5N2R126DRW9TGRWCW0ZAF2N2WET1H4NJEW77X0QT46Z5R3MZ0XPHD04002",
+ denomPubHash:
+ "JS61DTKAFM0BX8Q4XV3ZSKB921SM8QK745Z2AFXTKFMBHHFNBD8TQ5ETJHFNDGBGX22FFN2A2ERNYG1SGSDQWNQHQQ2B14DBVJYJG8R",
+ exchangeBaseUrl: "https://exchange.demo.taler.net/",
+ feeDeposit: {
+ currency: "KUDOS",
+ fraction: 1000000,
+ value: 0,
+ },
+ feeRefresh: {
+ currency: "KUDOS",
+ fraction: 1000000,
+ value: 0,
+ },
+ feeRefund: {
+ currency: "KUDOS",
+ fraction: 1000000,
+ value: 0,
+ },
+ feeWithdraw: {
+ currency: "KUDOS",
+ fraction: 1000000,
+ value: 0,
+ },
+ isOffered: true,
+ isRevoked: false,
+ masterSig:
+ "8S4VZGHE5WE0N5ZVCHYW9KZZR4YAKK15S46MV1HR1QB9AAMH3NWPW4DCR4NYGJK33Q8YNFY80SWNS6XKAP5DEVK933TM894FJ2VGE3G",
+ stampExpireDeposit: {
+ t_ms: 1742909388000,
+ },
+ stampExpireLegal: {
+ t_ms: 1900589388000,
+ },
+ stampExpireWithdraw: {
+ t_ms: 1679837388000,
+ },
+ stampStart: {
+ t_ms: 1585229388000,
+ },
+ status: 0,
+ value: {
+ currency: "KUDOS",
+ fraction: 0,
+ value: 5,
+ },
+ },
+ {
+ denomPub:
+ "040000YG3T1ADB8DVA6BD3EPV6ZHSHTDW35DEN4VH1AE6CSB7P1PSDTNTJG866PHF6QB1CCWYCVRGA0FVBJ9Q0G7KV7AD9010GDYBQH0NNPHW744MTNXVXWBGGGRGQGYK4DTYN1DSWQ1FZNDSZZPB5BEKG2PDJ93NX2JTN06Y8QMS2G734Z9XHC10EENBG2KVB7EJ3CM8PV1T32RC7AY62F3496E8D8KRHJQQTT67DSGMNKK86QXVDTYW677FG27DP20E8XY3M6FQD53NDJ1WWES91401MV1A3VXVPGC76GZVDD62W3WTJ1YMKHTTA3MRXX3VEAAH3XTKDN1ER7X6CZPMYTF8VK735VP2B2TZGTF28TTW4FZS32SBS64APCDF6SZQ427N5538TJC7SRE71YSP5ET8GS904002",
+ denomPubHash:
+ "8T51NEY81VMPQ180EQ5WR0YH7GMNNT90W55Q0514KZM18AZT71FHJGJHQXGK0WTA7ACN1X2SD0S53XPBQ1A9KH960R48VCVVM6E3TH8",
+ exchangeBaseUrl: "https://exchange.demo.taler.net/",
+ feeDeposit: {
+ currency: "KUDOS",
+ fraction: 1000000,
+ value: 0,
+ },
+ feeRefresh: {
+ currency: "KUDOS",
+ fraction: 1000000,
+ value: 0,
+ },
+ feeRefund: {
+ currency: "KUDOS",
+ fraction: 1000000,
+ value: 0,
+ },
+ feeWithdraw: {
+ currency: "KUDOS",
+ fraction: 1000000,
+ value: 0,
+ },
+ isOffered: true,
+ isRevoked: false,
+ masterSig:
+ "E3AWGAG8VB42P3KXM8B04Z6M483SX59R3Y4T53C3NXCA2NPB6C7HVCMVX05DC6S58E9X40NGEBQNYXKYMYCF3ASY2C4WP1WCZ4ME610",
+ stampExpireDeposit: {
+ t_ms: 1742909388000,
+ },
+ stampExpireLegal: {
+ t_ms: 1900589388000,
+ },
+ stampExpireWithdraw: {
+ t_ms: 1679837388000,
+ },
+ stampStart: {
+ t_ms: 1585229388000,
+ },
+ status: 0,
+ value: {
+ currency: "KUDOS",
+ fraction: 0,
+ value: 1,
+ },
+ },
+ {
+ denomPub:
+ "040000ZC0G60E9QQ5PD81TSDWD9GV5Y6P8Z05NSPA696DP07NGQQVSRQXBA76Q6PRB0YFX295RG4MTQJXAZZ860ET307HSC2X37XAVGQXRVB8Q4F1V7NP5ZEVKTX75DZK1QRAVHEZGQYKSSH6DBCJNQF6V9WNQF3GEYVA4KCBHA7JF772KHXM9642C28Z0AS4XXXV2PABAN5C8CHYD5H7JDFNK3920W5Q69X0BS84XZ4RE2PW6HM1WZ6KGZ3MKWWWCPKQ1FSFABRBWKAB09PF563BEBXKY6M38QETPH5EDWGANHD0SC3QV0WXYVB7BNHNNQ0J5BNV56K563SYHM4E5ND260YRJSYA1GN5YSW2B1J5T1A1EBNYF2DN6JNJKWXWEQ42G5YS17ZSZ5EWDRA9QKV8EGTCNAD04002",
+ denomPubHash:
+ "A41HW0Q2H9PCNMEWW0C0N45QAYVXZ8SBVRRAHE4W6X24SV1TH38ANTWDT80JXEBW9Z8PVPGT9GFV2EYZWJ5JW5W1N34NFNKHQSZ1PFR",
+ exchangeBaseUrl: "https://exchange.demo.taler.net/",
+ feeDeposit: {
+ currency: "KUDOS",
+ fraction: 1000000,
+ value: 0,
+ },
+ feeRefresh: {
+ currency: "KUDOS",
+ fraction: 1000000,
+ value: 0,
+ },
+ feeRefund: {
+ currency: "KUDOS",
+ fraction: 1000000,
+ value: 0,
+ },
+ feeWithdraw: {
+ currency: "KUDOS",
+ fraction: 1000000,
+ value: 0,
+ },
+ isOffered: true,
+ isRevoked: false,
+ masterSig:
+ "0ES1RKV002XB4YP21SN0QB7RSDHGYT0XAE65JYN8AVJAA6H7JZFN7JADXT521DJS89XMGPZGR8GCXF1516Y0Q9QDV00E6NMFA6CF838",
+ stampExpireDeposit: {
+ t_ms: 1742909388000,
+ },
+ stampExpireLegal: {
+ t_ms: 1900589388000,
+ },
+ stampExpireWithdraw: {
+ t_ms: 1679837388000,
+ },
+ stampStart: {
+ t_ms: 1585229388000,
+ },
+ status: 0,
+ value: {
+ currency: "KUDOS",
+ fraction: 10000000,
+ value: 0,
+ },
+ },
+ {
+ denomPub:
+ "040000ZSK2PMVY6E3NBQ52KXMW029M60F4BWYTDS0FZSD0PE53CNZ9H6TM3GQK1WRTEKQ5GRWJ1J9DY6Y42SP47QVT1XD1G0W05SQ5F3F7P5KSWR0FJBJ9NZBXQEVN8Q4JRC94X3JJ3XV3KBYTZ2HTDFV28C3H2SRR0XGNZB4FY85NDZF1G4AEYJJ9QB3C0V8H70YB8RV3FKTNH7XS4K4HFNZHJ5H9VMX5SM9Z2DX37HA5WFH0E2MJBVVF2BWWA5M0HPPSB365RAE2AMD42Q65A96WD80X27SB2ZNQZ8WX0K13FWF85GZ6YNYAJGE1KGN06JDEKE9QD68Z651D7XE8V6664TVVC8M68S7WD0DSXMJQKQ0BNJXNDE29Q7MRX6DA3RW0PZ44B3TKRK0294FPVZTNSTA6XF04002",
+ denomPubHash:
+ "F5NGBX33DTV4595XZZVK0S2MA1VMXFEJQERE5EBP5DS4QQ9EFRANN7YHWC1TKSHT2K6CQWDBRES8D3DWR0KZF5RET40B4AZXZ0RW1ZG",
+ exchangeBaseUrl: "https://exchange.demo.taler.net/",
+ feeDeposit: {
+ currency: "KUDOS",
+ fraction: 1000000,
+ value: 0,
+ },
+ feeRefresh: {
+ currency: "KUDOS",
+ fraction: 1000000,
+ value: 0,
+ },
+ feeRefund: {
+ currency: "KUDOS",
+ fraction: 1000000,
+ value: 0,
+ },
+ feeWithdraw: {
+ currency: "KUDOS",
+ fraction: 1000000,
+ value: 0,
+ },
+ isOffered: true,
+ isRevoked: false,
+ masterSig:
+ "58QEB6C6N7602E572E3JYANVVJ9BRW0V9E2ZFDW940N47YVQDK9SAFPWBN5YGT3G1742AFKQ0CYR4DM2VWV0Z0T1XMEKWN6X2EZ9M0R",
+ stampExpireDeposit: {
+ t_ms: 1742909388000,
+ },
+ stampExpireLegal: {
+ t_ms: 1900589388000,
+ },
+ stampExpireWithdraw: {
+ t_ms: 1679837388000,
+ },
+ stampStart: {
+ t_ms: 1585229388000,
+ },
+ status: 0,
+ value: {
+ currency: "KUDOS",
+ fraction: 0,
+ value: 2,
+ },
+ },
+ ];
+
+ const res = getWithdrawDenomList(amount, denoms);
+
+ console.error("cost", Amounts.stringify(res.totalWithdrawCost));
+ console.error("withdraw amount", Amounts.stringify(amount));
+
+ t.assert(Amounts.cmp(res.totalWithdrawCost, amount) <= 0);
+ t.pass();
+});
diff --git a/packages/taler-wallet-core/src/operations/withdraw.d.ts.map b/packages/taler-wallet-core/src/operations/withdraw.d.ts.map
new file mode 100644
index 000000000..51eeb1888
--- /dev/null
+++ b/packages/taler-wallet-core/src/operations/withdraw.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"withdraw.d.ts","sourceRoot":"","sources":["withdraw.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,UAAU,EAAW,MAAM,iBAAiB,CAAC;AACtD,OAAO,EACL,kBAAkB,EAQlB,yBAAyB,EAGzB,mBAAmB,EACpB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,mBAAmB,EACnB,uBAAuB,EAGxB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAGL,uBAAuB,EACxB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAgC9C;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,eAAe,EAAE,UAAU,EAC3B,MAAM,EAAE,kBAAkB,EAAE,GAC3B,yBAAyB,CAiD3B;AAED;;;GAGG;AACH,wBAAsB,qBAAqB,CACzC,EAAE,EAAE,mBAAmB,EACvB,gBAAgB,EAAE,MAAM,GACvB,OAAO,CAAC,mBAAmB,CAAC,CAyB9B;AA2QD,wBAAgB,yBAAyB,CACvC,GAAG,EAAE,yBAAyB,GAC7B,mBAAmB,CAWrB;AAED;;;;;;GAMG;AACH,wBAAsB,sBAAsB,CAC1C,EAAE,EAAE,mBAAmB,EACvB,eAAe,EAAE,MAAM,EACvB,MAAM,EAAE,UAAU,GACjB,OAAO,CAAC,yBAAyB,CAAC,CA8CpC;AAyBD,wBAAsB,oBAAoB,CACxC,EAAE,EAAE,mBAAmB,EACvB,iBAAiB,EAAE,MAAM,EACzB,QAAQ,UAAQ,GACf,OAAO,CAAC,IAAI,CAAC,CAOf;AAsED,wBAAsB,yBAAyB,CAC7C,EAAE,EAAE,mBAAmB,EACvB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,UAAU,GACjB,OAAO,CAAC,uBAAuB,CAAC,CA8FlC;AAED,wBAAsB,0BAA0B,CAC9C,EAAE,EAAE,mBAAmB,EACvB,gBAAgB,EAAE,MAAM,GACvB,OAAO,CAAC,uBAAuB,CAAC,CA2ClC"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts
new file mode 100644
index 000000000..3b0aa0095
--- /dev/null
+++ b/packages/taler-wallet-core/src/operations/withdraw.ts
@@ -0,0 +1,759 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019-2020 Taler Systems SA
+
+ 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 { AmountJson, Amounts } from "../util/amounts";
+import {
+ DenominationRecord,
+ Stores,
+ DenominationStatus,
+ CoinStatus,
+ CoinRecord,
+ initRetryInfo,
+ updateRetryInfoTimeout,
+ CoinSourceType,
+ DenominationSelectionInfo,
+ PlanchetRecord,
+ WithdrawalSourceType,
+ DenomSelectionState,
+} from "../types/dbTypes";
+import {
+ BankWithdrawDetails,
+ ExchangeWithdrawDetails,
+ OperationErrorDetails,
+ ExchangeListItem,
+} from "../types/walletTypes";
+import {
+ codecForWithdrawOperationStatusResponse,
+ codecForWithdrawResponse,
+ WithdrawUriInfoResponse,
+} from "../types/talerTypes";
+import { InternalWalletState } from "./state";
+import { parseWithdrawUri } from "../util/taleruri";
+import { Logger } from "../util/logging";
+import { updateExchangeFromUrl, getExchangeTrust } from "./exchanges";
+import { WALLET_EXCHANGE_PROTOCOL_VERSION } from "./versions";
+
+import * as LibtoolVersion from "../util/libtoolVersion";
+import { guardOperationException } from "./errors";
+import { NotificationType } from "../types/notifications";
+import {
+ getTimestampNow,
+ getDurationRemaining,
+ timestampCmp,
+ timestampSubtractDuraction,
+} from "../util/time";
+import { readSuccessResponseJsonOrThrow } from "../util/http";
+import { URL } from "../util/url";
+
+const logger = new Logger("withdraw.ts");
+
+function isWithdrawableDenom(d: DenominationRecord): boolean {
+ const now = getTimestampNow();
+ const started = timestampCmp(now, d.stampStart) >= 0;
+ const lastPossibleWithdraw = timestampSubtractDuraction(
+ d.stampExpireWithdraw,
+ { d_ms: 50 * 1000 },
+ );
+ const remaining = getDurationRemaining(lastPossibleWithdraw, now);
+ const stillOkay = remaining.d_ms !== 0;
+ return started && stillOkay && !d.isRevoked;
+}
+
+/**
+ * Get a list of denominations (with repetitions possible)
+ * whose total value is as close as possible to the available
+ * amount, but never larger.
+ */
+export function getWithdrawDenomList(
+ amountAvailable: AmountJson,
+ denoms: DenominationRecord[],
+): DenominationSelectionInfo {
+ let remaining = Amounts.copy(amountAvailable);
+
+ const selectedDenoms: {
+ count: number;
+ denom: DenominationRecord;
+ }[] = [];
+
+ let totalCoinValue = Amounts.getZero(amountAvailable.currency);
+ let totalWithdrawCost = Amounts.getZero(amountAvailable.currency);
+
+ denoms = denoms.filter(isWithdrawableDenom);
+ denoms.sort((d1, d2) => Amounts.cmp(d2.value, d1.value));
+
+ for (const d of denoms) {
+ let count = 0;
+ const cost = Amounts.add(d.value, d.feeWithdraw).amount;
+ for (;;) {
+ if (Amounts.cmp(remaining, cost) < 0) {
+ break;
+ }
+ remaining = Amounts.sub(remaining, cost).amount;
+ count++;
+ }
+ if (count > 0) {
+ totalCoinValue = Amounts.add(
+ totalCoinValue,
+ Amounts.mult(d.value, count).amount,
+ ).amount;
+ totalWithdrawCost = Amounts.add(
+ totalWithdrawCost,
+ Amounts.mult(cost, count).amount,
+ ).amount;
+ selectedDenoms.push({
+ count,
+ denom: d,
+ });
+ }
+
+ if (Amounts.isZero(remaining)) {
+ break;
+ }
+ }
+
+ return {
+ selectedDenoms,
+ totalCoinValue,
+ totalWithdrawCost,
+ };
+}
+
+/**
+ * Get information about a withdrawal from
+ * a taler://withdraw URI by asking the bank.
+ */
+export async function getBankWithdrawalInfo(
+ ws: InternalWalletState,
+ talerWithdrawUri: string,
+): Promise<BankWithdrawDetails> {
+ const uriResult = parseWithdrawUri(talerWithdrawUri);
+ if (!uriResult) {
+ throw Error(`can't parse URL ${talerWithdrawUri}`);
+ }
+ const reqUrl = new URL(
+ `api/withdraw-operation/${uriResult.withdrawalOperationId}`,
+ uriResult.bankIntegrationApiBaseUrl,
+ );
+ const resp = await ws.http.get(reqUrl.href);
+ const status = await readSuccessResponseJsonOrThrow(
+ resp,
+ codecForWithdrawOperationStatusResponse(),
+ );
+
+ return {
+ amount: Amounts.parseOrThrow(status.amount),
+ confirmTransferUrl: status.confirm_transfer_url,
+ extractedStatusUrl: reqUrl.href,
+ selectionDone: status.selection_done,
+ senderWire: status.sender_wire,
+ suggestedExchange: status.suggested_exchange,
+ transferDone: status.transfer_done,
+ wireTypes: status.wire_types,
+ };
+}
+
+/**
+ * Return denominations that can potentially used for a withdrawal.
+ */
+async function getPossibleDenoms(
+ ws: InternalWalletState,
+ exchangeBaseUrl: string,
+): Promise<DenominationRecord[]> {
+ return await ws.db
+ .iterIndex(Stores.denominations.exchangeBaseUrlIndex, exchangeBaseUrl)
+ .filter((d) => {
+ return (
+ (d.status === DenominationStatus.Unverified ||
+ d.status === DenominationStatus.VerifiedGood) &&
+ !d.isRevoked
+ );
+ });
+}
+
+/**
+ * Given a planchet, withdraw a coin from the exchange.
+ */
+async function processPlanchet(
+ ws: InternalWalletState,
+ withdrawalGroupId: string,
+ coinIdx: number,
+): Promise<void> {
+ const withdrawalGroup = await ws.db.get(
+ Stores.withdrawalGroups,
+ withdrawalGroupId,
+ );
+ if (!withdrawalGroup) {
+ return;
+ }
+ let planchet = await ws.db.getIndexed(Stores.planchets.byGroupAndIndex, [
+ withdrawalGroupId,
+ coinIdx,
+ ]);
+ if (!planchet) {
+ let ci = 0;
+ let denomPubHash: string | undefined;
+ for (
+ let di = 0;
+ di < withdrawalGroup.denomsSel.selectedDenoms.length;
+ di++
+ ) {
+ const d = withdrawalGroup.denomsSel.selectedDenoms[di];
+ if (coinIdx >= ci && coinIdx < ci + d.count) {
+ denomPubHash = d.denomPubHash;
+ break;
+ }
+ ci += d.count;
+ }
+ if (!denomPubHash) {
+ throw Error("invariant violated");
+ }
+ const denom = await ws.db.getIndexed(
+ Stores.denominations.denomPubHashIndex,
+ denomPubHash,
+ );
+ if (!denom) {
+ throw Error("invariant violated");
+ }
+ if (withdrawalGroup.source.type != WithdrawalSourceType.Reserve) {
+ throw Error("invariant violated");
+ }
+ const reserve = await ws.db.get(
+ Stores.reserves,
+ withdrawalGroup.source.reservePub,
+ );
+ if (!reserve) {
+ throw Error("invariant violated");
+ }
+ const r = await ws.cryptoApi.createPlanchet({
+ denomPub: denom.denomPub,
+ feeWithdraw: denom.feeWithdraw,
+ reservePriv: reserve.reservePriv,
+ reservePub: reserve.reservePub,
+ value: denom.value,
+ });
+ const newPlanchet: PlanchetRecord = {
+ blindingKey: r.blindingKey,
+ coinEv: r.coinEv,
+ coinEvHash: r.coinEvHash,
+ coinIdx,
+ coinPriv: r.coinPriv,
+ coinPub: r.coinPub,
+ coinValue: r.coinValue,
+ denomPub: r.denomPub,
+ denomPubHash: r.denomPubHash,
+ isFromTip: false,
+ reservePub: r.reservePub,
+ withdrawalDone: false,
+ withdrawSig: r.withdrawSig,
+ withdrawalGroupId: withdrawalGroupId,
+ };
+ await ws.db.runWithWriteTransaction([Stores.planchets], async (tx) => {
+ const p = await tx.getIndexed(Stores.planchets.byGroupAndIndex, [
+ withdrawalGroupId,
+ coinIdx,
+ ]);
+ if (p) {
+ planchet = p;
+ return;
+ }
+ await tx.put(Stores.planchets, newPlanchet);
+ planchet = newPlanchet;
+ });
+ }
+ if (!planchet) {
+ throw Error("invariant violated");
+ }
+ if (planchet.withdrawalDone) {
+ logger.warn("processPlanchet: planchet already withdrawn");
+ return;
+ }
+ const exchange = await ws.db.get(
+ Stores.exchanges,
+ withdrawalGroup.exchangeBaseUrl,
+ );
+ if (!exchange) {
+ logger.error("db inconsistent: exchange for planchet not found");
+ return;
+ }
+
+ const denom = await ws.db.get(Stores.denominations, [
+ withdrawalGroup.exchangeBaseUrl,
+ planchet.denomPub,
+ ]);
+
+ if (!denom) {
+ console.error("db inconsistent: denom for planchet not found");
+ return;
+ }
+
+ logger.trace(
+ `processing planchet #${coinIdx} in withdrawal ${withdrawalGroupId}`,
+ );
+
+ const wd: any = {};
+ wd.denom_pub_hash = planchet.denomPubHash;
+ wd.reserve_pub = planchet.reservePub;
+ wd.reserve_sig = planchet.withdrawSig;
+ wd.coin_ev = planchet.coinEv;
+ const reqUrl = new URL(
+ `reserves/${planchet.reservePub}/withdraw`,
+ exchange.baseUrl,
+ ).href;
+
+ const resp = await ws.http.postJson(reqUrl, wd);
+ const r = await readSuccessResponseJsonOrThrow(
+ resp,
+ codecForWithdrawResponse(),
+ );
+
+ logger.trace(`got response for /withdraw`);
+
+ const denomSig = await ws.cryptoApi.rsaUnblind(
+ r.ev_sig,
+ planchet.blindingKey,
+ planchet.denomPub,
+ );
+
+ const isValid = await ws.cryptoApi.rsaVerify(
+ planchet.coinPub,
+ denomSig,
+ planchet.denomPub,
+ );
+
+ if (!isValid) {
+ throw Error("invalid RSA signature by the exchange");
+ }
+
+ logger.trace(`unblinded and verified`);
+
+ const coin: CoinRecord = {
+ blindingKey: planchet.blindingKey,
+ coinPriv: planchet.coinPriv,
+ coinPub: planchet.coinPub,
+ currentAmount: planchet.coinValue,
+ denomPub: planchet.denomPub,
+ denomPubHash: planchet.denomPubHash,
+ denomSig,
+ exchangeBaseUrl: withdrawalGroup.exchangeBaseUrl,
+ status: CoinStatus.Fresh,
+ coinSource: {
+ type: CoinSourceType.Withdraw,
+ coinIndex: coinIdx,
+ reservePub: planchet.reservePub,
+ withdrawalGroupId: withdrawalGroupId,
+ },
+ suspended: false,
+ };
+
+ let withdrawalGroupFinished = false;
+
+ const planchetCoinPub = planchet.coinPub;
+
+ const success = await ws.db.runWithWriteTransaction(
+ [Stores.coins, Stores.withdrawalGroups, Stores.reserves, Stores.planchets],
+ async (tx) => {
+ const ws = await tx.get(Stores.withdrawalGroups, withdrawalGroupId);
+ if (!ws) {
+ return false;
+ }
+ const p = await tx.get(Stores.planchets, planchetCoinPub);
+ if (!p) {
+ return false;
+ }
+ if (p.withdrawalDone) {
+ // Already withdrawn
+ return false;
+ }
+ p.withdrawalDone = true;
+ await tx.put(Stores.planchets, p);
+
+ let numTotal = 0;
+
+ for (const ds of ws.denomsSel.selectedDenoms) {
+ numTotal += ds.count;
+ }
+
+ let numDone = 0;
+
+ await tx
+ .iterIndexed(Stores.planchets.byGroup, withdrawalGroupId)
+ .forEach((x) => {
+ if (x.withdrawalDone) {
+ numDone++;
+ }
+ });
+
+ if (numDone > numTotal) {
+ throw Error(
+ "invariant violated (created more planchets than expected)",
+ );
+ }
+
+ if (numDone == numTotal) {
+ ws.timestampFinish = getTimestampNow();
+ ws.lastError = undefined;
+ ws.retryInfo = initRetryInfo(false);
+ withdrawalGroupFinished = true;
+ }
+ await tx.put(Stores.withdrawalGroups, ws);
+ await tx.add(Stores.coins, coin);
+ return true;
+ },
+ );
+
+ logger.trace(`withdrawal result stored in DB`);
+
+ if (success) {
+ ws.notify({
+ type: NotificationType.CoinWithdrawn,
+ });
+ }
+
+ if (withdrawalGroupFinished) {
+ ws.notify({
+ type: NotificationType.WithdrawGroupFinished,
+ withdrawalSource: withdrawalGroup.source,
+ });
+ }
+}
+
+export function denomSelectionInfoToState(
+ dsi: DenominationSelectionInfo,
+): DenomSelectionState {
+ return {
+ selectedDenoms: dsi.selectedDenoms.map((x) => {
+ return {
+ count: x.count,
+ denomPubHash: x.denom.denomPubHash,
+ };
+ }),
+ totalCoinValue: dsi.totalCoinValue,
+ totalWithdrawCost: dsi.totalWithdrawCost,
+ };
+}
+
+/**
+ * Get a list of denominations to withdraw from the given exchange for the
+ * given amount, making sure that all denominations' signatures are verified.
+ *
+ * Writes to the DB in order to record the result from verifying
+ * denominations.
+ */
+export async function selectWithdrawalDenoms(
+ ws: InternalWalletState,
+ exchangeBaseUrl: string,
+ amount: AmountJson,
+): Promise<DenominationSelectionInfo> {
+ const exchange = await ws.db.get(Stores.exchanges, exchangeBaseUrl);
+ if (!exchange) {
+ logger.error("exchange not found");
+ throw Error(`exchange ${exchangeBaseUrl} not found`);
+ }
+ const exchangeDetails = exchange.details;
+ if (!exchangeDetails) {
+ logger.error("exchange details not available");
+ throw Error(`exchange ${exchangeBaseUrl} details not available`);
+ }
+
+ let allValid = false;
+ let selectedDenoms: DenominationSelectionInfo;
+
+ // Find a denomination selection for the requested amount.
+ // If a selected denomination has not been validated yet
+ // and turns our to be invalid, we try again with the
+ // reduced set of denominations.
+ do {
+ allValid = true;
+ const nextPossibleDenoms = await getPossibleDenoms(ws, exchange.baseUrl);
+ selectedDenoms = getWithdrawDenomList(amount, nextPossibleDenoms);
+ for (const denomSel of selectedDenoms.selectedDenoms) {
+ const denom = denomSel.denom;
+ if (denom.status === DenominationStatus.Unverified) {
+ const valid = await ws.cryptoApi.isValidDenom(
+ denom,
+ exchangeDetails.masterPublicKey,
+ );
+ if (!valid) {
+ denom.status = DenominationStatus.VerifiedBad;
+ allValid = false;
+ } else {
+ denom.status = DenominationStatus.VerifiedGood;
+ }
+ await ws.db.put(Stores.denominations, denom);
+ }
+ }
+ } while (selectedDenoms.selectedDenoms.length > 0 && !allValid);
+
+ if (Amounts.cmp(selectedDenoms.totalWithdrawCost, amount) > 0) {
+ throw Error("Bug: withdrawal coin selection is wrong");
+ }
+
+ return selectedDenoms;
+}
+
+async function incrementWithdrawalRetry(
+ ws: InternalWalletState,
+ withdrawalGroupId: string,
+ err: OperationErrorDetails | undefined,
+): Promise<void> {
+ await ws.db.runWithWriteTransaction([Stores.withdrawalGroups], async (tx) => {
+ const wsr = await tx.get(Stores.withdrawalGroups, withdrawalGroupId);
+ if (!wsr) {
+ return;
+ }
+ if (!wsr.retryInfo) {
+ return;
+ }
+ wsr.retryInfo.retryCounter++;
+ updateRetryInfoTimeout(wsr.retryInfo);
+ wsr.lastError = err;
+ await tx.put(Stores.withdrawalGroups, wsr);
+ });
+ if (err) {
+ ws.notify({ type: NotificationType.WithdrawOperationError, error: err });
+ }
+}
+
+export async function processWithdrawGroup(
+ ws: InternalWalletState,
+ withdrawalGroupId: string,
+ forceNow = false,
+): Promise<void> {
+ const onOpErr = (e: OperationErrorDetails): Promise<void> =>
+ incrementWithdrawalRetry(ws, withdrawalGroupId, e);
+ await guardOperationException(
+ () => processWithdrawGroupImpl(ws, withdrawalGroupId, forceNow),
+ onOpErr,
+ );
+}
+
+async function resetWithdrawalGroupRetry(
+ ws: InternalWalletState,
+ withdrawalGroupId: string,
+): Promise<void> {
+ await ws.db.mutate(Stores.withdrawalGroups, withdrawalGroupId, (x) => {
+ if (x.retryInfo.active) {
+ x.retryInfo = initRetryInfo();
+ }
+ return x;
+ });
+}
+
+async function processInBatches(
+ workGen: Iterator<Promise<void>>,
+ batchSize: number,
+): Promise<void> {
+ for (;;) {
+ const batch: Promise<void>[] = [];
+ for (let i = 0; i < batchSize; i++) {
+ const wn = workGen.next();
+ if (wn.done) {
+ break;
+ }
+ batch.push(wn.value);
+ }
+ if (batch.length == 0) {
+ break;
+ }
+ logger.trace(`processing withdrawal batch of ${batch.length} elements`);
+ await Promise.all(batch);
+ }
+}
+
+async function processWithdrawGroupImpl(
+ ws: InternalWalletState,
+ withdrawalGroupId: string,
+ forceNow: boolean,
+): Promise<void> {
+ logger.trace("processing withdraw group", withdrawalGroupId);
+ if (forceNow) {
+ await resetWithdrawalGroupRetry(ws, withdrawalGroupId);
+ }
+ const withdrawalGroup = await ws.db.get(
+ Stores.withdrawalGroups,
+ withdrawalGroupId,
+ );
+ if (!withdrawalGroup) {
+ logger.trace("withdraw session doesn't exist");
+ return;
+ }
+
+ const numDenoms = withdrawalGroup.denomsSel.selectedDenoms.length;
+ const genWork = function* (): Iterator<Promise<void>> {
+ let coinIdx = 0;
+ for (let i = 0; i < numDenoms; i++) {
+ const count = withdrawalGroup.denomsSel.selectedDenoms[i].count;
+ for (let j = 0; j < count; j++) {
+ yield processPlanchet(ws, withdrawalGroupId, coinIdx);
+ coinIdx++;
+ }
+ }
+ };
+
+ // Withdraw coins in batches.
+ // The batch size is relatively large
+ await processInBatches(genWork(), 10);
+}
+
+export async function getExchangeWithdrawalInfo(
+ ws: InternalWalletState,
+ baseUrl: string,
+ amount: AmountJson,
+): Promise<ExchangeWithdrawDetails> {
+ const exchangeInfo = await updateExchangeFromUrl(ws, baseUrl);
+ const exchangeDetails = exchangeInfo.details;
+ if (!exchangeDetails) {
+ throw Error(`exchange ${exchangeInfo.baseUrl} details not available`);
+ }
+ const exchangeWireInfo = exchangeInfo.wireInfo;
+ if (!exchangeWireInfo) {
+ throw Error(`exchange ${exchangeInfo.baseUrl} wire details not available`);
+ }
+
+ const selectedDenoms = await selectWithdrawalDenoms(ws, baseUrl, amount);
+ const exchangeWireAccounts: string[] = [];
+ for (const account of exchangeWireInfo.accounts) {
+ exchangeWireAccounts.push(account.payto_uri);
+ }
+
+ const { isTrusted, isAudited } = await getExchangeTrust(ws, exchangeInfo);
+
+ let earliestDepositExpiration =
+ selectedDenoms.selectedDenoms[0].denom.stampExpireDeposit;
+ for (let i = 1; i < selectedDenoms.selectedDenoms.length; i++) {
+ const expireDeposit =
+ selectedDenoms.selectedDenoms[i].denom.stampExpireDeposit;
+ if (expireDeposit.t_ms < earliestDepositExpiration.t_ms) {
+ earliestDepositExpiration = expireDeposit;
+ }
+ }
+
+ const possibleDenoms = await ws.db
+ .iterIndex(Stores.denominations.exchangeBaseUrlIndex, baseUrl)
+ .filter((d) => d.isOffered);
+
+ const trustedAuditorPubs = [];
+ const currencyRecord = await ws.db.get(Stores.currencies, amount.currency);
+ if (currencyRecord) {
+ trustedAuditorPubs.push(
+ ...currencyRecord.auditors.map((a) => a.auditorPub),
+ );
+ }
+
+ let versionMatch;
+ if (exchangeDetails.protocolVersion) {
+ versionMatch = LibtoolVersion.compare(
+ WALLET_EXCHANGE_PROTOCOL_VERSION,
+ exchangeDetails.protocolVersion,
+ );
+
+ if (
+ versionMatch &&
+ !versionMatch.compatible &&
+ versionMatch.currentCmp === -1
+ ) {
+ console.warn(
+ `wallet's support for exchange protocol version ${WALLET_EXCHANGE_PROTOCOL_VERSION} might be outdated ` +
+ `(exchange has ${exchangeDetails.protocolVersion}), checking for updates`,
+ );
+ }
+ }
+
+ let tosAccepted = false;
+
+ if (exchangeInfo.termsOfServiceAcceptedTimestamp) {
+ if (
+ exchangeInfo.termsOfServiceAcceptedEtag ==
+ exchangeInfo.termsOfServiceLastEtag
+ ) {
+ tosAccepted = true;
+ }
+ }
+
+ const withdrawFee = Amounts.sub(
+ selectedDenoms.totalWithdrawCost,
+ selectedDenoms.totalCoinValue,
+ ).amount;
+
+ const ret: ExchangeWithdrawDetails = {
+ earliestDepositExpiration,
+ exchangeInfo,
+ exchangeWireAccounts,
+ exchangeVersion: exchangeDetails.protocolVersion || "unknown",
+ isAudited,
+ isTrusted,
+ numOfferedDenoms: possibleDenoms.length,
+ overhead: Amounts.sub(amount, selectedDenoms.totalWithdrawCost).amount,
+ selectedDenoms,
+ trustedAuditorPubs,
+ versionMatch,
+ walletVersion: WALLET_EXCHANGE_PROTOCOL_VERSION,
+ wireFees: exchangeWireInfo,
+ withdrawFee,
+ termsOfServiceAccepted: tosAccepted,
+ };
+ return ret;
+}
+
+export async function getWithdrawalDetailsForUri(
+ ws: InternalWalletState,
+ talerWithdrawUri: string,
+): Promise<WithdrawUriInfoResponse> {
+ const info = await getBankWithdrawalInfo(ws, talerWithdrawUri);
+ if (info.suggestedExchange) {
+ // FIXME: right now the exchange gets permanently added,
+ // we might want to only temporarily add it.
+ try {
+ await updateExchangeFromUrl(ws, info.suggestedExchange);
+ } catch (e) {
+ // We still continued if it failed, as other exchanges might be available.
+ // We don't want to fail if the bank-suggested exchange is broken/offline.
+ logger.trace(
+ `querying bank-suggested exchange (${info.suggestedExchange}) failed`,
+ );
+ }
+ }
+
+ const exchangesRes: (ExchangeListItem | undefined)[] = await ws.db
+ .iter(Stores.exchanges)
+ .map((x) => {
+ const details = x.details;
+ if (!details) {
+ return undefined;
+ }
+ if (!x.addComplete) {
+ return undefined;
+ }
+ if (!x.wireInfo) {
+ return undefined;
+ }
+ if (details.currency !== info.amount.currency) {
+ return undefined;
+ }
+ return {
+ exchangeBaseUrl: x.baseUrl,
+ currency: details.currency,
+ paytoUris: x.wireInfo.accounts.map((x) => x.payto_uri),
+ };
+ });
+ const exchanges = exchangesRes.filter((x) => !!x) as ExchangeListItem[];
+
+ return {
+ amount: Amounts.stringify(info.amount),
+ defaultExchangeBaseUrl: info.suggestedExchange,
+ possibleExchanges: exchanges,
+ };
+}
diff --git a/packages/taler-wallet-core/src/types/ReserveStatus.d.ts.map b/packages/taler-wallet-core/src/types/ReserveStatus.d.ts.map
new file mode 100644
index 000000000..6b84dde85
--- /dev/null
+++ b/packages/taler-wallet-core/src/types/ReserveStatus.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"ReserveStatus.d.ts","sourceRoot":"","sources":["ReserveStatus.ts"],"names":[],"mappings":"AAgBA;;GAEG;AAEH;;GAEG;AACH,OAAO,EAIL,KAAK,EACN,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EACL,kBAAkB,EAEnB,MAAM,sBAAsB,CAAC;AAE9B;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B;;OAEG;IACH,OAAO,EAAE,YAAY,CAAC;IAEtB;;OAEG;IACH,OAAO,EAAE,kBAAkB,EAAE,CAAC;CAC/B;AAED,eAAO,MAAM,qBAAqB,QAAO,KAAK,CAAC,aAAa,CAIjC,CAAC"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/types/ReserveStatus.ts b/packages/taler-wallet-core/src/types/ReserveStatus.ts
new file mode 100644
index 000000000..18601b9a7
--- /dev/null
+++ b/packages/taler-wallet-core/src/types/ReserveStatus.ts
@@ -0,0 +1,57 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * @author Florian Dold <dold@taler.net>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ codecForString,
+ makeCodecForObject,
+ makeCodecForList,
+ Codec,
+} from "../util/codec";
+import { AmountString } from "./talerTypes";
+import {
+ ReserveTransaction,
+ codecForReserveTransaction,
+} from "./ReserveTransaction";
+
+/**
+ * Status of a reserve.
+ *
+ * Schema type for the exchange's response to "/reserve/status".
+ */
+export interface ReserveStatus {
+ /**
+ * Balance left in the reserve.
+ */
+ balance: AmountString;
+
+ /**
+ * Transaction history for the reserve.
+ */
+ history: ReserveTransaction[];
+}
+
+export const codecForReserveStatus = (): Codec<ReserveStatus> =>
+ makeCodecForObject<ReserveStatus>()
+ .property("balance", codecForString)
+ .property("history", makeCodecForList(codecForReserveTransaction()))
+ .build("ReserveStatus");
diff --git a/packages/taler-wallet-core/src/types/ReserveTransaction.d.ts.map b/packages/taler-wallet-core/src/types/ReserveTransaction.d.ts.map
new file mode 100644
index 000000000..c4672bc45
--- /dev/null
+++ b/packages/taler-wallet-core/src/types/ReserveTransaction.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"ReserveTransaction.d.ts","sourceRoot":"","sources":["ReserveTransaction.ts"],"names":[],"mappings":"AAgBA;;GAEG;AAEH;;GAEG;AACH,OAAO,EAKL,KAAK,EACN,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,YAAY,EACZ,YAAY,EACZ,oBAAoB,EACpB,oBAAoB,EACpB,mBAAmB,EACpB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,SAAS,EAAqB,MAAM,cAAc,CAAC;AAE5D,0BAAkB,sBAAsB;IACtC,QAAQ,aAAa;IACrB,MAAM,WAAW;IACjB,MAAM,WAAW;IACjB,OAAO,YAAY;CACpB;AAED,MAAM,WAAW,0BAA0B;IACzC,IAAI,EAAE,sBAAsB,CAAC,QAAQ,CAAC;IAEtC;;OAEG;IACH,MAAM,EAAE,YAAY,CAAC;IAErB;;OAEG;IACH,WAAW,EAAE,YAAY,CAAC;IAE1B;;OAEG;IACH,eAAe,EAAE,YAAY,CAAC;IAE9B;;;OAGG;IACH,WAAW,EAAE,oBAAoB,CAAC;IAElC;;OAEG;IACH,YAAY,EAAE,YAAY,CAAC;CAC5B;AAED,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,sBAAsB,CAAC,MAAM,CAAC;IAEpC;;OAEG;IACH,MAAM,EAAE,YAAY,CAAC;IAErB;;OAEG;IACH,kBAAkB,EAAE,MAAM,CAAC;IAE3B;;OAEG;IACH,cAAc,EAAE,MAAM,CAAC;IAEvB;;OAEG;IACH,SAAS,EAAE,SAAS,CAAC;CACtB;AAED,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,sBAAsB,CAAC,OAAO,CAAC;IAErC;;OAEG;IACH,MAAM,EAAE,YAAY,CAAC;IAErB;;OAEG;IACH,WAAW,EAAE,YAAY,CAAC;IAE1B;;OAEG;IACH,IAAI,EAAE,MAAM,CAAC;IAEb;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;IAEf;;;;OAIG;IACH,YAAY,EAAE,oBAAoB,CAAC;IAEnC;;OAEG;IACH,YAAY,EAAE,oBAAoB,CAAC;IAEnC;;OAEG;IACH,SAAS,EAAE,SAAS,CAAC;CACtB;AAED,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,sBAAsB,CAAC,MAAM,CAAC;IAEpC;;OAEG;IACH,MAAM,EAAE,YAAY,CAAC;IAErB;;;;OAIG;IACH,YAAY,EAAE,oBAAoB,CAAC;IAEnC;;OAEG;IACH,YAAY,EAAE,oBAAoB,CAAC;IAEnC;;OAEG;IACH,SAAS,EAAE,SAAS,CAAC;IAErB;;OAEG;IACH,QAAQ,EAAE,mBAAmB,CAAC;CAC/B;AAED;;GAEG;AACH,oBAAY,kBAAkB,GAC1B,0BAA0B,GAC1B,wBAAwB,GACxB,yBAAyB,GACzB,wBAAwB,CAAC;AAE7B,eAAO,MAAM,kCAAkC,QAAO,KAAK,CACzD,0BAA0B,CASY,CAAC;AAEzC,eAAO,MAAM,gCAAgC,QAAO,KAAK,CACvD,wBAAwB,CAQY,CAAC;AAEvC,eAAO,MAAM,iCAAiC,QAAO,KAAK,CACxD,yBAAyB,CAWY,CAAC;AAExC,eAAO,MAAM,gCAAgC,QAAO,KAAK,CACvD,wBAAwB,CASY,CAAC;AAEvC,eAAO,MAAM,0BAA0B,QAAO,KAAK,CAAC,kBAAkB,CAmBlB,CAAC"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/types/ReserveTransaction.ts b/packages/taler-wallet-core/src/types/ReserveTransaction.ts
new file mode 100644
index 000000000..bdd9b0f93
--- /dev/null
+++ b/packages/taler-wallet-core/src/types/ReserveTransaction.ts
@@ -0,0 +1,250 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * @author Florian Dold <dold@taler.net>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ codecForString,
+ makeCodecForObject,
+ makeCodecForConstString,
+ makeCodecForUnion,
+ Codec,
+} from "../util/codec";
+import {
+ AmountString,
+ Base32String,
+ EddsaSignatureString,
+ EddsaPublicKeyString,
+ CoinPublicKeyString,
+} from "./talerTypes";
+import { Timestamp, codecForTimestamp } from "../util/time";
+
+export const enum ReserveTransactionType {
+ Withdraw = "WITHDRAW",
+ Credit = "CREDIT",
+ Recoup = "RECOUP",
+ Closing = "CLOSING",
+}
+
+export interface ReserveWithdrawTransaction {
+ type: ReserveTransactionType.Withdraw;
+
+ /**
+ * Amount withdrawn.
+ */
+ amount: AmountString;
+
+ /**
+ * Hash of the denomination public key of the coin.
+ */
+ h_denom_pub: Base32String;
+
+ /**
+ * Hash of the blinded coin to be signed
+ */
+ h_coin_envelope: Base32String;
+
+ /**
+ * Signature of 'TALER_WithdrawRequestPS' created with the reserves's
+ * private key.
+ */
+ reserve_sig: EddsaSignatureString;
+
+ /**
+ * Fee that is charged for withdraw.
+ */
+ withdraw_fee: AmountString;
+}
+
+export interface ReserveCreditTransaction {
+ type: ReserveTransactionType.Credit;
+
+ /**
+ * Amount withdrawn.
+ */
+ amount: AmountString;
+
+ /**
+ * Sender account payto://-URL
+ */
+ sender_account_url: string;
+
+ /**
+ * Transfer details uniquely identifying the transfer.
+ */
+ wire_reference: string;
+
+ /**
+ * Timestamp of the incoming wire transfer.
+ */
+ timestamp: Timestamp;
+}
+
+export interface ReserveClosingTransaction {
+ type: ReserveTransactionType.Closing;
+
+ /**
+ * Closing balance.
+ */
+ amount: AmountString;
+
+ /**
+ * Closing fee charged by the exchange.
+ */
+ closing_fee: AmountString;
+
+ /**
+ * Wire transfer subject.
+ */
+ wtid: string;
+
+ /**
+ * Hash of the wire account into which the funds were returned to.
+ */
+ h_wire: string;
+
+ /**
+ * This is a signature over a
+ * struct TALER_ReserveCloseConfirmationPS with purpose
+ * TALER_SIGNATURE_EXCHANGE_RESERVE_CLOSED.
+ */
+ exchange_sig: EddsaSignatureString;
+
+ /**
+ * Public key used to create exchange_sig.
+ */
+ exchange_pub: EddsaPublicKeyString;
+
+ /**
+ * Time when the reserve was closed.
+ */
+ timestamp: Timestamp;
+}
+
+export interface ReserveRecoupTransaction {
+ type: ReserveTransactionType.Recoup;
+
+ /**
+ * Amount paid back.
+ */
+ amount: AmountString;
+
+ /**
+ * This is a signature over
+ * a struct TALER_PaybackConfirmationPS with purpose
+ * TALER_SIGNATURE_EXCHANGE_CONFIRM_PAYBACK.
+ */
+ exchange_sig: EddsaSignatureString;
+
+ /**
+ * Public key used to create exchange_sig.
+ */
+ exchange_pub: EddsaPublicKeyString;
+
+ /**
+ * Time when the funds were paid back into the reserve.
+ */
+ timestamp: Timestamp;
+
+ /**
+ * Public key of the coin that was paid back.
+ */
+ coin_pub: CoinPublicKeyString;
+}
+
+/**
+ * Format of the exchange's transaction history for a reserve.
+ */
+export type ReserveTransaction =
+ | ReserveWithdrawTransaction
+ | ReserveCreditTransaction
+ | ReserveClosingTransaction
+ | ReserveRecoupTransaction;
+
+export const codecForReserveWithdrawTransaction = (): Codec<
+ ReserveWithdrawTransaction
+> =>
+ makeCodecForObject<ReserveWithdrawTransaction>()
+ .property("amount", codecForString)
+ .property("h_coin_envelope", codecForString)
+ .property("h_denom_pub", codecForString)
+ .property("reserve_sig", codecForString)
+ .property("type", makeCodecForConstString(ReserveTransactionType.Withdraw))
+ .property("withdraw_fee", codecForString)
+ .build("ReserveWithdrawTransaction");
+
+export const codecForReserveCreditTransaction = (): Codec<
+ ReserveCreditTransaction
+> =>
+ makeCodecForObject<ReserveCreditTransaction>()
+ .property("amount", codecForString)
+ .property("sender_account_url", codecForString)
+ .property("timestamp", codecForTimestamp)
+ .property("wire_reference", codecForString)
+ .property("type", makeCodecForConstString(ReserveTransactionType.Credit))
+ .build("ReserveCreditTransaction");
+
+export const codecForReserveClosingTransaction = (): Codec<
+ ReserveClosingTransaction
+> =>
+ makeCodecForObject<ReserveClosingTransaction>()
+ .property("amount", codecForString)
+ .property("closing_fee", codecForString)
+ .property("exchange_pub", codecForString)
+ .property("exchange_sig", codecForString)
+ .property("h_wire", codecForString)
+ .property("timestamp", codecForTimestamp)
+ .property("type", makeCodecForConstString(ReserveTransactionType.Closing))
+ .property("wtid", codecForString)
+ .build("ReserveClosingTransaction");
+
+export const codecForReserveRecoupTransaction = (): Codec<
+ ReserveRecoupTransaction
+> =>
+ makeCodecForObject<ReserveRecoupTransaction>()
+ .property("amount", codecForString)
+ .property("coin_pub", codecForString)
+ .property("exchange_pub", codecForString)
+ .property("exchange_sig", codecForString)
+ .property("timestamp", codecForTimestamp)
+ .property("type", makeCodecForConstString(ReserveTransactionType.Recoup))
+ .build("ReserveRecoupTransaction");
+
+export const codecForReserveTransaction = (): Codec<ReserveTransaction> =>
+ makeCodecForUnion<ReserveTransaction>()
+ .discriminateOn("type")
+ .alternative(
+ ReserveTransactionType.Withdraw,
+ codecForReserveWithdrawTransaction(),
+ )
+ .alternative(
+ ReserveTransactionType.Closing,
+ codecForReserveClosingTransaction(),
+ )
+ .alternative(
+ ReserveTransactionType.Recoup,
+ codecForReserveRecoupTransaction(),
+ )
+ .alternative(
+ ReserveTransactionType.Credit,
+ codecForReserveCreditTransaction(),
+ )
+ .build<ReserveTransaction>("ReserveTransaction");
diff --git a/packages/taler-wallet-core/src/types/dbTypes.d.ts.map b/packages/taler-wallet-core/src/types/dbTypes.d.ts.map
new file mode 100644
index 000000000..7b89c4de4
--- /dev/null
+++ b/packages/taler-wallet-core/src/types/dbTypes.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"dbTypes.d.ts","sourceRoot":"","sources":["dbTypes.ts"],"names":[],"mappings":"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"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/types/dbTypes.ts b/packages/taler-wallet-core/src/types/dbTypes.ts
new file mode 100644
index 000000000..3e1fdfe25
--- /dev/null
+++ b/packages/taler-wallet-core/src/types/dbTypes.ts
@@ -0,0 +1,1819 @@
+/*
+ This file is part of GNU Taler
+ (C) 2018-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/>
+ */
+
+/**
+ * Types for records stored in the wallet's database.
+ *
+ * Types for the objects in the database should end in "-Record".
+ */
+
+/**
+ * Imports.
+ */
+import { AmountJson } from "../util/amounts";
+import {
+ Auditor,
+ CoinDepositPermission,
+ TipResponse,
+ ExchangeSignKeyJson,
+ MerchantInfo,
+ Product,
+} from "./talerTypes";
+
+import { Index, Store } from "../util/query";
+import { OperationErrorDetails, RefreshReason } from "./walletTypes";
+import {
+ ReserveTransaction,
+ ReserveCreditTransaction,
+ ReserveWithdrawTransaction,
+ ReserveClosingTransaction,
+ ReserveRecoupTransaction,
+} from "./ReserveTransaction";
+import { Timestamp, Duration, getTimestampNow } from "../util/time";
+import { PayCoinSelection, PayCostInfo } from "../operations/pay";
+import { idbtypes } from "idb-bridge";
+
+export enum ReserveRecordStatus {
+ /**
+ * Reserve must be registered with the bank.
+ */
+ REGISTERING_BANK = "registering-bank",
+
+ /**
+ * We've registered reserve's information with the bank
+ * and are now waiting for the user to confirm the withdraw
+ * with the bank (typically 2nd factor auth).
+ */
+ WAIT_CONFIRM_BANK = "wait-confirm-bank",
+
+ /**
+ * Querying reserve status with the exchange.
+ */
+ QUERYING_STATUS = "querying-status",
+
+ /**
+ * Status is queried, the wallet must now select coins
+ * and start withdrawing.
+ */
+ WITHDRAWING = "withdrawing",
+
+ /**
+ * The corresponding withdraw record has been created.
+ * No further processing is done, unless explicitly requested
+ * by the user.
+ */
+ DORMANT = "dormant",
+}
+
+export interface RetryInfo {
+ firstTry: Timestamp;
+ nextRetry: Timestamp;
+ retryCounter: number;
+ active: boolean;
+}
+
+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 initRetryInfo(
+ active = true,
+ p: RetryPolicy = defaultRetryPolicy,
+): RetryInfo {
+ if (!active) {
+ return {
+ active: false,
+ firstTry: { t_ms: Number.MAX_SAFE_INTEGER },
+ nextRetry: { t_ms: Number.MAX_SAFE_INTEGER },
+ retryCounter: 0,
+ };
+ }
+ const info = {
+ firstTry: getTimestampNow(),
+ active: true,
+ nextRetry: { t_ms: 0 },
+ retryCounter: 0,
+ };
+ updateRetryInfoTimeout(info, p);
+ return info;
+}
+
+export const enum WalletReserveHistoryItemType {
+ Credit = "credit",
+ Withdraw = "withdraw",
+ Closing = "closing",
+ Recoup = "recoup",
+}
+
+export interface WalletReserveHistoryCreditItem {
+ type: WalletReserveHistoryItemType.Credit;
+
+ /**
+ * Amount we expect to see credited.
+ */
+ expectedAmount?: AmountJson;
+
+ /**
+ * Item from the reserve transaction history that this
+ * wallet reserve history item matches up with.
+ */
+ matchedExchangeTransaction?: ReserveCreditTransaction;
+}
+
+export interface WalletReserveHistoryWithdrawItem {
+ expectedAmount?: AmountJson;
+
+ /**
+ * Hash of the blinded coin.
+ *
+ * When this value is set, it indicates that a withdrawal is active
+ * in the wallet for the
+ */
+ expectedCoinEvHash?: string;
+
+ type: WalletReserveHistoryItemType.Withdraw;
+
+ /**
+ * Item from the reserve transaction history that this
+ * wallet reserve history item matches up with.
+ */
+ matchedExchangeTransaction?: ReserveWithdrawTransaction;
+}
+
+export interface WalletReserveHistoryClosingItem {
+ type: WalletReserveHistoryItemType.Closing;
+
+ /**
+ * Item from the reserve transaction history that this
+ * wallet reserve history item matches up with.
+ */
+ matchedExchangeTransaction?: ReserveClosingTransaction;
+}
+
+export interface WalletReserveHistoryRecoupItem {
+ type: WalletReserveHistoryItemType.Recoup;
+
+ /**
+ * Amount we expect to see recouped.
+ */
+ expectedAmount?: AmountJson;
+
+ /**
+ * Item from the reserve transaction history that this
+ * wallet reserve history item matches up with.
+ */
+ matchedExchangeTransaction?: ReserveRecoupTransaction;
+}
+
+export type WalletReserveHistoryItem =
+ | WalletReserveHistoryCreditItem
+ | WalletReserveHistoryWithdrawItem
+ | WalletReserveHistoryRecoupItem
+ | WalletReserveHistoryClosingItem;
+
+export interface ReserveHistoryRecord {
+ reservePub: string;
+ reserveTransactions: WalletReserveHistoryItem[];
+}
+
+export interface ReserveBankInfo {
+ /**
+ * Status URL that the wallet will use to query the status
+ * of the Taler withdrawal operation on the bank's side.
+ */
+ statusUrl: string;
+
+ confirmUrl?: string;
+
+ /**
+ * Exchange payto URI that the bank will use to fund the reserve.
+ */
+ exchangePaytoUri: string;
+}
+
+/**
+ * A reserve record as stored in the wallet's database.
+ */
+export interface ReserveRecord {
+ /**
+ * The reserve public key.
+ */
+ reservePub: string;
+
+ /**
+ * The reserve private key.
+ */
+ reservePriv: string;
+
+ /**
+ * The exchange base URL.
+ */
+ exchangeBaseUrl: string;
+
+ /**
+ * Currency of the reserve.
+ */
+ currency: string;
+
+ /**
+ * Time when the reserve was created.
+ */
+ timestampCreated: Timestamp;
+
+ /**
+ * Time when the information about this reserve was posted to the bank.
+ *
+ * Only applies if bankWithdrawStatusUrl is defined.
+ *
+ * Set to 0 if that hasn't happened yet.
+ */
+ timestampReserveInfoPosted: Timestamp | undefined;
+
+ /**
+ * Time when the reserve was confirmed by the bank.
+ *
+ * Set to undefined if not confirmed yet.
+ */
+ timestampBankConfirmed: Timestamp | undefined;
+
+ /**
+ * Wire information (as payto URI) for the bank account that
+ * transfered funds for this reserve.
+ */
+ senderWire?: string;
+
+ /**
+ * Amount that was sent by the user to fund the reserve.
+ */
+ instructedAmount: AmountJson;
+
+ /**
+ * Extra state for when this is a withdrawal involving
+ * a Taler-integrated bank.
+ */
+ bankInfo?: ReserveBankInfo;
+
+ initialWithdrawalGroupId: string;
+
+ /**
+ * Did we start the first withdrawal for this reserve?
+ *
+ * We only report a pending withdrawal for the reserve before
+ * the first withdrawal has started.
+ */
+ initialWithdrawalStarted: boolean;
+ initialDenomSel: DenomSelectionState;
+
+ reserveStatus: ReserveRecordStatus;
+
+ /**
+ * Time of the last successful status query.
+ */
+ lastSuccessfulStatusQuery: Timestamp | undefined;
+
+ /**
+ * Retry info. This field is present even if no retry is scheduled,
+ * because we need it to be present for the index on the object store
+ * to work.
+ */
+ retryInfo: RetryInfo;
+
+ /**
+ * Last error that happened in a reserve operation
+ * (either talking to the bank or the exchange).
+ */
+ lastError: OperationErrorDetails | undefined;
+}
+
+/**
+ * Auditor record as stored with currencies in the exchange database.
+ */
+export interface AuditorRecord {
+ /**
+ * Base url of the auditor.
+ */
+ baseUrl: string;
+ /**
+ * Public signing key of the auditor.
+ */
+ auditorPub: string;
+ /**
+ * Time when the auditing expires.
+ */
+ expirationStamp: number;
+}
+
+/**
+ * Exchange for currencies as stored in the wallet's currency
+ * information database.
+ */
+export interface ExchangeForCurrencyRecord {
+ /**
+ * FIXME: unused?
+ */
+ exchangePub: string;
+ /**
+ * Base URL of the exchange.
+ */
+ baseUrl: string;
+}
+
+/**
+ * Information about a currency as displayed in the wallet's database.
+ */
+export interface CurrencyRecord {
+ /**
+ * Name of the currency.
+ */
+ name: string;
+ /**
+ * Number of fractional digits to show when rendering the currency.
+ */
+ fractionalDigits: number;
+ /**
+ * Auditors that the wallet trusts for this currency.
+ */
+ auditors: AuditorRecord[];
+ /**
+ * Exchanges that the wallet trusts for this currency.
+ */
+ exchanges: ExchangeForCurrencyRecord[];
+}
+
+/**
+ * Status of a denomination.
+ */
+export enum DenominationStatus {
+ /**
+ * Verification was delayed.
+ */
+ Unverified,
+ /**
+ * Verified as valid.
+ */
+ VerifiedGood,
+ /**
+ * Verified as invalid.
+ */
+ VerifiedBad,
+}
+
+/**
+ * Denomination record as stored in the wallet's database.
+ */
+export interface DenominationRecord {
+ /**
+ * Value of one coin of the denomination.
+ */
+ value: AmountJson;
+
+ /**
+ * The denomination public key.
+ */
+ denomPub: string;
+
+ /**
+ * Hash of the denomination public key.
+ * Stored in the database for faster lookups.
+ */
+ denomPubHash: string;
+
+ /**
+ * Fee for withdrawing.
+ */
+ feeWithdraw: AmountJson;
+
+ /**
+ * Fee for depositing.
+ */
+ feeDeposit: AmountJson;
+
+ /**
+ * Fee for refreshing.
+ */
+ feeRefresh: AmountJson;
+
+ /**
+ * Fee for refunding.
+ */
+ feeRefund: AmountJson;
+
+ /**
+ * Validity start date of the denomination.
+ */
+ stampStart: Timestamp;
+
+ /**
+ * Date after which the currency can't be withdrawn anymore.
+ */
+ stampExpireWithdraw: Timestamp;
+
+ /**
+ * Date after the denomination officially doesn't exist anymore.
+ */
+ stampExpireLegal: Timestamp;
+
+ /**
+ * Data after which coins of this denomination can't be deposited anymore.
+ */
+ stampExpireDeposit: Timestamp;
+
+ /**
+ * Signature by the exchange's master key over the denomination
+ * information.
+ */
+ masterSig: string;
+
+ /**
+ * Did we verify the signature on the denomination?
+ *
+ * FIXME: Rename to "verificationStatus"?
+ */
+ status: DenominationStatus;
+
+ /**
+ * Was this denomination still offered by the exchange the last time
+ * we checked?
+ * Only false when the exchange redacts a previously published denomination.
+ */
+ isOffered: boolean;
+
+ /**
+ * Did the exchange revoke the denomination?
+ * When this field is set to true in the database, the same transaction
+ * should also mark all affected coins as revoked.
+ */
+ isRevoked: boolean;
+
+ /**
+ * Base URL of the exchange.
+ */
+ exchangeBaseUrl: string;
+}
+
+/**
+ * Details about the exchange that we only know after
+ * querying /keys and /wire.
+ */
+export interface ExchangeDetails {
+ /**
+ * Master public key of the exchange.
+ */
+ masterPublicKey: string;
+
+ /**
+ * Auditors (partially) auditing the exchange.
+ */
+ auditors: Auditor[];
+
+ /**
+ * Currency that the exchange offers.
+ */
+ currency: string;
+
+ /**
+ * Last observed protocol version.
+ */
+ protocolVersion: string;
+
+ /**
+ * Signing keys we got from the exchange, can also contain
+ * older signing keys that are not returned by /keys anymore.
+ */
+ signingKeys: ExchangeSignKeyJson[];
+
+ /**
+ * Timestamp for last update.
+ */
+ lastUpdateTime: Timestamp;
+}
+
+export const enum ExchangeUpdateStatus {
+ FetchKeys = "fetch-keys",
+ FetchWire = "fetch-wire",
+ FetchTerms = "fetch-terms",
+ FinalizeUpdate = "finalize-update",
+ Finished = "finished",
+}
+
+export interface ExchangeBankAccount {
+ payto_uri: string;
+}
+
+export interface ExchangeWireInfo {
+ feesForType: { [wireMethod: string]: WireFee[] };
+ accounts: ExchangeBankAccount[];
+}
+
+/**
+ * Summary of updates to the exchange.
+ */
+// eslint-disable-next-line @typescript-eslint/no-empty-interface
+export interface ExchangeUpdateDiff {
+ // FIXME: implement!
+}
+
+export const enum ExchangeUpdateReason {
+ Initial = "initial",
+ Forced = "forced",
+ Scheduled = "scheduled",
+}
+
+/**
+ * Exchange record as stored in the wallet's database.
+ */
+export interface ExchangeRecord {
+ /**
+ * Base url of the exchange.
+ */
+ baseUrl: string;
+
+ /**
+ * Did we finish adding the exchange?
+ */
+ addComplete: boolean;
+
+ /**
+ * Is this a permanent or temporary exchange record?
+ */
+ permanent: boolean;
+
+ /**
+ * Was the exchange added as a built-in exchange?
+ */
+ builtIn: boolean;
+
+ /**
+ * Details, once known.
+ */
+ details: ExchangeDetails | undefined;
+
+ /**
+ * Mapping from wire method type to the wire fee.
+ */
+ wireInfo: ExchangeWireInfo | undefined;
+
+ /**
+ * When was the exchange added to the wallet?
+ */
+ timestampAdded: Timestamp;
+
+ /**
+ * Terms of service text or undefined if not downloaded yet.
+ */
+ termsOfServiceText: string | undefined;
+
+ /**
+ * ETag for last terms of service download.
+ */
+ termsOfServiceLastEtag: string | undefined;
+
+ /**
+ * ETag for last terms of service download.
+ */
+ termsOfServiceAcceptedEtag: string | undefined;
+
+ /**
+ * ETag for last terms of service download.
+ */
+ termsOfServiceAcceptedTimestamp: Timestamp | undefined;
+
+ /**
+ * Time when the update to the exchange has been started or
+ * undefined if no update is in progress.
+ */
+ updateStarted: Timestamp | undefined;
+
+ /**
+ * Status of updating the info about the exchange.
+ */
+ updateStatus: ExchangeUpdateStatus;
+
+ updateReason?: ExchangeUpdateReason;
+
+ /**
+ * Update diff, will be incorporated when the update is finalized.
+ */
+ updateDiff: ExchangeUpdateDiff | undefined;
+
+ lastError?: OperationErrorDetails;
+}
+
+/**
+ * A coin that isn't yet signed by an exchange.
+ */
+export interface PlanchetRecord {
+ /**
+ * Public key of the coin.
+ */
+ coinPub: string;
+
+ /**
+ * Private key of the coin.
+ */
+ coinPriv: string;
+
+ /**
+ * Withdrawal group that this planchet belongs to
+ * (or the empty string).
+ */
+ withdrawalGroupId: string;
+
+ /**
+ * Index within the withdrawal group (or -1).
+ */
+ coinIdx: number;
+
+ withdrawalDone: boolean;
+
+ /**
+ * Public key of the reserve, this might be a reserve not
+ * known to the wallet if the planchet is from a tip.
+ */
+ reservePub: string;
+ denomPubHash: string;
+ denomPub: string;
+ blindingKey: string;
+ withdrawSig: string;
+ coinEv: string;
+ coinEvHash: string;
+ coinValue: AmountJson;
+ isFromTip: boolean;
+}
+
+/**
+ * Planchet for a coin during refrehs.
+ */
+export interface RefreshPlanchetRecord {
+ /**
+ * Public key for the coin.
+ */
+ publicKey: string;
+ /**
+ * Private key for the coin.
+ */
+ privateKey: string;
+ /**
+ * Blinded public key.
+ */
+ coinEv: string;
+ /**
+ * Blinding key used.
+ */
+ blindingKey: string;
+}
+
+/**
+ * Status of a coin.
+ */
+export const enum CoinStatus {
+ /**
+ * Withdrawn and never shown to anybody.
+ */
+ Fresh = "fresh",
+ /**
+ * A coin that has been spent and refreshed.
+ */
+ Dormant = "dormant",
+}
+
+export const enum CoinSourceType {
+ Withdraw = "withdraw",
+ Refresh = "refresh",
+ Tip = "tip",
+}
+
+export interface WithdrawCoinSource {
+ type: CoinSourceType.Withdraw;
+ withdrawalGroupId: string;
+
+ /**
+ * Index of the coin in the withdrawal session.
+ */
+ coinIndex: number;
+
+ /**
+ * Reserve public key for the reserve we got this coin from.
+ */
+ reservePub: string;
+}
+
+export interface RefreshCoinSource {
+ type: CoinSourceType.Refresh;
+ oldCoinPub: string;
+}
+
+export interface TipCoinSource {
+ type: CoinSourceType.Tip;
+}
+
+export type CoinSource = WithdrawCoinSource | RefreshCoinSource | TipCoinSource;
+
+/**
+ * CoinRecord as stored in the "coins" data store
+ * of the wallet database.
+ */
+export interface CoinRecord {
+ /**
+ * Where did the coin come from? Used for recouping coins.
+ */
+ coinSource: CoinSource;
+
+ /**
+ * Public key of the coin.
+ */
+ coinPub: string;
+
+ /**
+ * Private key to authorize operations on the coin.
+ */
+ coinPriv: string;
+
+ /**
+ * Key used by the exchange used to sign the coin.
+ */
+ denomPub: string;
+
+ /**
+ * Hash of the public key that signs the coin.
+ */
+ denomPubHash: string;
+
+ /**
+ * Unblinded signature by the exchange.
+ */
+ denomSig: string;
+
+ /**
+ * Amount that's left on the coin.
+ */
+ currentAmount: AmountJson;
+
+ /**
+ * Base URL that identifies the exchange from which we got the
+ * coin.
+ */
+ exchangeBaseUrl: string;
+
+ /**
+ * The coin is currently suspended, and will not be used for payments.
+ */
+ suspended: boolean;
+
+ /**
+ * Blinding key used when withdrawing the coin.
+ * Potentionally sed again during payback.
+ */
+ blindingKey: string;
+
+ /**
+ * Status of the coin.
+ */
+ status: CoinStatus;
+}
+
+export const enum ProposalStatus {
+ /**
+ * Not downloaded yet.
+ */
+ DOWNLOADING = "downloading",
+ /**
+ * Proposal downloaded, but the user needs to accept/reject it.
+ */
+ PROPOSED = "proposed",
+ /**
+ * The user has accepted the proposal.
+ */
+ ACCEPTED = "accepted",
+ /**
+ * The user has rejected the proposal.
+ */
+ REFUSED = "refused",
+ /**
+ * Downloaded proposal was detected as a re-purchase.
+ */
+ REPURCHASE = "repurchase",
+}
+
+export interface ProposalDownload {
+ /**
+ * The contract that was offered by the merchant.
+ */
+ contractTermsRaw: string;
+
+ contractData: WalletContractData;
+}
+
+/**
+ * Record for a downloaded order, stored in the wallet's database.
+ */
+export interface ProposalRecord {
+ orderId: string;
+
+ merchantBaseUrl: string;
+
+ /**
+ * Downloaded data from the merchant.
+ */
+ download: ProposalDownload | undefined;
+
+ /**
+ * Unique ID when the order is stored in the wallet DB.
+ */
+ proposalId: string;
+
+ /**
+ * Timestamp (in ms) of when the record
+ * was created.
+ */
+ timestamp: Timestamp;
+
+ /**
+ * Private key for the nonce.
+ */
+ noncePriv: string;
+
+ /**
+ * Public key for the nonce.
+ */
+ noncePub: string;
+
+ claimToken: string | undefined;
+
+ proposalStatus: ProposalStatus;
+
+ repurchaseProposalId: string | undefined;
+
+ /**
+ * Session ID we got when downloading the contract.
+ */
+ downloadSessionId?: string;
+
+ /**
+ * Retry info, even present when the operation isn't active to allow indexing
+ * on the next retry timestamp.
+ */
+ retryInfo: RetryInfo;
+
+ lastError: OperationErrorDetails | undefined;
+}
+
+/**
+ * Status of a tip we got from a merchant.
+ */
+export interface TipRecord {
+ lastError: OperationErrorDetails | undefined;
+
+ /**
+ * Has the user accepted the tip? Only after the tip has been accepted coins
+ * withdrawn from the tip may be used.
+ */
+ acceptedTimestamp: Timestamp | undefined;
+
+ /**
+ * Has the user rejected the tip?
+ */
+ rejectedTimestamp: Timestamp | undefined;
+
+ /**
+ * Have we picked up the tip record from the merchant already?
+ */
+ pickedUp: boolean;
+
+ /**
+ * The tipped amount.
+ */
+ amount: AmountJson;
+
+ totalFees: AmountJson;
+
+ /**
+ * Timestamp, the tip can't be picked up anymore after this deadline.
+ */
+ deadline: Timestamp;
+
+ /**
+ * The exchange that will sign our coins, chosen by the merchant.
+ */
+ exchangeUrl: string;
+
+ /**
+ * Base URL of the merchant that is giving us the tip.
+ */
+ merchantBaseUrl: string;
+
+ /**
+ * Planchets, the members included in TipPlanchetDetail will be sent to the
+ * merchant.
+ */
+ planchets?: TipPlanchet[];
+
+ denomsSel: DenomSelectionState;
+
+ /**
+ * Response if the merchant responded,
+ * undefined otherwise.
+ */
+ response?: TipResponse[];
+
+ /**
+ * Tip ID chosen by the wallet.
+ */
+ tipId: string;
+
+ /**
+ * The merchant's identifier for this tip.
+ */
+ merchantTipId: string;
+
+ /**
+ * URL to go to once the tip has been accepted.
+ */
+ nextUrl?: string;
+
+ createdTimestamp: Timestamp;
+
+ /**
+ * Retry info, even present when the operation isn't active to allow indexing
+ * on the next retry timestamp.
+ */
+ retryInfo: RetryInfo;
+}
+
+export interface RefreshGroupRecord {
+ /**
+ * Retry info, even present when the operation isn't active to allow indexing
+ * on the next retry timestamp.
+ */
+ retryInfo: RetryInfo;
+
+ lastError: OperationErrorDetails | undefined;
+
+ lastErrorPerCoin: { [coinIndex: number]: OperationErrorDetails };
+
+ refreshGroupId: string;
+
+ reason: RefreshReason;
+
+ oldCoinPubs: string[];
+
+ refreshSessionPerCoin: (RefreshSessionRecord | undefined)[];
+
+ /**
+ * Flag for each coin whether refreshing finished.
+ * If a coin can't be refreshed (remaining value too small),
+ * it will be marked as finished, but no refresh session will
+ * be created.
+ */
+ finishedPerCoin: boolean[];
+
+ /**
+ * Timestamp when the refresh session finished.
+ */
+ timestampFinished: Timestamp | undefined;
+}
+
+/**
+ * Ongoing refresh
+ */
+export interface RefreshSessionRecord {
+ lastError: OperationErrorDetails | undefined;
+
+ /**
+ * Public key that's being melted in this session.
+ */
+ meltCoinPub: string;
+
+ /**
+ * How much of the coin's value is melted away
+ * with this refresh session?
+ */
+ amountRefreshInput: AmountJson;
+
+ /**
+ * Sum of the value of denominations we want
+ * to withdraw in this session, without fees.
+ */
+ amountRefreshOutput: AmountJson;
+
+ /**
+ * Signature to confirm the melting.
+ */
+ confirmSig: string;
+
+ /**
+ * Hased denominations of the newly requested coins.
+ */
+ newDenomHashes: string[];
+
+ /**
+ * Denominations of the newly requested coins.
+ */
+ newDenoms: string[];
+
+ /**
+ * Planchets for each cut-and-choose instance.
+ */
+ planchetsForGammas: RefreshPlanchetRecord[][];
+
+ /**
+ * The transfer keys, kappa of them.
+ */
+ transferPubs: string[];
+
+ /**
+ * Private keys for the transfer public keys.
+ */
+ transferPrivs: string[];
+
+ /**
+ * The no-reveal-index after we've done the melting.
+ */
+ norevealIndex?: number;
+
+ /**
+ * Hash of the session.
+ */
+ hash: string;
+
+ /**
+ * Timestamp when the refresh session finished.
+ */
+ finishedTimestamp: Timestamp | undefined;
+
+ /**
+ * When has this refresh session been created?
+ */
+ timestampCreated: Timestamp;
+
+ /**
+ * Base URL for the exchange we're doing the refresh with.
+ */
+ exchangeBaseUrl: string;
+}
+
+/**
+ * Tipping planchet stored in the database.
+ */
+export interface TipPlanchet {
+ blindingKey: string;
+ coinEv: string;
+ coinPriv: string;
+ coinPub: string;
+ coinValue: AmountJson;
+ denomPubHash: string;
+ denomPub: string;
+}
+
+/**
+ * Wire fee for one wire method as stored in the
+ * wallet's database.
+ */
+export interface WireFee {
+ /**
+ * Fee for wire transfers.
+ */
+ wireFee: AmountJson;
+
+ /**
+ * Fees to close and refund a reserve.
+ */
+ closingFee: AmountJson;
+
+ /**
+ * Start date of the fee.
+ */
+ startStamp: Timestamp;
+
+ /**
+ * End date of the fee.
+ */
+ endStamp: Timestamp;
+
+ /**
+ * Signature made by the exchange master key.
+ */
+ sig: string;
+}
+
+/**
+ * Record to store information about a refund event.
+ *
+ * All information about a refund is stored with the purchase,
+ * this event is just for the history.
+ *
+ * The event is only present for completed refunds.
+ */
+export interface RefundEventRecord {
+ timestamp: Timestamp;
+ merchantExecutionTimestamp: Timestamp;
+ refundGroupId: string;
+ proposalId: string;
+}
+
+export const enum RefundState {
+ Failed = "failed",
+ Applied = "applied",
+ Pending = "pending",
+}
+
+/**
+ * State of one refund from the merchant, maintained by the wallet.
+ */
+export type WalletRefundItem =
+ | WalletRefundFailedItem
+ | WalletRefundPendingItem
+ | WalletRefundAppliedItem;
+
+export interface WalletRefundItemCommon {
+ executionTime: Timestamp;
+ refundAmount: AmountJson;
+ refundFee: AmountJson;
+
+ /**
+ * Upper bound on the refresh cost incurred by
+ * applying this refund.
+ *
+ * Might be lower in practice when two refunds on the same
+ * coin are refreshed in the same refresh operation.
+ */
+ totalRefreshCostBound: AmountJson;
+}
+
+/**
+ * Failed refund, either because the merchant did
+ * something wrong or it expired.
+ */
+export interface WalletRefundFailedItem extends WalletRefundItemCommon {
+ type: RefundState.Failed;
+}
+
+export interface WalletRefundPendingItem extends WalletRefundItemCommon {
+ type: RefundState.Pending;
+}
+
+export interface WalletRefundAppliedItem extends WalletRefundItemCommon {
+ type: RefundState.Applied;
+}
+
+export const enum RefundReason {
+ /**
+ * Normal refund given by the merchant.
+ */
+ NormalRefund = "normal-refund",
+ /**
+ * Refund from an aborted payment.
+ */
+ AbortRefund = "abort-pay-refund",
+}
+
+/**
+ * Record stored for every time we successfully submitted
+ * a payment to the merchant (both first time and re-play).
+ */
+export interface PayEventRecord {
+ proposalId: string;
+ sessionId: string | undefined;
+ isReplay: boolean;
+ timestamp: Timestamp;
+}
+
+export interface ExchangeUpdatedEventRecord {
+ exchangeBaseUrl: string;
+ timestamp: Timestamp;
+}
+
+export interface ReserveUpdatedEventRecord {
+ amountReserveBalance: string;
+ amountExpected: string;
+ reservePub: string;
+ timestamp: Timestamp;
+ reserveUpdateId: string;
+ newHistoryTransactions: ReserveTransaction[];
+}
+
+export interface AllowedAuditorInfo {
+ auditorBaseUrl: string;
+ auditorPub: string;
+}
+
+export interface AllowedExchangeInfo {
+ exchangeBaseUrl: string;
+ exchangePub: string;
+}
+
+/**
+ * Data extracted from the contract terms that is relevant for payment
+ * processing in the wallet.
+ */
+export interface WalletContractData {
+ products?: Product[];
+ summaryI18n: { [lang_tag: string]: string } | undefined;
+ fulfillmentUrl: string;
+ contractTermsHash: string;
+ merchantSig: string;
+ merchantPub: string;
+ merchant: MerchantInfo;
+ amount: AmountJson;
+ orderId: string;
+ merchantBaseUrl: string;
+ summary: string;
+ autoRefund: Duration | undefined;
+ maxWireFee: AmountJson;
+ wireFeeAmortization: number;
+ payDeadline: Timestamp;
+ refundDeadline: Timestamp;
+ allowedAuditors: AllowedAuditorInfo[];
+ allowedExchanges: AllowedExchangeInfo[];
+ timestamp: Timestamp;
+ wireMethod: string;
+ wireInfoHash: string;
+ maxDepositFee: AmountJson;
+}
+
+/**
+ * Record that stores status information about one purchase, starting from when
+ * the customer accepts a proposal. Includes refund status if applicable.
+ */
+export interface PurchaseRecord {
+ /**
+ * Proposal ID for this purchase. Uniquely identifies the
+ * purchase and the proposal.
+ */
+ proposalId: string;
+
+ /**
+ * Contract terms we got from the merchant.
+ */
+ contractTermsRaw: string;
+
+ contractData: WalletContractData;
+
+ /**
+ * Deposit permissions, available once the user has accepted the payment.
+ */
+ coinDepositPermissions: CoinDepositPermission[];
+
+ payCoinSelection: PayCoinSelection;
+
+ payCostInfo: PayCostInfo;
+
+ /**
+ * Timestamp of the first time that sending a payment to the merchant
+ * for this purchase was successful.
+ */
+ timestampFirstSuccessfulPay: Timestamp | undefined;
+
+ /**
+ * When was the purchase made?
+ * Refers to the time that the user accepted.
+ */
+ timestampAccept: Timestamp;
+
+ /**
+ * Pending refunds for the purchase. A refund is pending
+ * when the merchant reports a transient error from the exchange.
+ */
+ refunds: { [refundKey: string]: WalletRefundItem };
+
+ /**
+ * When was the last refund made?
+ * Set to 0 if no refund was made on the purchase.
+ */
+ timestampLastRefundStatus: Timestamp | undefined;
+
+ /**
+ * Last session signature that we submitted to /pay (if any).
+ */
+ lastSessionId: string | undefined;
+
+ /**
+ * Set for the first payment, or on re-plays.
+ */
+ paymentSubmitPending: boolean;
+
+ /**
+ * Do we need to query the merchant for the refund status
+ * of the payment?
+ */
+ refundStatusRequested: boolean;
+
+ /**
+ * An abort (with refund) was requested for this (incomplete!) purchase.
+ */
+ abortRequested: boolean;
+
+ /**
+ * The abort (with refund) was completed for this (incomplete!) purchase.
+ */
+ abortDone: boolean;
+
+ payRetryInfo: RetryInfo;
+
+ lastPayError: OperationErrorDetails | undefined;
+
+ /**
+ * Retry information for querying the refund status with the merchant.
+ */
+ refundStatusRetryInfo: RetryInfo;
+
+ /**
+ * Last error (or undefined) for querying the refund status with the merchant.
+ */
+ lastRefundStatusError: OperationErrorDetails | undefined;
+
+ /**
+ * Continue querying the refund status until this deadline has expired.
+ */
+ autoRefundDeadline: Timestamp | undefined;
+}
+
+/**
+ * Information about wire information for bank accounts we withdrew coins from.
+ */
+export interface SenderWireRecord {
+ paytoUri: string;
+}
+
+/**
+ * Configuration key/value entries to configure
+ * the wallet.
+ */
+export interface ConfigRecord {
+ key: string;
+ value: any;
+}
+
+/**
+ * Coin that we're depositing ourselves.
+ */
+export interface DepositCoin {
+ coinPaySig: CoinDepositPermission;
+
+ /**
+ * Undefined if coin not deposited, otherwise signature
+ * from the exchange confirming the deposit.
+ */
+ depositedSig?: string;
+}
+
+/**
+ * Record stored in the wallet's database when the user sends coins back to
+ * their own bank account. Stores the status of coins that are deposited to
+ * the wallet itself, where the wallet acts as a "merchant" for the customer.
+ */
+export interface CoinsReturnRecord {
+ contractTermsRaw: string;
+
+ contractData: WalletContractData;
+
+ /**
+ * Private key where corresponding
+ * public key is used in the contract terms
+ * as merchant pub.
+ */
+ merchantPriv: string;
+
+ coins: DepositCoin[];
+
+ /**
+ * Exchange base URL to deposit coins at.
+ */
+ exchange: string;
+
+ /**
+ * Our own wire information for the deposit.
+ */
+ wire: any;
+}
+
+export const enum WithdrawalSourceType {
+ Tip = "tip",
+ Reserve = "reserve",
+}
+
+export interface WithdrawalSourceTip {
+ type: WithdrawalSourceType.Tip;
+ tipId: string;
+}
+
+export interface WithdrawalSourceReserve {
+ type: WithdrawalSourceType.Reserve;
+ reservePub: string;
+}
+
+export type WithdrawalSource = WithdrawalSourceTip | WithdrawalSourceReserve;
+
+export interface DenominationSelectionInfo {
+ totalCoinValue: AmountJson;
+ totalWithdrawCost: AmountJson;
+ selectedDenoms: {
+ /**
+ * How many times do we withdraw this denomination?
+ */
+ count: number;
+ denom: DenominationRecord;
+ }[];
+}
+
+export interface DenomSelectionState {
+ totalCoinValue: AmountJson;
+ totalWithdrawCost: AmountJson;
+ selectedDenoms: {
+ denomPubHash: string;
+ count: number;
+ }[];
+}
+
+/**
+ * Group of withdrawal operations that need to be executed.
+ * (Either for a normal withdrawal or from a tip.)
+ *
+ * The withdrawal group record is only created after we know
+ * the coin selection we want to withdraw.
+ */
+export interface WithdrawalGroupRecord {
+ withdrawalGroupId: string;
+
+ /**
+ * Withdrawal source. Fields that don't apply to the respective
+ * withdrawal source type must be null (i.e. can't be absent),
+ * otherwise the IndexedDB indexing won't like us.
+ */
+ source: WithdrawalSource;
+
+ exchangeBaseUrl: string;
+
+ /**
+ * When was the withdrawal operation started started?
+ * Timestamp in milliseconds.
+ */
+ timestampStart: Timestamp;
+
+ /**
+ * When was the withdrawal operation completed?
+ */
+ timestampFinish?: Timestamp;
+
+ /**
+ * Amount including fees (i.e. the amount subtracted from the
+ * reserve to withdraw all coins in this withdrawal session).
+ */
+ rawWithdrawalAmount: AmountJson;
+
+ denomsSel: DenomSelectionState;
+
+ /**
+ * Retry info, always present even on completed operations so that indexing works.
+ */
+ retryInfo: RetryInfo;
+
+ /**
+ * Last error per coin/planchet, or undefined if no error occured for
+ * the coin/planchet.
+ */
+ lastErrorPerCoin: { [coinIndex: number]: OperationErrorDetails };
+
+ lastError: OperationErrorDetails | undefined;
+}
+
+export interface BankWithdrawUriRecord {
+ /**
+ * The withdraw URI we got from the bank.
+ */
+ talerWithdrawUri: string;
+
+ /**
+ * Reserve that was created for the withdraw URI.
+ */
+ reservePub: string;
+}
+
+/**
+ * Status of recoup operations that were grouped together.
+ *
+ * The remaining amount of involved coins should be set to zero
+ * in the same transaction that inserts the RecoupGroupRecord.
+ */
+export interface RecoupGroupRecord {
+ /**
+ * Unique identifier for the recoup group record.
+ */
+ recoupGroupId: string;
+
+ timestampStarted: Timestamp;
+
+ timestampFinished: Timestamp | undefined;
+
+ /**
+ * Public keys that identify the coins being recouped
+ * as part of this session.
+ *
+ * (Structured like this to enable multiEntry indexing in IndexedDB.)
+ */
+ coinPubs: string[];
+
+ /**
+ * Array of flags to indicate whether the recoup finished on each individual coin.
+ */
+ recoupFinishedPerCoin: boolean[];
+
+ /**
+ * We store old amount (i.e. before recoup) of recouped coins here,
+ * as the balance of a recouped coin is set to zero when the
+ * recoup group is created.
+ */
+ oldAmountPerCoin: AmountJson[];
+
+ /**
+ * Public keys of coins that should be scheduled for refreshing
+ * after all individual recoups are done.
+ */
+ scheduleRefreshCoins: string[];
+
+ /**
+ * Retry info.
+ */
+ retryInfo: RetryInfo;
+
+ /**
+ * Last error that occured, if any.
+ */
+ lastError: OperationErrorDetails | undefined;
+}
+
+export const enum ImportPayloadType {
+ CoreSchema = "core-schema",
+}
+
+/**
+ * Record to keep track of data imported into the wallet.
+ */
+export class WalletImportRecord {
+ /**
+ * Unique ID to reference this import record.
+ */
+ walletImportId: string;
+
+ /**
+ * When was the data imported?
+ */
+ timestampImportStarted: Timestamp;
+
+ timestampImportFinished: Timestamp | undefined;
+
+ payloadType: ImportPayloadType;
+
+ /**
+ * The actual data to import.
+ */
+ payload: any;
+}
+
+/* tslint:disable:completed-docs */
+
+class ExchangesStore extends Store<ExchangeRecord> {
+ constructor() {
+ super("exchanges", { keyPath: "baseUrl" });
+ }
+}
+
+class CoinsStore extends Store<CoinRecord> {
+ constructor() {
+ super("coins", { keyPath: "coinPub" });
+ }
+
+ exchangeBaseUrlIndex = new Index<string, CoinRecord>(
+ this,
+ "exchangeBaseUrl",
+ "exchangeBaseUrl",
+ );
+ denomPubIndex = new Index<string, CoinRecord>(
+ this,
+ "denomPubIndex",
+ "denomPub",
+ );
+ denomPubHashIndex = new Index<string, CoinRecord>(
+ this,
+ "denomPubHashIndex",
+ "denomPubHash",
+ );
+}
+
+class ProposalsStore extends Store<ProposalRecord> {
+ constructor() {
+ super("proposals", { keyPath: "proposalId" });
+ }
+ urlAndOrderIdIndex = new Index<string, ProposalRecord>(this, "urlIndex", [
+ "merchantBaseUrl",
+ "orderId",
+ ]);
+}
+
+class PurchasesStore extends Store<PurchaseRecord> {
+ constructor() {
+ super("purchases", { keyPath: "proposalId" });
+ }
+
+ fulfillmentUrlIndex = new Index<string, PurchaseRecord>(
+ this,
+ "fulfillmentUrlIndex",
+ "contractData.fulfillmentUrl",
+ );
+ orderIdIndex = new Index<string, PurchaseRecord>(this, "orderIdIndex", [
+ "contractData.merchantBaseUrl",
+ "contractData.orderId",
+ ]);
+}
+
+class DenominationsStore extends Store<DenominationRecord> {
+ constructor() {
+ // cast needed because of bug in type annotations
+ super("denominations", {
+ keyPath: (["exchangeBaseUrl", "denomPub"] as any) as idbtypes.IDBKeyPath,
+ });
+ }
+
+ denomPubHashIndex = new Index<string, DenominationRecord>(
+ this,
+ "denomPubHashIndex",
+ "denomPubHash",
+ );
+ exchangeBaseUrlIndex = new Index<string, DenominationRecord>(
+ this,
+ "exchangeBaseUrlIndex",
+ "exchangeBaseUrl",
+ );
+ denomPubIndex = new Index<string, DenominationRecord>(
+ this,
+ "denomPubIndex",
+ "denomPub",
+ );
+}
+
+class CurrenciesStore extends Store<CurrencyRecord> {
+ constructor() {
+ super("currencies", { keyPath: "name" });
+ }
+}
+
+class ConfigStore extends Store<ConfigRecord> {
+ constructor() {
+ super("config", { keyPath: "key" });
+ }
+}
+
+class ReservesStore extends Store<ReserveRecord> {
+ constructor() {
+ super("reserves", { keyPath: "reservePub" });
+ }
+}
+
+class ReserveHistoryStore extends Store<ReserveHistoryRecord> {
+ constructor() {
+ super("reserveHistory", { keyPath: "reservePub" });
+ }
+}
+
+class TipsStore extends Store<TipRecord> {
+ constructor() {
+ super("tips", { keyPath: "tipId" });
+ }
+}
+
+class SenderWiresStore extends Store<SenderWireRecord> {
+ constructor() {
+ super("senderWires", { keyPath: "paytoUri" });
+ }
+}
+
+class WithdrawalGroupsStore extends Store<WithdrawalGroupRecord> {
+ constructor() {
+ super("withdrawals", { keyPath: "withdrawalGroupId" });
+ }
+}
+
+class PlanchetsStore extends Store<PlanchetRecord> {
+ constructor() {
+ super("planchets", { keyPath: "coinPub" });
+ }
+ byGroupAndIndex = new Index<string, PlanchetRecord>(
+ this,
+ "withdrawalGroupAndCoinIdxIndex",
+ ["withdrawalGroupId", "coinIdx"],
+ );
+ byGroup = new Index<string, PlanchetRecord>(
+ this,
+ "withdrawalGroupIndex",
+ "withdrawalGroupId",
+ );
+}
+
+class RefundEventsStore extends Store<RefundEventRecord> {
+ constructor() {
+ super("refundEvents", { keyPath: "refundGroupId" });
+ }
+}
+
+class PayEventsStore extends Store<PayEventRecord> {
+ constructor() {
+ super("payEvents", { keyPath: "proposalId" });
+ }
+}
+
+class ExchangeUpdatedEventsStore extends Store<ExchangeUpdatedEventRecord> {
+ constructor() {
+ super("exchangeUpdatedEvents", { keyPath: "exchangeBaseUrl" });
+ }
+}
+
+class ReserveUpdatedEventsStore extends Store<ReserveUpdatedEventRecord> {
+ constructor() {
+ super("reserveUpdatedEvents", { keyPath: "reservePub" });
+ }
+}
+
+class BankWithdrawUrisStore extends Store<BankWithdrawUriRecord> {
+ constructor() {
+ super("bankWithdrawUris", { keyPath: "talerWithdrawUri" });
+ }
+}
+
+class WalletImportsStore extends Store<WalletImportRecord> {
+ constructor() {
+ super("walletImports", { keyPath: "walletImportId" });
+ }
+}
+
+/**
+ * The stores and indices for the wallet database.
+ */
+
+export const Stores = {
+ coins: new CoinsStore(),
+ coinsReturns: new Store<CoinsReturnRecord>("coinsReturns", {
+ keyPath: "contractTermsHash",
+ }),
+ config: new ConfigStore(),
+ currencies: new CurrenciesStore(),
+ denominations: new DenominationsStore(),
+ exchanges: new ExchangesStore(),
+ proposals: new ProposalsStore(),
+ refreshGroups: new Store<RefreshGroupRecord>("refreshGroups", {
+ keyPath: "refreshGroupId",
+ }),
+ recoupGroups: new Store<RecoupGroupRecord>("recoupGroups", {
+ keyPath: "recoupGroupId",
+ }),
+ reserves: new ReservesStore(),
+ reserveHistory: new ReserveHistoryStore(),
+ purchases: new PurchasesStore(),
+ tips: new TipsStore(),
+ senderWires: new SenderWiresStore(),
+ withdrawalGroups: new WithdrawalGroupsStore(),
+ planchets: new PlanchetsStore(),
+ bankWithdrawUris: new BankWithdrawUrisStore(),
+ refundEvents: new RefundEventsStore(),
+ payEvents: new PayEventsStore(),
+ reserveUpdatedEvents: new ReserveUpdatedEventsStore(),
+ exchangeUpdatedEvents: new ExchangeUpdatedEventsStore(),
+ walletImports: new WalletImportsStore(),
+};
+
+/* tslint:enable:completed-docs */
diff --git a/packages/taler-wallet-core/src/types/notifications.d.ts.map b/packages/taler-wallet-core/src/types/notifications.d.ts.map
new file mode 100644
index 000000000..f1b3318d5
--- /dev/null
+++ b/packages/taler-wallet-core/src/types/notifications.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"notifications.d.ts","sourceRoot":"","sources":["notifications.ts"],"names":[],"mappings":"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"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/types/notifications.ts b/packages/taler-wallet-core/src/types/notifications.ts
new file mode 100644
index 000000000..945b86eea
--- /dev/null
+++ b/packages/taler-wallet-core/src/types/notifications.ts
@@ -0,0 +1,255 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 GNUnet e.V.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Type and schema definitions for notifications from the wallet to clients
+ * of the wallet.
+ */
+
+/**
+ * Imports.
+ */
+import { OperationErrorDetails } from "./walletTypes";
+import { WithdrawalSource } from "./dbTypes";
+
+export const enum NotificationType {
+ CoinWithdrawn = "coin-withdrawn",
+ ProposalAccepted = "proposal-accepted",
+ ProposalDownloaded = "proposal-downloaded",
+ RefundsSubmitted = "refunds-submitted",
+ RecoupStarted = "recoup-started",
+ RecoupFinished = "recoup-finished",
+ RefreshRevealed = "refresh-revealed",
+ RefreshMelted = "refresh-melted",
+ RefreshStarted = "refresh-started",
+ RefreshUnwarranted = "refresh-unwarranted",
+ ReserveUpdated = "reserve-updated",
+ ReserveConfirmed = "reserve-confirmed",
+ ReserveCreated = "reserve-created",
+ WithdrawGroupCreated = "withdraw-group-created",
+ WithdrawGroupFinished = "withdraw-group-finished",
+ WaitingForRetry = "waiting-for-retry",
+ RefundStarted = "refund-started",
+ RefundQueried = "refund-queried",
+ RefundFinished = "refund-finished",
+ ExchangeOperationError = "exchange-operation-error",
+ RefreshOperationError = "refresh-operation-error",
+ RecoupOperationError = "recoup-operation-error",
+ RefundApplyOperationError = "refund-apply-error",
+ RefundStatusOperationError = "refund-status-error",
+ ProposalOperationError = "proposal-error",
+ TipOperationError = "tip-error",
+ PayOperationError = "pay-error",
+ WithdrawOperationError = "withdraw-error",
+ ReserveNotYetFound = "reserve-not-yet-found",
+ ReserveOperationError = "reserve-error",
+ InternalError = "internal-error",
+ PendingOperationProcessed = "pending-operation-processed",
+ ProposalRefused = "proposal-refused",
+ ReserveRegisteredWithBank = "reserve-registered-with-bank",
+}
+
+export interface ProposalAcceptedNotification {
+ type: NotificationType.ProposalAccepted;
+ proposalId: string;
+}
+
+export interface InternalErrorNotification {
+ type: NotificationType.InternalError;
+ message: string;
+ exception: any;
+}
+
+export interface ReserveNotYetFoundNotification {
+ type: NotificationType.ReserveNotYetFound;
+ reservePub: string;
+}
+
+export interface CoinWithdrawnNotification {
+ type: NotificationType.CoinWithdrawn;
+}
+
+export interface RefundStartedNotification {
+ type: NotificationType.RefundStarted;
+}
+
+export interface RefundQueriedNotification {
+ type: NotificationType.RefundQueried;
+}
+
+export interface ProposalDownloadedNotification {
+ type: NotificationType.ProposalDownloaded;
+ proposalId: string;
+}
+
+export interface RefundsSubmittedNotification {
+ type: NotificationType.RefundsSubmitted;
+ proposalId: string;
+}
+
+export interface RecoupStartedNotification {
+ type: NotificationType.RecoupStarted;
+}
+
+export interface RecoupFinishedNotification {
+ type: NotificationType.RecoupFinished;
+}
+
+export interface RefreshMeltedNotification {
+ type: NotificationType.RefreshMelted;
+}
+
+export interface RefreshRevealedNotification {
+ type: NotificationType.RefreshRevealed;
+}
+
+export interface RefreshStartedNotification {
+ type: NotificationType.RefreshStarted;
+}
+
+export interface RefreshRefusedNotification {
+ type: NotificationType.RefreshUnwarranted;
+}
+
+export interface ReserveUpdatedNotification {
+ type: NotificationType.ReserveUpdated;
+}
+
+export interface ReserveConfirmedNotification {
+ type: NotificationType.ReserveConfirmed;
+}
+
+export interface WithdrawalGroupCreatedNotification {
+ type: NotificationType.WithdrawGroupCreated;
+ withdrawalGroupId: string;
+}
+
+export interface WithdrawalGroupFinishedNotification {
+ type: NotificationType.WithdrawGroupFinished;
+ withdrawalSource: WithdrawalSource;
+}
+
+export interface WaitingForRetryNotification {
+ type: NotificationType.WaitingForRetry;
+ numPending: number;
+ numGivingLiveness: number;
+}
+
+export interface RefundFinishedNotification {
+ type: NotificationType.RefundFinished;
+}
+
+export interface ExchangeOperationErrorNotification {
+ type: NotificationType.ExchangeOperationError;
+ error: OperationErrorDetails;
+}
+
+export interface RefreshOperationErrorNotification {
+ type: NotificationType.RefreshOperationError;
+ error: OperationErrorDetails;
+}
+
+export interface RefundStatusOperationErrorNotification {
+ type: NotificationType.RefundStatusOperationError;
+ error: OperationErrorDetails;
+}
+
+export interface RefundApplyOperationErrorNotification {
+ type: NotificationType.RefundApplyOperationError;
+ error: OperationErrorDetails;
+}
+
+export interface PayOperationErrorNotification {
+ type: NotificationType.PayOperationError;
+ error: OperationErrorDetails;
+}
+
+export interface ProposalOperationErrorNotification {
+ type: NotificationType.ProposalOperationError;
+ error: OperationErrorDetails;
+}
+
+export interface TipOperationErrorNotification {
+ type: NotificationType.TipOperationError;
+}
+
+export interface WithdrawOperationErrorNotification {
+ type: NotificationType.WithdrawOperationError;
+ error: OperationErrorDetails;
+}
+
+export interface RecoupOperationErrorNotification {
+ type: NotificationType.RecoupOperationError;
+ error: OperationErrorDetails;
+}
+
+export interface ReserveOperationErrorNotification {
+ type: NotificationType.ReserveOperationError;
+ error: OperationErrorDetails;
+}
+
+export interface ReserveCreatedNotification {
+ type: NotificationType.ReserveCreated;
+ reservePub: string;
+}
+
+export interface PendingOperationProcessedNotification {
+ type: NotificationType.PendingOperationProcessed;
+}
+
+export interface ProposalRefusedNotification {
+ type: NotificationType.ProposalRefused;
+}
+
+export interface ReserveRegisteredWithBankNotification {
+ type: NotificationType.ReserveRegisteredWithBank;
+}
+
+export type WalletNotification =
+ | WithdrawOperationErrorNotification
+ | ReserveOperationErrorNotification
+ | ExchangeOperationErrorNotification
+ | RefreshOperationErrorNotification
+ | RefundStatusOperationErrorNotification
+ | RefundApplyOperationErrorNotification
+ | ProposalOperationErrorNotification
+ | PayOperationErrorNotification
+ | TipOperationErrorNotification
+ | ProposalAcceptedNotification
+ | ProposalDownloadedNotification
+ | RefundsSubmittedNotification
+ | RecoupStartedNotification
+ | RecoupFinishedNotification
+ | RefreshMeltedNotification
+ | RefreshRevealedNotification
+ | RefreshStartedNotification
+ | RefreshRefusedNotification
+ | ReserveUpdatedNotification
+ | ReserveCreatedNotification
+ | ReserveConfirmedNotification
+ | WithdrawalGroupFinishedNotification
+ | WaitingForRetryNotification
+ | RefundStartedNotification
+ | RefundFinishedNotification
+ | RefundQueriedNotification
+ | WithdrawalGroupCreatedNotification
+ | CoinWithdrawnNotification
+ | RecoupOperationErrorNotification
+ | InternalErrorNotification
+ | PendingOperationProcessedNotification
+ | ProposalRefusedNotification
+ | ReserveRegisteredWithBankNotification
+ | ReserveNotYetFoundNotification;
diff --git a/packages/taler-wallet-core/src/types/pending.d.ts.map b/packages/taler-wallet-core/src/types/pending.d.ts.map
new file mode 100644
index 000000000..2f88198fa
--- /dev/null
+++ b/packages/taler-wallet-core/src/types/pending.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"pending.d.ts","sourceRoot":"","sources":["pending.ts"],"names":[],"mappings":"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"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/types/pending.ts b/packages/taler-wallet-core/src/types/pending.ts
new file mode 100644
index 000000000..85f7585c5
--- /dev/null
+++ b/packages/taler-wallet-core/src/types/pending.ts
@@ -0,0 +1,258 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 GNUnet e.V.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Type and schema definitions for pending operations in the wallet.
+ */
+
+/**
+ * Imports.
+ */
+import { OperationErrorDetails, BalancesResponse } from "./walletTypes";
+import { WithdrawalSource, RetryInfo, ReserveRecordStatus } from "./dbTypes";
+import { Timestamp, Duration } from "../util/time";
+
+export const enum PendingOperationType {
+ Bug = "bug",
+ ExchangeUpdate = "exchange-update",
+ Pay = "pay",
+ ProposalChoice = "proposal-choice",
+ ProposalDownload = "proposal-download",
+ Refresh = "refresh",
+ Reserve = "reserve",
+ Recoup = "recoup",
+ RefundQuery = "refund-query",
+ TipChoice = "tip-choice",
+ TipPickup = "tip-pickup",
+ Withdraw = "withdraw",
+}
+
+/**
+ * Information about a pending operation.
+ */
+export type PendingOperationInfo = PendingOperationInfoCommon &
+ (
+ | PendingBugOperation
+ | PendingExchangeUpdateOperation
+ | PendingPayOperation
+ | PendingProposalChoiceOperation
+ | PendingProposalDownloadOperation
+ | PendingRefreshOperation
+ | PendingRefundQueryOperation
+ | PendingReserveOperation
+ | PendingTipChoiceOperation
+ | PendingTipPickupOperation
+ | PendingWithdrawOperation
+ | PendingRecoupOperation
+ );
+
+/**
+ * The wallet is currently updating information about an exchange.
+ */
+export interface PendingExchangeUpdateOperation {
+ type: PendingOperationType.ExchangeUpdate;
+ stage: ExchangeUpdateOperationStage;
+ reason: string;
+ exchangeBaseUrl: string;
+ lastError: OperationErrorDetails | undefined;
+}
+
+/**
+ * Some interal error happened in the wallet. This pending operation
+ * should *only* be reported for problems in the wallet, not when
+ * a problem with a merchant/exchange/etc. occurs.
+ */
+export interface PendingBugOperation {
+ type: PendingOperationType.Bug;
+ message: string;
+ details: any;
+}
+
+/**
+ * Current state of an exchange update operation.
+ */
+export const enum ExchangeUpdateOperationStage {
+ FetchKeys = "fetch-keys",
+ FetchWire = "fetch-wire",
+ FinalizeUpdate = "finalize-update",
+}
+
+export const enum ReserveType {
+ /**
+ * Manually created.
+ */
+ Manual = "manual",
+ /**
+ * Withdrawn from a bank that has "tight" Taler integration
+ */
+ TalerBankWithdraw = "taler-bank-withdraw",
+}
+
+/**
+ * Status of processing a reserve.
+ *
+ * Does *not* include the withdrawal operation that might result
+ * from this.
+ */
+export interface PendingReserveOperation {
+ type: PendingOperationType.Reserve;
+ retryInfo: RetryInfo | undefined;
+ stage: ReserveRecordStatus;
+ timestampCreated: Timestamp;
+ reserveType: ReserveType;
+ reservePub: string;
+ bankWithdrawConfirmUrl?: string;
+}
+
+/**
+ * Status of an ongoing withdrawal operation.
+ */
+export interface PendingRefreshOperation {
+ type: PendingOperationType.Refresh;
+ lastError?: OperationErrorDetails;
+ refreshGroupId: string;
+ finishedPerCoin: boolean[];
+ retryInfo: RetryInfo;
+}
+
+/**
+ * Status of downloading signed contract terms from a merchant.
+ */
+export interface PendingProposalDownloadOperation {
+ type: PendingOperationType.ProposalDownload;
+ merchantBaseUrl: string;
+ proposalTimestamp: Timestamp;
+ proposalId: string;
+ orderId: string;
+ lastError?: OperationErrorDetails;
+ retryInfo: RetryInfo;
+}
+
+/**
+ * User must choose whether to accept or reject the merchant's
+ * proposed contract terms.
+ */
+export interface PendingProposalChoiceOperation {
+ type: PendingOperationType.ProposalChoice;
+ merchantBaseUrl: string;
+ proposalTimestamp: Timestamp;
+ proposalId: string;
+}
+
+/**
+ * The wallet is picking up a tip that the user has accepted.
+ */
+export interface PendingTipPickupOperation {
+ type: PendingOperationType.TipPickup;
+ tipId: string;
+ merchantBaseUrl: string;
+ merchantTipId: string;
+}
+
+/**
+ * The wallet has been offered a tip, and the user now needs to
+ * decide whether to accept or reject the tip.
+ */
+export interface PendingTipChoiceOperation {
+ type: PendingOperationType.TipChoice;
+ tipId: string;
+ merchantBaseUrl: string;
+ merchantTipId: string;
+}
+
+/**
+ * The wallet is signing coins and then sending them to
+ * the merchant.
+ */
+export interface PendingPayOperation {
+ type: PendingOperationType.Pay;
+ proposalId: string;
+ isReplay: boolean;
+ retryInfo: RetryInfo;
+ lastError: OperationErrorDetails | undefined;
+}
+
+/**
+ * The wallet is querying the merchant about whether any refund
+ * permissions are available for a purchase.
+ */
+export interface PendingRefundQueryOperation {
+ type: PendingOperationType.RefundQuery;
+ proposalId: string;
+ retryInfo: RetryInfo;
+ lastError: OperationErrorDetails | undefined;
+}
+
+export interface PendingRecoupOperation {
+ type: PendingOperationType.Recoup;
+ recoupGroupId: string;
+ retryInfo: RetryInfo;
+ lastError: OperationErrorDetails | undefined;
+}
+
+/**
+ * Status of an ongoing withdrawal operation.
+ */
+export interface PendingWithdrawOperation {
+ type: PendingOperationType.Withdraw;
+ source: WithdrawalSource;
+ lastError: OperationErrorDetails | undefined;
+ withdrawalGroupId: string;
+ numCoinsWithdrawn: number;
+ numCoinsTotal: number;
+}
+
+/**
+ * Fields that are present in every pending operation.
+ */
+export interface PendingOperationInfoCommon {
+ /**
+ * Type of the pending operation.
+ */
+ type: PendingOperationType;
+
+ /**
+ * Set to true if the operation indicates that something is really in progress,
+ * as opposed to some regular scheduled operation or a permanent failure.
+ */
+ givesLifeness: boolean;
+}
+
+/**
+ * Response returned from the pending operations API.
+ */
+export interface PendingOperationsResponse {
+ /**
+ * List of pending operations.
+ */
+ pendingOperations: PendingOperationInfo[];
+
+ /**
+ * Current wallet balance, including pending balances.
+ */
+ walletBalance: BalancesResponse;
+
+ /**
+ * When is the next pending operation due to be re-tried?
+ */
+ nextRetryDelay: Duration;
+
+ /**
+ * Does this response only include pending operations that
+ * are due to be executed right now?
+ */
+ onlyDue: boolean;
+}
diff --git a/packages/taler-wallet-core/src/types/schemacore.ts b/packages/taler-wallet-core/src/types/schemacore.ts
new file mode 100644
index 000000000..820f68d18
--- /dev/null
+++ b/packages/taler-wallet-core/src/types/schemacore.ts
@@ -0,0 +1,58 @@
+/*
+ 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/>
+ */
+
+/**
+ * Core of the wallet's schema, used for painless export, import
+ * and schema migration.
+ *
+ * If this schema is extended, it must be extended in a completely
+ * backwards-compatible way.
+ */
+
+interface CoreCoin {
+ exchangeBaseUrl: string;
+ coinPub: string;
+ coinPriv: string;
+ amountRemaining: string;
+}
+
+interface CorePurchase {
+ noncePub: string;
+ noncePriv: string;
+ paySig: string;
+ contractTerms: any;
+}
+
+interface CoreReserve {
+ reservePub: string;
+ reservePriv: string;
+ exchangeBaseUrl: string;
+}
+
+interface SchemaCore {
+ coins: CoreCoin[];
+ purchases: CorePurchase[];
+
+ /**
+ * Schema version (of full schema) of wallet that exported the core schema.
+ */
+ versionExporter: number;
+
+ /**
+ * Schema version of the database that has been exported to the core schema
+ */
+ versionSourceDatabase: number;
+}
diff --git a/packages/taler-wallet-core/src/types/talerTypes.d.ts.map b/packages/taler-wallet-core/src/types/talerTypes.d.ts.map
new file mode 100644
index 000000000..0419ea14f
--- /dev/null
+++ b/packages/taler-wallet-core/src/types/talerTypes.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"talerTypes.d.ts","sourceRoot":"","sources":["talerTypes.ts"],"names":[],"mappings":"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"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/types/talerTypes.ts b/packages/taler-wallet-core/src/types/talerTypes.ts
new file mode 100644
index 000000000..acebbda95
--- /dev/null
+++ b/packages/taler-wallet-core/src/types/talerTypes.ts
@@ -0,0 +1,1272 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 GNUnet e.V.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Type and schema definitions and helpers for the core GNU Taler protocol.
+ *
+ * All types here should be "@Checkable".
+ *
+ * Even though the rest of the wallet uses camelCase for fields, use snake_case
+ * here, since that's the convention for the Taler JSON+HTTP API.
+ */
+
+/**
+ * Imports.
+ */
+
+import {
+ makeCodecForObject,
+ codecForString,
+ makeCodecForList,
+ makeCodecOptional,
+ codecForAny,
+ codecForNumber,
+ codecForBoolean,
+ makeCodecForMap,
+ Codec,
+ makeCodecForConstNumber,
+ makeCodecForUnion,
+ makeCodecForConstString,
+} from "../util/codec";
+import {
+ Timestamp,
+ codecForTimestamp,
+ Duration,
+ codecForDuration,
+} from "../util/time";
+import { ExchangeListItem } from "./walletTypes";
+
+/**
+ * Denomination as found in the /keys response from the exchange.
+ */
+export class Denomination {
+ /**
+ * Value of one coin of the denomination.
+ */
+ value: string;
+
+ /**
+ * Public signing key of the denomination.
+ */
+ denom_pub: string;
+
+ /**
+ * Fee for withdrawing.
+ */
+ fee_withdraw: string;
+
+ /**
+ * Fee for depositing.
+ */
+ fee_deposit: string;
+
+ /**
+ * Fee for refreshing.
+ */
+ fee_refresh: string;
+
+ /**
+ * Fee for refunding.
+ */
+ fee_refund: string;
+
+ /**
+ * Start date from which withdraw is allowed.
+ */
+ stamp_start: Timestamp;
+
+ /**
+ * End date for withdrawing.
+ */
+ stamp_expire_withdraw: Timestamp;
+
+ /**
+ * Expiration date after which the exchange can forget about
+ * the currency.
+ */
+ stamp_expire_legal: Timestamp;
+
+ /**
+ * Date after which the coins of this denomination can't be
+ * deposited anymore.
+ */
+ stamp_expire_deposit: Timestamp;
+
+ /**
+ * Signature over the denomination information by the exchange's master
+ * signing key.
+ */
+ master_sig: string;
+}
+
+/**
+ * Signature by the auditor that a particular denomination key is audited.
+ */
+export class AuditorDenomSig {
+ /**
+ * Denomination public key's hash.
+ */
+ denom_pub_h: string;
+
+ /**
+ * The signature.
+ */
+ auditor_sig: string;
+}
+
+/**
+ * Auditor information as given by the exchange in /keys.
+ */
+export class Auditor {
+ /**
+ * Auditor's public key.
+ */
+ auditor_pub: string;
+
+ /**
+ * Base URL of the auditor.
+ */
+ auditor_url: string;
+
+ /**
+ * List of signatures for denominations by the auditor.
+ */
+ denomination_keys: AuditorDenomSig[];
+}
+
+/**
+ * Request that we send to the exchange to get a payback.
+ */
+export interface RecoupRequest {
+ /**
+ * Hashed enomination public key of the coin we want to get
+ * paid back.
+ */
+ denom_pub_hash: string;
+
+ /**
+ * Signature over the coin public key by the denomination.
+ */
+ denom_sig: string;
+
+ /**
+ * Coin public key of the coin we want to refund.
+ */
+ coin_pub: string;
+
+ /**
+ * Blinding key that was used during withdraw,
+ * used to prove that we were actually withdrawing the coin.
+ */
+ coin_blind_key_secret: string;
+
+ /**
+ * Signature made by the coin, authorizing the payback.
+ */
+ coin_sig: string;
+
+ /**
+ * Was the coin refreshed (and thus the recoup should go to the old coin)?
+ */
+ refreshed: boolean;
+}
+
+/**
+ * Response that we get from the exchange for a payback request.
+ */
+export class RecoupConfirmation {
+ /**
+ * Public key of the reserve that will receive the payback.
+ */
+ reserve_pub?: string;
+
+ /**
+ * Public key of the old coin that will receive the recoup,
+ * provided if refreshed was true.
+ */
+ old_coin_pub?: string;
+}
+
+/**
+ * Deposit permission for a single coin.
+ */
+export interface CoinDepositPermission {
+ /**
+ * Signature by the coin.
+ */
+ coin_sig: string;
+ /**
+ * Public key of the coin being spend.
+ */
+ coin_pub: string;
+ /**
+ * Signature made by the denomination public key.
+ */
+ ub_sig: string;
+ /**
+ * The denomination public key associated with this coin.
+ */
+ h_denom: string;
+ /**
+ * The amount that is subtracted from this coin with this payment.
+ */
+ contribution: string;
+
+ /**
+ * URL of the exchange this coin was withdrawn from.
+ */
+ exchange_url: string;
+}
+
+/**
+ * Information about an exchange as stored inside a
+ * merchant's contract terms.
+ */
+export class ExchangeHandle {
+ /**
+ * Master public signing key of the exchange.
+ */
+ master_pub: string;
+
+ /**
+ * Base URL of the exchange.
+ */
+ url: string;
+}
+
+export class AuditorHandle {
+ /**
+ * Official name of the auditor.
+ */
+ name: string;
+
+ /**
+ * Master public signing key of the auditor.
+ */
+ master_pub: string;
+
+ /**
+ * Base URL of the auditor.
+ */
+ url: string;
+}
+
+export interface MerchantInfo {
+ name: string;
+ jurisdiction: string | undefined;
+ address: string | undefined;
+}
+
+export interface Tax {
+ // the name of the tax
+ name: string;
+
+ // amount paid in tax
+ tax: AmountString;
+}
+
+export interface Product {
+ // merchant-internal identifier for the product.
+ product_id?: string;
+
+ // Human-readable product description.
+ description: string;
+
+ // Map from IETF BCP 47 language tags to localized descriptions
+ description_i18n?: { [lang_tag: string]: string };
+
+ // The number of units of the product to deliver to the customer.
+ quantity?: number;
+
+ // The unit in which the product is measured (liters, kilograms, packages, etc.)
+ unit?: string;
+
+ // The price of the product; this is the total price for quantity times unit of this product.
+ price?: AmountString;
+
+ // An optional base64-encoded product image
+ image?: string;
+
+ // a list of taxes paid by the merchant for this product. Can be empty.
+ taxes?: Tax[];
+
+ // time indicating when this product should be delivered
+ delivery_date?: Timestamp;
+
+ // where to deliver this product. This may be an URL for online delivery
+ // (i.e. 'http://example.com/download' or 'mailto:customer@example.com'),
+ // or a location label defined inside the proposition's 'locations'.
+ // The presence of a colon (':') indicates the use of an URL.
+ delivery_location?: string;
+}
+
+/**
+ * Contract terms from a merchant.
+ */
+export class ContractTerms {
+ /**
+ * Hash of the merchant's wire details.
+ */
+ h_wire: string;
+
+ /**
+ * Hash of the merchant's wire details.
+ */
+ auto_refund?: Duration;
+
+ /**
+ * Wire method the merchant wants to use.
+ */
+ wire_method: string;
+
+ /**
+ * Human-readable short summary of the contract.
+ */
+ summary: string;
+
+ summary_i18n?: { [lang_tag: string]: string };
+
+ /**
+ * Nonce used to ensure freshness.
+ */
+ nonce: string;
+
+ /**
+ * Total amount payable.
+ */
+ amount: string;
+
+ /**
+ * Auditors accepted by the merchant.
+ */
+ auditors: AuditorHandle[];
+
+ /**
+ * Deadline to pay for the contract.
+ */
+ pay_deadline: Timestamp;
+
+ /**
+ * Delivery locations.
+ */
+ locations: any;
+
+ /**
+ * Maximum deposit fee covered by the merchant.
+ */
+ max_fee: string;
+
+ /**
+ * Information about the merchant.
+ */
+ merchant: MerchantInfo;
+
+ /**
+ * Public key of the merchant.
+ */
+ merchant_pub: string;
+
+ /**
+ * List of accepted exchanges.
+ */
+ exchanges: ExchangeHandle[];
+
+ /**
+ * Products that are sold in this contract.
+ */
+ products?: Product[];
+
+ /**
+ * Deadline for refunds.
+ */
+ refund_deadline: Timestamp;
+
+ /**
+ * Deadline for the wire transfer.
+ */
+ wire_transfer_deadline: Timestamp;
+
+ /**
+ * Time when the contract was generated by the merchant.
+ */
+ timestamp: Timestamp;
+
+ /**
+ * Order id to uniquely identify the purchase within
+ * one merchant instance.
+ */
+ order_id: string;
+
+ /**
+ * Base URL of the merchant's backend.
+ */
+ merchant_base_url: string;
+
+ /**
+ * Fulfillment URL to view the product or
+ * delivery status.
+ */
+ fulfillment_url: string;
+
+ /**
+ * Share of the wire fee that must be settled with one payment.
+ */
+ wire_fee_amortization?: number;
+
+ /**
+ * Maximum wire fee that the merchant agrees to pay for.
+ */
+ max_wire_fee?: string;
+
+ /**
+ * Extra data, interpreted by the mechant only.
+ */
+ extra: any;
+}
+
+/**
+ * Refund permission in the format that the merchant gives it to us.
+ */
+export class MerchantAbortPayRefundDetails {
+ /**
+ * Amount to be refunded.
+ */
+ refund_amount: string;
+
+ /**
+ * Fee for the refund.
+ */
+ refund_fee: string;
+
+ /**
+ * Public key of the coin being refunded.
+ */
+ coin_pub: string;
+
+ /**
+ * Refund transaction ID between merchant and exchange.
+ */
+ rtransaction_id: number;
+
+ /**
+ * Exchange's key used for the signature.
+ */
+ exchange_pub?: string;
+
+ /**
+ * Exchange's signature to confirm the refund.
+ */
+ exchange_sig?: string;
+
+ /**
+ * Error replay from the exchange (if any).
+ */
+ exchange_reply?: any;
+
+ /**
+ * Error code from the exchange (if any).
+ */
+ exchange_code?: number;
+
+ /**
+ * HTTP status code of the exchange's response
+ * to the merchant's refund request.
+ */
+ exchange_http_status: number;
+}
+
+/**
+ * Response for a refund pickup or a /pay in abort mode.
+ */
+export class MerchantRefundResponse {
+ /**
+ * Public key of the merchant
+ */
+ merchant_pub: string;
+
+ /**
+ * Contract terms hash of the contract that
+ * is being refunded.
+ */
+ h_contract_terms: string;
+
+ /**
+ * The signed refund permissions, to be sent to the exchange.
+ */
+ refunds: MerchantAbortPayRefundDetails[];
+}
+
+/**
+ * Planchet detail sent to the merchant.
+ */
+export interface TipPlanchetDetail {
+ /**
+ * Hashed denomination public key.
+ */
+ denom_pub_hash: string;
+
+ /**
+ * Coin's blinded public key.
+ */
+ coin_ev: string;
+}
+
+/**
+ * Request sent to the merchant to pick up a tip.
+ */
+export interface TipPickupRequest {
+ /**
+ * Identifier of the tip.
+ */
+ tip_id: string;
+
+ /**
+ * List of planchets the wallet wants to use for the tip.
+ */
+ planchets: TipPlanchetDetail[];
+}
+
+/**
+ * Reserve signature, defined as separate class to facilitate
+ * schema validation with "@Checkable".
+ */
+export class ReserveSigSingleton {
+ /**
+ * Reserve signature.
+ */
+ reserve_sig: string;
+}
+
+/**
+ * Response of the merchant
+ * to the TipPickupRequest.
+ */
+export class TipResponse {
+ /**
+ * Public key of the reserve
+ */
+ reserve_pub: string;
+
+ /**
+ * The order of the signatures matches the planchets list.
+ */
+ reserve_sigs: ReserveSigSingleton[];
+}
+
+/**
+ * Element of the payback list that the
+ * exchange gives us in /keys.
+ */
+export class Recoup {
+ /**
+ * The hash of the denomination public key for which the payback is offered.
+ */
+ h_denom_pub: string;
+}
+
+/**
+ * Structure of one exchange signing key in the /keys response.
+ */
+export class ExchangeSignKeyJson {
+ stamp_start: Timestamp;
+ stamp_expire: Timestamp;
+ stamp_end: Timestamp;
+ key: EddsaPublicKeyString;
+ master_sig: EddsaSignatureString;
+}
+
+/**
+ * Structure that the exchange gives us in /keys.
+ */
+export class ExchangeKeysJson {
+ /**
+ * List of offered denominations.
+ */
+ denoms: Denomination[];
+
+ /**
+ * The exchange's master public key.
+ */
+ master_public_key: string;
+
+ /**
+ * The list of auditors (partially) auditing the exchange.
+ */
+ auditors: Auditor[];
+
+ /**
+ * Timestamp when this response was issued.
+ */
+ list_issue_date: Timestamp;
+
+ /**
+ * List of revoked denominations.
+ */
+ recoup?: Recoup[];
+
+ /**
+ * Short-lived signing keys used to sign online
+ * responses.
+ */
+ signkeys: ExchangeSignKeyJson[];
+
+ /**
+ * Protocol version.
+ */
+ version: string;
+}
+
+/**
+ * Wire fees as anounced by the exchange.
+ */
+export class WireFeesJson {
+ /**
+ * Cost of a wire transfer.
+ */
+ wire_fee: string;
+
+ /**
+ * Cost of clising a reserve.
+ */
+ closing_fee: string;
+
+ /**
+ * Signature made with the exchange's master key.
+ */
+ sig: string;
+
+ /**
+ * Date from which the fee applies.
+ */
+ start_date: Timestamp;
+
+ /**
+ * Data after which the fee doesn't apply anymore.
+ */
+ end_date: Timestamp;
+}
+
+export class AccountInfo {
+ payto_uri: string;
+ master_sig: string;
+}
+
+export class ExchangeWireJson {
+ accounts: AccountInfo[];
+ fees: { [methodName: string]: WireFeesJson[] };
+}
+
+/**
+ * Proposal returned from the contract URL.
+ */
+export class Proposal {
+ /**
+ * Contract terms for the propoal.
+ * Raw, un-decoded JSON object.
+ */
+ contract_terms: any;
+
+ /**
+ * Signature over contract, made by the merchant. The public key used for signing
+ * must be contract_terms.merchant_pub.
+ */
+ sig: string;
+}
+
+/**
+ * Response from the internal merchant API.
+ */
+export class CheckPaymentResponse {
+ order_status: string;
+ refunded: boolean | undefined;
+ refunded_amount: string | undefined;
+ contract_terms: any | undefined;
+ taler_pay_uri: string | undefined;
+ contract_url: string | undefined;
+}
+
+/**
+ * Response from the bank.
+ */
+export class WithdrawOperationStatusResponse {
+ selection_done: boolean;
+
+ transfer_done: boolean;
+
+ amount: string;
+
+ sender_wire?: string;
+
+ suggested_exchange?: string;
+
+ confirm_transfer_url?: string;
+
+ wire_types: string[];
+}
+
+/**
+ * Response from the merchant.
+ */
+export class TipPickupGetResponse {
+ extra: any;
+
+ amount: string;
+
+ amount_left: string;
+
+ exchange_url: string;
+
+ stamp_expire: Timestamp;
+
+ stamp_created: Timestamp;
+}
+
+export class WithdrawResponse {
+ ev_sig: string;
+}
+
+/**
+ * Easy to process format for the public data of coins
+ * managed by the wallet.
+ */
+export interface CoinDumpJson {
+ coins: Array<{
+ /**
+ * The coin's denomination's public key.
+ */
+ denom_pub: string;
+ /**
+ * Hash of denom_pub.
+ */
+ denom_pub_hash: string;
+ /**
+ * Value of the denomination (without any fees).
+ */
+ denom_value: string;
+ /**
+ * Public key of the coin.
+ */
+ coin_pub: string;
+ /**
+ * Base URL of the exchange for the coin.
+ */
+ exchange_base_url: string;
+ /**
+ * Remaining value on the coin, to the knowledge of
+ * the wallet.
+ */
+ remaining_value: string;
+ /**
+ * Public key of the parent coin.
+ * Only present if this coin was obtained via refreshing.
+ */
+ refresh_parent_coin_pub: string | undefined;
+ /**
+ * Public key of the reserve for this coin.
+ * Only present if this coin was obtained via refreshing.
+ */
+ withdrawal_reserve_pub: string | undefined;
+ /**
+ * Is the coin suspended?
+ * Suspended coins are not considered for payments.
+ */
+ coin_suspended: boolean;
+ }>;
+}
+
+export interface MerchantPayResponse {
+ sig: string;
+}
+
+export interface ExchangeMeltResponse {
+ /**
+ * Which of the kappa indices does the client not have to reveal.
+ */
+ noreveal_index: number;
+
+ /**
+ * Signature of TALER_RefreshMeltConfirmationPS whereby the exchange
+ * affirms the successful melt and confirming the noreveal_index
+ */
+ exchange_sig: EddsaSignatureString;
+
+ /*
+ * public EdDSA key of the exchange that was used to generate the signature.
+ * Should match one of the exchange's signing keys from /keys. Again given
+ * explicitly as the client might otherwise be confused by clock skew as to
+ * which signing key was used.
+ */
+ exchange_pub: EddsaPublicKeyString;
+
+ /*
+ * Base URL to use for operations on the refresh context
+ * (so the reveal operation). If not given,
+ * the base URL is the same as the one used for this request.
+ * Can be used if the base URL for /refreshes/ differs from that
+ * for /coins/, i.e. for load balancing. Clients SHOULD
+ * respect the refresh_base_url if provided. Any HTTP server
+ * belonging to an exchange MUST generate a 307 or 308 redirection
+ * to the correct base URL should a client uses the wrong base
+ * URL, or if the base URL has changed since the melt.
+ *
+ * When melting the same coin twice (technically allowed
+ * as the response might have been lost on the network),
+ * the exchange may return different values for the refresh_base_url.
+ */
+ refresh_base_url?: string;
+}
+
+export interface ExchangeRevealItem {
+ ev_sig: string;
+}
+
+export interface ExchangeRevealResponse {
+ // List of the exchange's blinded RSA signatures on the new coins.
+ ev_sigs: ExchangeRevealItem[];
+}
+
+interface MerchantOrderStatusPaid {
+ /**
+ * Was the payment refunded (even partially, via refund or abort)?
+ */
+ refunded: boolean;
+
+ /**
+ * Amount that was refunded in total.
+ */
+ refund_amount: AmountString;
+
+ /**
+ * Successful refunds for this payment, empty array for none.
+ */
+ refunds: MerchantCoinRefundStatus[];
+
+ /**
+ * Public key of the merchant.
+ */
+ merchant_pub: EddsaPublicKeyString;
+}
+
+export type MerchantCoinRefundStatus =
+ | MerchantCoinRefundSuccessStatus
+ | MerchantCoinRefundFailureStatus;
+
+export interface MerchantCoinRefundSuccessStatus {
+ type: "success";
+
+ // HTTP status of the exchange request, 200 (integer) required for refund confirmations.
+ exchange_status: 200;
+
+ // the EdDSA :ref:signature (binary-only) with purpose
+ // TALER_SIGNATURE_EXCHANGE_CONFIRM_REFUND using a current signing key of the
+ // exchange affirming the successful refund
+ exchange_sig: EddsaSignatureString;
+
+ // public EdDSA key of the exchange that was used to generate the signature.
+ // Should match one of the exchange's signing keys from /keys. It is given
+ // explicitly as the client might otherwise be confused by clock skew as to
+ // which signing key was used.
+ exchange_pub: EddsaPublicKeyString;
+
+ // Refund transaction ID.
+ rtransaction_id: number;
+
+ // public key of a coin that was refunded
+ coin_pub: EddsaPublicKeyString;
+
+ // Amount that was refunded, including refund fee charged by the exchange
+ // to the customer.
+ refund_amount: AmountString;
+
+ execution_time: Timestamp;
+}
+
+export interface MerchantCoinRefundFailureStatus {
+ type: "failure";
+
+ // HTTP status of the exchange request, must NOT be 200.
+ exchange_status: number;
+
+ // Taler error code from the exchange reply, if available.
+ exchange_code?: number;
+
+ // If available, HTTP reply from the exchange.
+ exchange_reply?: any;
+
+ // Refund transaction ID.
+ rtransaction_id: number;
+
+ // public key of a coin that was refunded
+ coin_pub: EddsaPublicKeyString;
+
+ // Amount that was refunded, including refund fee charged by the exchange
+ // to the customer.
+ refund_amount: AmountString;
+
+ execution_time: Timestamp;
+}
+
+export interface MerchantOrderStatusUnpaid {
+ /**
+ * URI that the wallet must process to complete the payment.
+ */
+ taler_pay_uri: string;
+
+ /**
+ * Alternative order ID which was paid for already in the same session.
+ *
+ * Only given if the same product was purchased before in the same session.
+ */
+ already_paid_order_id?: string;
+}
+
+export interface WithdrawUriInfoResponse {
+ amount: AmountString;
+ defaultExchangeBaseUrl?: string;
+ possibleExchanges: ExchangeListItem[];
+}
+
+/**
+ * Response body for the following endpoint:
+ *
+ * POST {talerBankIntegrationApi}/withdrawal-operation/{wopid}
+ */
+export interface BankWithdrawalOperationPostResponse {
+ transfer_done: boolean;
+}
+
+export const codecForBankWithdrawalOperationPostResponse = (): Codec<
+ BankWithdrawalOperationPostResponse
+> =>
+ makeCodecForObject<BankWithdrawalOperationPostResponse>()
+ .property("transfer_done", codecForBoolean)
+ .build("BankWithdrawalOperationPostResponse");
+
+export type AmountString = string;
+export type Base32String = string;
+export type EddsaSignatureString = string;
+export type EddsaPublicKeyString = string;
+export type CoinPublicKeyString = string;
+
+export const codecForDenomination = (): Codec<Denomination> =>
+ makeCodecForObject<Denomination>()
+ .property("value", codecForString)
+ .property("denom_pub", codecForString)
+ .property("fee_withdraw", codecForString)
+ .property("fee_deposit", codecForString)
+ .property("fee_refresh", codecForString)
+ .property("fee_refund", codecForString)
+ .property("stamp_start", codecForTimestamp)
+ .property("stamp_expire_withdraw", codecForTimestamp)
+ .property("stamp_expire_legal", codecForTimestamp)
+ .property("stamp_expire_deposit", codecForTimestamp)
+ .property("master_sig", codecForString)
+ .build("Denomination");
+
+export const codecForAuditorDenomSig = (): Codec<AuditorDenomSig> =>
+ makeCodecForObject<AuditorDenomSig>()
+ .property("denom_pub_h", codecForString)
+ .property("auditor_sig", codecForString)
+ .build("AuditorDenomSig");
+
+export const codecForAuditor = (): Codec<Auditor> =>
+ makeCodecForObject<Auditor>()
+ .property("auditor_pub", codecForString)
+ .property("auditor_url", codecForString)
+ .property("denomination_keys", makeCodecForList(codecForAuditorDenomSig()))
+ .build("Auditor");
+
+export const codecForExchangeHandle = (): Codec<ExchangeHandle> =>
+ makeCodecForObject<ExchangeHandle>()
+ .property("master_pub", codecForString)
+ .property("url", codecForString)
+ .build("ExchangeHandle");
+
+export const codecForAuditorHandle = (): Codec<AuditorHandle> =>
+ makeCodecForObject<AuditorHandle>()
+ .property("name", codecForString)
+ .property("master_pub", codecForString)
+ .property("url", codecForString)
+ .build("AuditorHandle");
+
+export const codecForMerchantInfo = (): Codec<MerchantInfo> =>
+ makeCodecForObject<MerchantInfo>()
+ .property("name", codecForString)
+ .property("address", makeCodecOptional(codecForString))
+ .property("jurisdiction", makeCodecOptional(codecForString))
+ .build("MerchantInfo");
+
+export const codecForTax = (): Codec<Tax> =>
+ makeCodecForObject<Tax>()
+ .property("name", codecForString)
+ .property("tax", codecForString)
+ .build("Tax");
+
+export const codecForI18n = (): Codec<{ [lang_tag: string]: string }> =>
+ makeCodecForMap(codecForString);
+
+export const codecForProduct = (): Codec<Product> =>
+ makeCodecForObject<Product>()
+ .property("product_id", makeCodecOptional(codecForString))
+ .property("description", codecForString)
+ .property("description_i18n", makeCodecOptional(codecForI18n()))
+ .property("quantity", makeCodecOptional(codecForNumber))
+ .property("unit", makeCodecOptional(codecForString))
+ .property("price", makeCodecOptional(codecForString))
+ .property("delivery_date", makeCodecOptional(codecForTimestamp))
+ .property("delivery_location", makeCodecOptional(codecForString))
+ .build("Tax");
+
+export const codecForContractTerms = (): Codec<ContractTerms> =>
+ makeCodecForObject<ContractTerms>()
+ .property("order_id", codecForString)
+ .property("fulfillment_url", codecForString)
+ .property("merchant_base_url", codecForString)
+ .property("h_wire", codecForString)
+ .property("auto_refund", makeCodecOptional(codecForDuration))
+ .property("wire_method", codecForString)
+ .property("summary", codecForString)
+ .property("summary_i18n", makeCodecOptional(codecForI18n()))
+ .property("nonce", codecForString)
+ .property("amount", codecForString)
+ .property("auditors", makeCodecForList(codecForAuditorHandle()))
+ .property("pay_deadline", codecForTimestamp)
+ .property("refund_deadline", codecForTimestamp)
+ .property("wire_transfer_deadline", codecForTimestamp)
+ .property("timestamp", codecForTimestamp)
+ .property("locations", codecForAny)
+ .property("max_fee", codecForString)
+ .property("max_wire_fee", makeCodecOptional(codecForString))
+ .property("merchant", codecForMerchantInfo())
+ .property("merchant_pub", codecForString)
+ .property("exchanges", makeCodecForList(codecForExchangeHandle()))
+ .property(
+ "products",
+ makeCodecOptional(makeCodecForList(codecForProduct())),
+ )
+ .property("extra", codecForAny)
+ .build("ContractTerms");
+
+export const codecForMerchantRefundPermission = (): Codec<
+ MerchantAbortPayRefundDetails
+> =>
+ makeCodecForObject<MerchantAbortPayRefundDetails>()
+ .property("refund_amount", codecForString)
+ .property("refund_fee", codecForString)
+ .property("coin_pub", codecForString)
+ .property("rtransaction_id", codecForNumber)
+ .property("exchange_http_status", codecForNumber)
+ .property("exchange_code", makeCodecOptional(codecForNumber))
+ .property("exchange_reply", makeCodecOptional(codecForAny))
+ .property("exchange_sig", makeCodecOptional(codecForString))
+ .property("exchange_pub", makeCodecOptional(codecForString))
+ .build("MerchantRefundPermission");
+
+export const codecForMerchantRefundResponse = (): Codec<
+ MerchantRefundResponse
+> =>
+ makeCodecForObject<MerchantRefundResponse>()
+ .property("merchant_pub", codecForString)
+ .property("h_contract_terms", codecForString)
+ .property("refunds", makeCodecForList(codecForMerchantRefundPermission()))
+ .build("MerchantRefundResponse");
+
+export const codecForReserveSigSingleton = (): Codec<ReserveSigSingleton> =>
+ makeCodecForObject<ReserveSigSingleton>()
+ .property("reserve_sig", codecForString)
+ .build("ReserveSigSingleton");
+
+export const codecForTipResponse = (): Codec<TipResponse> =>
+ makeCodecForObject<TipResponse>()
+ .property("reserve_pub", codecForString)
+ .property("reserve_sigs", makeCodecForList(codecForReserveSigSingleton()))
+ .build("TipResponse");
+
+export const codecForRecoup = (): Codec<Recoup> =>
+ makeCodecForObject<Recoup>()
+ .property("h_denom_pub", codecForString)
+ .build("Recoup");
+
+export const codecForExchangeSigningKey = (): Codec<ExchangeSignKeyJson> =>
+ makeCodecForObject<ExchangeSignKeyJson>()
+ .property("key", codecForString)
+ .property("master_sig", codecForString)
+ .property("stamp_end", codecForTimestamp)
+ .property("stamp_start", codecForTimestamp)
+ .property("stamp_expire", codecForTimestamp)
+ .build("ExchangeSignKeyJson");
+
+export const codecForExchangeKeysJson = (): Codec<ExchangeKeysJson> =>
+ makeCodecForObject<ExchangeKeysJson>()
+ .property("denoms", makeCodecForList(codecForDenomination()))
+ .property("master_public_key", codecForString)
+ .property("auditors", makeCodecForList(codecForAuditor()))
+ .property("list_issue_date", codecForTimestamp)
+ .property("recoup", makeCodecOptional(makeCodecForList(codecForRecoup())))
+ .property("signkeys", makeCodecForList(codecForExchangeSigningKey()))
+ .property("version", codecForString)
+ .build("KeysJson");
+
+export const codecForWireFeesJson = (): Codec<WireFeesJson> =>
+ makeCodecForObject<WireFeesJson>()
+ .property("wire_fee", codecForString)
+ .property("closing_fee", codecForString)
+ .property("sig", codecForString)
+ .property("start_date", codecForTimestamp)
+ .property("end_date", codecForTimestamp)
+ .build("WireFeesJson");
+
+export const codecForAccountInfo = (): Codec<AccountInfo> =>
+ makeCodecForObject<AccountInfo>()
+ .property("payto_uri", codecForString)
+ .property("master_sig", codecForString)
+ .build("AccountInfo");
+
+export const codecForExchangeWireJson = (): Codec<ExchangeWireJson> =>
+ makeCodecForObject<ExchangeWireJson>()
+ .property("accounts", makeCodecForList(codecForAccountInfo()))
+ .property("fees", makeCodecForMap(makeCodecForList(codecForWireFeesJson())))
+ .build("ExchangeWireJson");
+
+export const codecForProposal = (): Codec<Proposal> =>
+ makeCodecForObject<Proposal>()
+ .property("contract_terms", codecForAny)
+ .property("sig", codecForString)
+ .build("Proposal");
+
+export const codecForCheckPaymentResponse = (): Codec<CheckPaymentResponse> =>
+ makeCodecForObject<CheckPaymentResponse>()
+ .property("order_status", codecForString)
+ .property("refunded", makeCodecOptional(codecForBoolean))
+ .property("refunded_amount", makeCodecOptional(codecForString))
+ .property("contract_terms", makeCodecOptional(codecForAny))
+ .property("taler_pay_uri", makeCodecOptional(codecForString))
+ .property("contract_url", makeCodecOptional(codecForString))
+ .build("CheckPaymentResponse");
+
+export const codecForWithdrawOperationStatusResponse = (): Codec<
+ WithdrawOperationStatusResponse
+> =>
+ makeCodecForObject<WithdrawOperationStatusResponse>()
+ .property("selection_done", codecForBoolean)
+ .property("transfer_done", codecForBoolean)
+ .property("amount", codecForString)
+ .property("sender_wire", makeCodecOptional(codecForString))
+ .property("suggested_exchange", makeCodecOptional(codecForString))
+ .property("confirm_transfer_url", makeCodecOptional(codecForString))
+ .property("wire_types", makeCodecForList(codecForString))
+ .build("WithdrawOperationStatusResponse");
+
+export const codecForTipPickupGetResponse = (): Codec<TipPickupGetResponse> =>
+ makeCodecForObject<TipPickupGetResponse>()
+ .property("extra", codecForAny)
+ .property("amount", codecForString)
+ .property("amount_left", codecForString)
+ .property("exchange_url", codecForString)
+ .property("stamp_expire", codecForTimestamp)
+ .property("stamp_created", codecForTimestamp)
+ .build("TipPickupGetResponse");
+
+export const codecForRecoupConfirmation = (): Codec<RecoupConfirmation> =>
+ makeCodecForObject<RecoupConfirmation>()
+ .property("reserve_pub", makeCodecOptional(codecForString))
+ .property("old_coin_pub", makeCodecOptional(codecForString))
+ .build("RecoupConfirmation");
+
+export const codecForWithdrawResponse = (): Codec<WithdrawResponse> =>
+ makeCodecForObject<WithdrawResponse>()
+ .property("ev_sig", codecForString)
+ .build("WithdrawResponse");
+
+export const codecForMerchantPayResponse = (): Codec<MerchantPayResponse> =>
+ makeCodecForObject<MerchantPayResponse>()
+ .property("sig", codecForString)
+ .build("MerchantPayResponse");
+
+export const codecForExchangeMeltResponse = (): Codec<ExchangeMeltResponse> =>
+ makeCodecForObject<ExchangeMeltResponse>()
+ .property("exchange_pub", codecForString)
+ .property("exchange_sig", codecForString)
+ .property("noreveal_index", codecForNumber)
+ .property("refresh_base_url", makeCodecOptional(codecForString))
+ .build("ExchangeMeltResponse");
+
+export const codecForExchangeRevealItem = (): Codec<ExchangeRevealItem> =>
+ makeCodecForObject<ExchangeRevealItem>()
+ .property("ev_sig", codecForString)
+ .build("ExchangeRevealItem");
+
+export const codecForExchangeRevealResponse = (): Codec<
+ ExchangeRevealResponse
+> =>
+ makeCodecForObject<ExchangeRevealResponse>()
+ .property("ev_sigs", makeCodecForList(codecForExchangeRevealItem()))
+ .build("ExchangeRevealResponse");
+
+export const codecForMerchantCoinRefundSuccessStatus = (): Codec<
+ MerchantCoinRefundSuccessStatus
+> =>
+ makeCodecForObject<MerchantCoinRefundSuccessStatus>()
+ .property("type", makeCodecForConstString("success"))
+ .property("coin_pub", codecForString)
+ .property("exchange_status", makeCodecForConstNumber(200))
+ .property("exchange_sig", codecForString)
+ .property("rtransaction_id", codecForNumber)
+ .property("refund_amount", codecForString)
+ .property("exchange_pub", codecForString)
+ .property("execution_time", codecForTimestamp)
+ .build("MerchantCoinRefundSuccessStatus");
+
+export const codecForMerchantCoinRefundFailureStatus = (): Codec<
+ MerchantCoinRefundFailureStatus
+> =>
+ makeCodecForObject<MerchantCoinRefundFailureStatus>()
+ .property("type", makeCodecForConstString("failure"))
+ .property("coin_pub", codecForString)
+ .property("exchange_status", makeCodecForConstNumber(200))
+ .property("rtransaction_id", codecForNumber)
+ .property("refund_amount", codecForString)
+ .property("exchange_code", makeCodecOptional(codecForNumber))
+ .property("exchange_reply", makeCodecOptional(codecForAny))
+ .property("execution_time", codecForTimestamp)
+ .build("MerchantCoinRefundSuccessStatus");
+
+export const codecForMerchantCoinRefundStatus = (): Codec<
+ MerchantCoinRefundStatus
+> =>
+ makeCodecForUnion<MerchantCoinRefundStatus>()
+ .discriminateOn("type")
+ .alternative("success", codecForMerchantCoinRefundSuccessStatus())
+ .alternative("failure", codecForMerchantCoinRefundFailureStatus())
+ .build("MerchantCoinRefundStatus");
+
+export const codecForMerchantOrderStatusPaid = (): Codec<
+ MerchantOrderStatusPaid
+> =>
+ makeCodecForObject<MerchantOrderStatusPaid>()
+ .property("merchant_pub", codecForString)
+ .property("refund_amount", codecForString)
+ .property("refunded", codecForBoolean)
+ .property("refunds", makeCodecForList(codecForMerchantCoinRefundStatus()))
+ .build("MerchantOrderStatusPaid");
+
+export const codecForMerchantOrderStatusUnpaid = (): Codec<
+ MerchantOrderStatusUnpaid
+> =>
+ makeCodecForObject<MerchantOrderStatusUnpaid>()
+ .property("taler_pay_uri", codecForString)
+ .property("already_paid_order_id", makeCodecOptional(codecForString))
+ .build("MerchantOrderStatusUnpaid");
diff --git a/packages/taler-wallet-core/src/types/transactions.d.ts.map b/packages/taler-wallet-core/src/types/transactions.d.ts.map
new file mode 100644
index 000000000..95e19a21c
--- /dev/null
+++ b/packages/taler-wallet-core/src/types/transactions.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"transactions.d.ts","sourceRoot":"","sources":["transactions.ts"],"names":[],"mappings":"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"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/types/transactions.ts b/packages/taler-wallet-core/src/types/transactions.ts
new file mode 100644
index 000000000..de378f51a
--- /dev/null
+++ b/packages/taler-wallet-core/src/types/transactions.ts
@@ -0,0 +1,314 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Type and schema definitions for the wallet's transaction list.
+ *
+ * @author Florian Dold
+ * @author Torsten Grote
+ */
+
+/**
+ * Imports.
+ */
+import { Timestamp } from "../util/time";
+import { AmountString, Product } from "./talerTypes";
+import {
+ Codec,
+ makeCodecForObject,
+ makeCodecOptional,
+ codecForString,
+} from "../util/codec";
+
+export interface TransactionsRequest {
+ /**
+ * return only transactions in the given currency
+ */
+ currency?: string;
+
+ /**
+ * if present, results will be limited to transactions related to the given search string
+ */
+ search?: string;
+}
+
+export interface TransactionsResponse {
+ // a list of past and pending transactions sorted by pending, timestamp and transactionId.
+ // In case two events are both pending and have the same timestamp,
+ // they are sorted by the transactionId
+ // (lexically ascending and locale-independent comparison).
+ transactions: Transaction[];
+}
+
+interface TransactionError {
+ /**
+ * TALER_EC_* unique error code.
+ * The action(s) offered and message displayed on the transaction item depend on this code.
+ */
+ ec: number;
+
+ /**
+ * English-only error hint, if available.
+ */
+ hint?: string;
+
+ /**
+ * Error details specific to "ec", if applicable/available
+ */
+ details?: any;
+}
+
+export interface TransactionCommon {
+ // opaque unique ID for the transaction, used as a starting point for paginating queries
+ // and for invoking actions on the transaction (e.g. deleting/hiding it from the history)
+ transactionId: string;
+
+ // the type of the transaction; different types might provide additional information
+ type: TransactionType;
+
+ // main timestamp of the transaction
+ timestamp: Timestamp;
+
+ // true if the transaction is still pending, false otherwise
+ // If a transaction is not longer pending, its timestamp will be updated,
+ // but its transactionId will remain unchanged
+ pending: boolean;
+
+ // Raw amount of the transaction (exclusive of fees or other extra costs)
+ amountRaw: AmountString;
+
+ // Amount added or removed from the wallet's balance (including all fees and other costs)
+ amountEffective: AmountString;
+
+ error?: TransactionError;
+}
+
+export type Transaction =
+ | TransactionWithdrawal
+ | TransactionPayment
+ | TransactionRefund
+ | TransactionTip
+ | TransactionRefresh;
+
+export const enum TransactionType {
+ Withdrawal = "withdrawal",
+ Payment = "payment",
+ Refund = "refund",
+ Refresh = "refresh",
+ Tip = "tip",
+}
+
+export const enum WithdrawalType {
+ TalerBankIntegrationApi = "taler-bank-integration-api",
+ ManualTransfer = "manual-transfer",
+}
+
+export type WithdrawalDetails =
+ | WithdrawalDetailsForManualTransfer
+ | WithdrawalDetailsForTalerBankIntegrationApi;
+
+interface WithdrawalDetailsForManualTransfer {
+ type: WithdrawalType.ManualTransfer;
+
+ /**
+ * Payto URIs that the exchange supports.
+ *
+ * Already contains the amount and message.
+ */
+ exchangePaytoUris: string[];
+}
+
+interface WithdrawalDetailsForTalerBankIntegrationApi {
+ type: WithdrawalType.TalerBankIntegrationApi;
+
+ /**
+ * Set to true if the bank has confirmed the withdrawal, false if not.
+ * An unconfirmed withdrawal usually requires user-input and should be highlighted in the UI.
+ * See also bankConfirmationUrl below.
+ */
+ confirmed: boolean;
+
+ /**
+ * If the withdrawal is unconfirmed, this can include a URL for user
+ * initiated confirmation.
+ */
+ bankConfirmationUrl?: string;
+}
+
+// This should only be used for actual withdrawals
+// and not for tips that have their own transactions type.
+interface TransactionWithdrawal extends TransactionCommon {
+ type: TransactionType.Withdrawal;
+
+ /**
+ * Exchange of the withdrawal.
+ */
+ exchangeBaseUrl: string;
+
+ /**
+ * Amount that got subtracted from the reserve balance.
+ */
+ amountRaw: AmountString;
+
+ /**
+ * Amount that actually was (or will be) added to the wallet's balance.
+ */
+ amountEffective: AmountString;
+
+ withdrawalDetails: WithdrawalDetails;
+}
+
+export const enum PaymentStatus {
+ /**
+ * Explicitly aborted after timeout / failure
+ */
+ Aborted = "aborted",
+
+ /**
+ * Payment failed, wallet will auto-retry.
+ * User should be given the option to retry now / abort.
+ */
+ Failed = "failed",
+
+ /**
+ * Paid successfully
+ */
+ Paid = "paid",
+
+ /**
+ * User accepted, payment is processing.
+ */
+ Accepted = "accepted",
+}
+
+export interface TransactionPayment extends TransactionCommon {
+ type: TransactionType.Payment;
+
+ /**
+ * Additional information about the payment.
+ */
+ info: PaymentShortInfo;
+
+ /**
+ * How far did the wallet get with processing the payment?
+ */
+ status: PaymentStatus;
+
+ /**
+ * Amount that must be paid for the contract
+ */
+ amountRaw: AmountString;
+
+ /**
+ * Amount that was paid, including deposit, wire and refresh fees.
+ */
+ amountEffective: AmountString;
+}
+
+interface PaymentShortInfo {
+ /**
+ * Order ID, uniquely identifies the order within a merchant instance
+ */
+ orderId: string;
+
+ /**
+ * More information about the merchant
+ */
+ merchant: any;
+
+ /**
+ * Summary of the order, given by the merchant
+ */
+ summary: string;
+
+ /**
+ * Map from IETF BCP 47 language tags to localized summaries
+ */
+ summary_i18n?: { [lang_tag: string]: string };
+
+ /**
+ * List of products that are part of the order
+ */
+ products: Product[] | undefined;
+
+ /**
+ * URL of the fulfillment, given by the merchant
+ */
+ fulfillmentUrl: string;
+}
+
+interface TransactionRefund extends TransactionCommon {
+ type: TransactionType.Refund;
+
+ // ID for the transaction that is refunded
+ refundedTransactionId: string;
+
+ // Additional information about the refunded payment
+ info: PaymentShortInfo;
+
+ // Part of the refund that couldn't be applied because the refund permissions were expired
+ amountInvalid: AmountString;
+
+ // Amount that has been refunded by the merchant
+ amountRaw: AmountString;
+
+ // Amount will be added to the wallet's balance after fees and refreshing
+ amountEffective: AmountString;
+}
+
+interface TransactionTip extends TransactionCommon {
+ type: TransactionType.Tip;
+
+ // true if the user still needs to accept/decline this tip
+ waiting: boolean;
+
+ // true if the user has accepted this top, false otherwise
+ accepted: boolean;
+
+ // Exchange that the tip will be (or was) withdrawn from
+ exchangeBaseUrl: string;
+
+ // More information about the merchant that sent the tip
+ merchant: any;
+
+ // Raw amount of the tip, without extra fees that apply
+ amountRaw: AmountString;
+
+ // Amount will be (or was) added to the wallet's balance after fees and refreshing
+ amountEffective: AmountString;
+}
+
+// A transaction shown for refreshes that are not associated to other transactions
+// such as a refresh necessary before coin expiration.
+// It should only be returned by the API if the effective amount is different from zero.
+interface TransactionRefresh extends TransactionCommon {
+ type: TransactionType.Refresh;
+
+ // Exchange that the coins are refreshed with
+ exchangeBaseUrl: string;
+
+ // Raw amount that is refreshed
+ amountRaw: AmountString;
+
+ // Amount that will be paid as fees for the refresh
+ amountEffective: AmountString;
+}
+
+export const codecForTransactionsRequest = (): Codec<TransactionsRequest> =>
+ makeCodecForObject<TransactionsRequest>()
+ .property("currency", makeCodecOptional(codecForString))
+ .property("search", makeCodecOptional(codecForString))
+ .build("TransactionsRequest");
diff --git a/packages/taler-wallet-core/src/types/types-test.ts b/packages/taler-wallet-core/src/types/types-test.ts
new file mode 100644
index 000000000..afdc01844
--- /dev/null
+++ b/packages/taler-wallet-core/src/types/types-test.ts
@@ -0,0 +1,55 @@
+/*
+ This file is part of TALER
+ (C) 2017 Inria and GNUnet e.V.
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import test from "ava";
+import { codecForContractTerms } from "./talerTypes";
+
+test("contract terms validation", (t) => {
+ const c = {
+ nonce: "123123123",
+ h_wire: "123",
+ amount: "EUR:1.5",
+ auditors: [],
+ exchanges: [{ master_pub: "foo", url: "foo" }],
+ fulfillment_url: "foo",
+ max_fee: "EUR:1.5",
+ merchant_pub: "12345",
+ merchant: { name: "Foo" },
+ order_id: "test_order",
+ pay_deadline: { t_ms: 42 },
+ wire_transfer_deadline: { t_ms: 42 },
+ merchant_base_url: "https://example.com/pay",
+ products: [],
+ refund_deadline: { t_ms: 42 },
+ summary: "hello",
+ timestamp: { t_ms: 42 },
+ wire_method: "test",
+ };
+
+ codecForContractTerms().decode(c);
+
+ const c1 = JSON.parse(JSON.stringify(c));
+ c1.pay_deadline = "foo";
+
+ try {
+ codecForContractTerms().decode(c1);
+ } catch (e) {
+ t.pass();
+ return;
+ }
+
+ t.fail();
+});
diff --git a/packages/taler-wallet-core/src/types/walletTypes.d.ts.map b/packages/taler-wallet-core/src/types/walletTypes.d.ts.map
new file mode 100644
index 000000000..d802110fd
--- /dev/null
+++ b/packages/taler-wallet-core/src/types/walletTypes.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"walletTypes.d.ts","sourceRoot":"","sources":["walletTypes.ts"],"names":[],"mappings":"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"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/types/walletTypes.ts b/packages/taler-wallet-core/src/types/walletTypes.ts
new file mode 100644
index 000000000..04f50f29a
--- /dev/null
+++ b/packages/taler-wallet-core/src/types/walletTypes.ts
@@ -0,0 +1,516 @@
+/*
+ This file is part of GNU Taler
+ (C) 2015-2020 Taler Systems SA
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Types used by clients of the wallet.
+ *
+ * These types are defined in a separate file make tree shaking easier, since
+ * some components use these types (via RPC) but do not depend on the wallet
+ * code directly.
+ *
+ * @author Florian Dold <dold@taler.net>
+ */
+
+/**
+ * Imports.
+ */
+import { AmountJson, codecForAmountJson } from "../util/amounts";
+import * as LibtoolVersion from "../util/libtoolVersion";
+import {
+ ExchangeRecord,
+ ExchangeWireInfo,
+ DenominationSelectionInfo,
+} from "./dbTypes";
+import { Timestamp } from "../util/time";
+import {
+ makeCodecForObject,
+ codecForString,
+ makeCodecOptional,
+ Codec,
+} from "../util/codec";
+import { AmountString } from "./talerTypes";
+
+/**
+ * Response for the create reserve request to the wallet.
+ */
+export class CreateReserveResponse {
+ /**
+ * Exchange URL where the bank should create the reserve.
+ * The URL is canonicalized in the response.
+ */
+ exchange: string;
+
+ /**
+ * Reserve public key of the newly created reserve.
+ */
+ reservePub: string;
+}
+
+/**
+ * Information about what will happen when creating a reserve.
+ *
+ * Sent to the wallet frontend to be rendered and shown to the user.
+ */
+export interface ExchangeWithdrawDetails {
+ /**
+ * Exchange that the reserve will be created at.
+ */
+ exchangeInfo: ExchangeRecord;
+
+ /**
+ * Filtered wire info to send to the bank.
+ */
+ exchangeWireAccounts: string[];
+
+ /**
+ * Selected denominations for withdraw.
+ */
+ selectedDenoms: DenominationSelectionInfo;
+
+ /**
+ * Fees for withdraw.
+ */
+ withdrawFee: AmountJson;
+
+ /**
+ * Remaining balance that is too small to be withdrawn.
+ */
+ overhead: AmountJson;
+
+ /**
+ * Wire fees from the exchange.
+ */
+ wireFees: ExchangeWireInfo;
+
+ /**
+ * Does the wallet know about an auditor for
+ * the exchange that the reserve.
+ */
+ isAudited: boolean;
+
+ /**
+ * Did the user already accept the current terms of service for the exchange?
+ */
+ termsOfServiceAccepted: boolean;
+
+ /**
+ * The exchange is trusted directly.
+ */
+ isTrusted: boolean;
+
+ /**
+ * The earliest deposit expiration of the selected coins.
+ */
+ earliestDepositExpiration: Timestamp;
+
+ /**
+ * Number of currently offered denominations.
+ */
+ numOfferedDenoms: number;
+
+ /**
+ * Public keys of trusted auditors for the currency we're withdrawing.
+ */
+ trustedAuditorPubs: string[];
+
+ /**
+ * Result of checking the wallet's version
+ * against the exchange's version.
+ *
+ * Older exchanges don't return version information.
+ */
+ versionMatch: LibtoolVersion.VersionMatchResult | undefined;
+
+ /**
+ * Libtool-style version string for the exchange or "unknown"
+ * for older exchanges.
+ */
+ exchangeVersion: string;
+
+ /**
+ * Libtool-style version string for the wallet.
+ */
+ walletVersion: string;
+}
+
+export interface Balance {
+ available: AmountString;
+ pendingIncoming: AmountString;
+ pendingOutgoing: AmountString;
+
+ // Does the balance for this currency have a pending
+ // transaction?
+ hasPendingTransactions: boolean;
+
+ // Is there a pending transaction that would affect the balance
+ // and requires user input?
+ requiresUserInput: boolean;
+}
+
+export interface BalancesResponse {
+ balances: Balance[];
+}
+
+/**
+ * For terseness.
+ */
+export function mkAmount(
+ value: number,
+ fraction: number,
+ currency: string,
+): AmountJson {
+ return { value, fraction, currency };
+}
+
+/**
+ * Result for confirmPay
+ */
+export interface ConfirmPayResult {
+ nextUrl: string;
+}
+
+/**
+ * Information about all sender wire details known to the wallet,
+ * as well as exchanges that accept these wire types.
+ */
+export interface SenderWireInfos {
+ /**
+ * Mapping from exchange base url to list of accepted
+ * wire types.
+ */
+ exchangeWireTypes: { [exchangeBaseUrl: string]: string[] };
+
+ /**
+ * Sender wire information stored in the wallet.
+ */
+ senderWires: string[];
+}
+
+/**
+ * Request to create a reserve.
+ */
+export interface CreateReserveRequest {
+ /**
+ * The initial amount for the reserve.
+ */
+ amount: AmountJson;
+
+ /**
+ * Exchange URL where the bank should create the reserve.
+ */
+ exchange: string;
+
+ /**
+ * Payto URI that identifies the exchange's account that the funds
+ * for this reserve go into.
+ */
+ exchangePaytoUri?: string;
+
+ /**
+ * Wire details (as a payto URI) for the bank account that sent the funds to
+ * the exchange.
+ */
+ senderWire?: string;
+
+ /**
+ * URL to fetch the withdraw status from the bank.
+ */
+ bankWithdrawStatusUrl?: string;
+}
+
+export const codecForCreateReserveRequest = (): Codec<CreateReserveRequest> =>
+ makeCodecForObject<CreateReserveRequest>()
+ .property("amount", codecForAmountJson())
+ .property("exchange", codecForString)
+ .property("exchangePaytoUri", codecForString)
+ .property("senderWire", makeCodecOptional(codecForString))
+ .property("bankWithdrawStatusUrl", makeCodecOptional(codecForString))
+ .build("CreateReserveRequest");
+
+/**
+ * Request to mark a reserve as confirmed.
+ */
+export interface ConfirmReserveRequest {
+ /**
+ * Public key of then reserve that should be marked
+ * as confirmed.
+ */
+ reservePub: string;
+}
+
+export const codecForConfirmReserveRequest = (): Codec<ConfirmReserveRequest> =>
+ makeCodecForObject<ConfirmReserveRequest>()
+ .property("reservePub", codecForString)
+ .build("ConfirmReserveRequest");
+
+/**
+ * Wire coins to the user's own bank account.
+ */
+export class ReturnCoinsRequest {
+ /**
+ * The amount to wire.
+ */
+ amount: AmountJson;
+
+ /**
+ * The exchange to take the coins from.
+ */
+ exchange: string;
+
+ /**
+ * Wire details for the bank account of the customer that will
+ * receive the funds.
+ */
+ senderWire?: string;
+
+ /**
+ * Verify that a value matches the schema of this class and convert it into a
+ * member.
+ */
+ static checked: (obj: any) => ReturnCoinsRequest;
+}
+
+/**
+ * Status of processing a tip.
+ */
+export interface TipStatus {
+ accepted: boolean;
+ amount: AmountJson;
+ amountLeft: AmountJson;
+ nextUrl: string;
+ exchangeUrl: string;
+ tipId: string;
+ merchantTipId: string;
+ merchantOrigin: string;
+ expirationTimestamp: Timestamp;
+ timestamp: Timestamp;
+ totalFees: AmountJson;
+}
+
+export interface BenchmarkResult {
+ time: { [s: string]: number };
+ repetitions: number;
+}
+
+/**
+ * Cached next URL for a particular session id.
+ */
+export interface NextUrlResult {
+ nextUrl: string;
+ lastSessionId: string | undefined;
+}
+
+export const enum PreparePayResultType {
+ PaymentPossible = "payment-possible",
+ InsufficientBalance = "insufficient-balance",
+ AlreadyConfirmed = "already-confirmed",
+}
+
+export type PreparePayResult =
+ | PreparePayResultInsufficientBalance
+ | PreparePayResultAlreadyConfirmed
+ | PreparePayResultPaymentPossible;
+
+export interface PreparePayResultPaymentPossible {
+ status: PreparePayResultType.PaymentPossible;
+ proposalId: string;
+ contractTerms: Record<string, unknown>;
+ amountRaw: string;
+ amountEffective: string;
+}
+
+export interface PreparePayResultInsufficientBalance {
+ status: PreparePayResultType.InsufficientBalance;
+ proposalId: string;
+ contractTerms: Record<string, unknown>;
+}
+
+export interface PreparePayResultAlreadyConfirmed {
+ status: PreparePayResultType.AlreadyConfirmed;
+ contractTerms: Record<string, unknown>;
+ paid: boolean;
+ // Only specified if paid.
+ nextUrl?: string;
+}
+
+export interface BankWithdrawDetails {
+ selectionDone: boolean;
+ transferDone: boolean;
+ amount: AmountJson;
+ senderWire?: string;
+ suggestedExchange?: string;
+ confirmTransferUrl?: string;
+ wireTypes: string[];
+ extractedStatusUrl: string;
+}
+
+export interface AcceptWithdrawalResponse {
+ reservePub: string;
+ confirmTransferUrl?: string;
+}
+
+/**
+ * Details about a purchase, including refund status.
+ */
+export interface PurchaseDetails {
+ contractTerms: Record<string, undefined>;
+ hasRefund: boolean;
+ totalRefundAmount: AmountJson;
+ totalRefundAndRefreshFees: AmountJson;
+}
+
+export interface WalletDiagnostics {
+ walletManifestVersion: string;
+ walletManifestDisplayVersion: string;
+ errors: string[];
+ firefoxIdbProblem: boolean;
+ dbOutdated: boolean;
+}
+
+export interface OperationErrorDetails {
+ talerErrorCode: number;
+ talerErrorHint: string;
+ message: string;
+ details: unknown;
+}
+
+export interface PlanchetCreationResult {
+ coinPub: string;
+ coinPriv: string;
+ reservePub: string;
+ denomPubHash: string;
+ denomPub: string;
+ blindingKey: string;
+ withdrawSig: string;
+ coinEv: string;
+ coinValue: AmountJson;
+ coinEvHash: string;
+}
+
+export interface PlanchetCreationRequest {
+ value: AmountJson;
+ feeWithdraw: AmountJson;
+ denomPub: string;
+ reservePub: string;
+ reservePriv: string;
+}
+
+/**
+ * Reasons for why a coin is being refreshed.
+ */
+export const enum RefreshReason {
+ Manual = "manual",
+ Pay = "pay",
+ Refund = "refund",
+ AbortPay = "abort-pay",
+ Recoup = "recoup",
+ BackupRestored = "backup-restored",
+}
+
+/**
+ * Wrapper for coin public keys.
+ */
+export interface CoinPublicKey {
+ readonly coinPub: string;
+}
+
+/**
+ * Wrapper for refresh group IDs.
+ */
+export interface RefreshGroupId {
+ readonly refreshGroupId: string;
+}
+
+/**
+ * Private data required to make a deposit permission.
+ */
+export interface DepositInfo {
+ exchangeBaseUrl: string;
+ contractTermsHash: string;
+ coinPub: string;
+ coinPriv: string;
+ spendAmount: AmountJson;
+ timestamp: Timestamp;
+ refundDeadline: Timestamp;
+ merchantPub: string;
+ feeDeposit: AmountJson;
+ wireInfoHash: string;
+ denomPubHash: string;
+ denomSig: string;
+}
+
+export interface ExchangesListRespose {
+ exchanges: ExchangeListItem[];
+}
+
+export interface ExchangeListItem {
+ exchangeBaseUrl: string;
+ currency: string;
+ paytoUris: string[];
+}
+
+export interface AcceptManualWithdrawalResult {
+ /**
+ * Payto URIs that can be used to fund the withdrawal.
+ */
+ exchangePaytoUris: string[];
+
+ /**
+ * Public key of the newly created reserve.
+ */
+ reservePub: string;
+}
+
+export interface ManualWithdrawalDetails {
+ /**
+ * Did the user accept the current version of the exchange's
+ * terms of service?
+ */
+ tosAccepted: boolean;
+
+ /**
+ * Amount that the user will transfer to the exchange.
+ */
+ amountRaw: AmountString;
+
+ /**
+ * Amount that will be added to the user's wallet balance.
+ */
+ amountEffective: AmountString;
+
+ /**
+ * Ways to pay the exchange.
+ */
+ paytoUris: string[];
+}
+
+export interface GetExchangeTosResult {
+ /**
+ * Markdown version of the current ToS.
+ */
+ tos: string;
+
+ /**
+ * Version tag of the current ToS.
+ */
+ currentEtag: string;
+
+ /**
+ * Version tag of the last ToS that the user has accepted,
+ * if any.
+ */
+ acceptedEtag: string | undefined;
+}
diff --git a/packages/taler-wallet-core/src/util/RequestThrottler.d.ts.map b/packages/taler-wallet-core/src/util/RequestThrottler.d.ts.map
new file mode 100644
index 000000000..3a2fa1081
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/RequestThrottler.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"RequestThrottler.d.ts","sourceRoot":"","sources":["RequestThrottler.ts"],"names":[],"mappings":"AAiGA;;;;GAIG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,aAAa,CAAyC;IAE9D;;;;OAIG;IACH,OAAO,CAAC,QAAQ;IAShB;;;;OAIG;IACH,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO;CAI3C"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/util/RequestThrottler.ts b/packages/taler-wallet-core/src/util/RequestThrottler.ts
new file mode 100644
index 000000000..6f51a72bc
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/RequestThrottler.ts
@@ -0,0 +1,129 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 GNUnet e.V.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Implementation of token bucket throttling.
+ */
+
+/**
+ * Imports.
+ */
+import { getTimestampNow, timestampDifference } from "../util/time";
+import { URL } from "./url";
+
+/**
+ * Maximum request per second, per origin.
+ */
+const MAX_PER_SECOND = 50;
+
+/**
+ * Maximum request per minute, per origin.
+ */
+const MAX_PER_MINUTE = 100;
+
+/**
+ * Maximum request per hour, per origin.
+ */
+const MAX_PER_HOUR = 1000;
+
+/**
+ * Throttling state for one origin.
+ */
+class OriginState {
+ private tokensSecond: number = MAX_PER_SECOND;
+ private tokensMinute: number = MAX_PER_MINUTE;
+ private tokensHour: number = MAX_PER_HOUR;
+ private lastUpdate = getTimestampNow();
+
+ private refill(): void {
+ const now = getTimestampNow();
+ const d = timestampDifference(now, this.lastUpdate);
+ if (d.d_ms === "forever") {
+ throw Error("assertion failed");
+ }
+ const d_s = d.d_ms / 1000;
+ this.tokensSecond = Math.min(
+ MAX_PER_SECOND,
+ this.tokensSecond + d_s / 1000,
+ );
+ this.tokensMinute = Math.min(
+ MAX_PER_MINUTE,
+ this.tokensMinute + (d_s / 1000) * 60,
+ );
+ this.tokensHour = Math.min(
+ MAX_PER_HOUR,
+ this.tokensHour + (d_s / 1000) * 60 * 60,
+ );
+ this.lastUpdate = now;
+ }
+
+ /**
+ * Return true if the request for this origin should be throttled.
+ * Otherwise, take a token out of the respective buckets.
+ */
+ applyThrottle(): boolean {
+ this.refill();
+ if (this.tokensSecond < 1) {
+ console.log("request throttled (per second limit exceeded)");
+ return true;
+ }
+ if (this.tokensMinute < 1) {
+ console.log("request throttled (per minute limit exceeded)");
+ return true;
+ }
+ if (this.tokensHour < 1) {
+ console.log("request throttled (per hour limit exceeded)");
+ return true;
+ }
+ this.tokensSecond--;
+ this.tokensMinute--;
+ this.tokensHour--;
+ return false;
+ }
+}
+
+/**
+ * Request throttler, used as a "last layer of defense" when some
+ * other part of the re-try logic is broken and we're sending too
+ * many requests to the same exchange/bank/merchant.
+ */
+export class RequestThrottler {
+ private perOriginInfo: { [origin: string]: OriginState } = {};
+
+ /**
+ * Get the throttling state for an origin, or
+ * initialize if no state is associated with the
+ * origin yet.
+ */
+ private getState(origin: string): OriginState {
+ const s = this.perOriginInfo[origin];
+ if (s) {
+ return s;
+ }
+ const ns = (this.perOriginInfo[origin] = new OriginState());
+ return ns;
+ }
+
+ /**
+ * Apply throttling to a request.
+ *
+ * @returns whether the request should be throttled.
+ */
+ applyThrottle(requestUrl: string): boolean {
+ const origin = new URL(requestUrl).origin;
+ return this.getState(origin).applyThrottle();
+ }
+}
diff --git a/packages/taler-wallet-core/src/util/amounts-test.ts b/packages/taler-wallet-core/src/util/amounts-test.ts
new file mode 100644
index 000000000..afd8caa51
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/amounts-test.ts
@@ -0,0 +1,140 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import test from "ava";
+
+import { Amounts, AmountJson } from "../util/amounts";
+
+const jAmt = (
+ value: number,
+ fraction: number,
+ currency: string,
+): AmountJson => ({ value, fraction, currency });
+
+const sAmt = (s: string): AmountJson => Amounts.parseOrThrow(s);
+
+test("amount addition (simple)", (t) => {
+ const a1 = jAmt(1, 0, "EUR");
+ const a2 = jAmt(1, 0, "EUR");
+ const a3 = jAmt(2, 0, "EUR");
+ t.true(0 === Amounts.cmp(Amounts.add(a1, a2).amount, a3));
+ t.pass();
+});
+
+test("amount addition (saturation)", (t) => {
+ const a1 = jAmt(1, 0, "EUR");
+ const res = Amounts.add(jAmt(Amounts.maxAmountValue, 0, "EUR"), a1);
+ t.true(res.saturated);
+ t.pass();
+});
+
+test("amount subtraction (simple)", (t) => {
+ const a1 = jAmt(2, 5, "EUR");
+ const a2 = jAmt(1, 0, "EUR");
+ const a3 = jAmt(1, 5, "EUR");
+ t.true(0 === Amounts.cmp(Amounts.sub(a1, a2).amount, a3));
+ t.pass();
+});
+
+test("amount subtraction (saturation)", (t) => {
+ const a1 = jAmt(0, 0, "EUR");
+ const a2 = jAmt(1, 0, "EUR");
+ let res = Amounts.sub(a1, a2);
+ t.true(res.saturated);
+ res = Amounts.sub(a1, a1);
+ t.true(!res.saturated);
+ t.pass();
+});
+
+test("amount comparison", (t) => {
+ t.is(Amounts.cmp(jAmt(1, 0, "EUR"), jAmt(1, 0, "EUR")), 0);
+ t.is(Amounts.cmp(jAmt(1, 1, "EUR"), jAmt(1, 0, "EUR")), 1);
+ t.is(Amounts.cmp(jAmt(1, 1, "EUR"), jAmt(1, 2, "EUR")), -1);
+ t.is(Amounts.cmp(jAmt(1, 0, "EUR"), jAmt(0, 0, "EUR")), 1);
+ t.is(Amounts.cmp(jAmt(0, 0, "EUR"), jAmt(1, 0, "EUR")), -1);
+ t.is(Amounts.cmp(jAmt(1, 0, "EUR"), jAmt(0, 100000000, "EUR")), 0);
+ t.throws(() => Amounts.cmp(jAmt(1, 0, "FOO"), jAmt(1, 0, "BAR")));
+ t.pass();
+});
+
+test("amount parsing", (t) => {
+ t.is(
+ Amounts.cmp(Amounts.parseOrThrow("TESTKUDOS:0"), jAmt(0, 0, "TESTKUDOS")),
+ 0,
+ );
+ t.is(
+ Amounts.cmp(Amounts.parseOrThrow("TESTKUDOS:10"), jAmt(10, 0, "TESTKUDOS")),
+ 0,
+ );
+ t.is(
+ Amounts.cmp(
+ Amounts.parseOrThrow("TESTKUDOS:0.1"),
+ jAmt(0, 10000000, "TESTKUDOS"),
+ ),
+ 0,
+ );
+ t.is(
+ Amounts.cmp(
+ Amounts.parseOrThrow("TESTKUDOS:0.00000001"),
+ jAmt(0, 1, "TESTKUDOS"),
+ ),
+ 0,
+ );
+ t.is(
+ Amounts.cmp(
+ Amounts.parseOrThrow("TESTKUDOS:4503599627370496.99999999"),
+ jAmt(4503599627370496, 99999999, "TESTKUDOS"),
+ ),
+ 0,
+ );
+ t.throws(() => Amounts.parseOrThrow("foo:"));
+ t.throws(() => Amounts.parseOrThrow("1.0"));
+ t.throws(() => Amounts.parseOrThrow("42"));
+ t.throws(() => Amounts.parseOrThrow(":1.0"));
+ t.throws(() => Amounts.parseOrThrow(":42"));
+ t.throws(() => Amounts.parseOrThrow("EUR:.42"));
+ t.throws(() => Amounts.parseOrThrow("EUR:42."));
+ t.throws(() => Amounts.parseOrThrow("TESTKUDOS:4503599627370497.99999999"));
+ t.is(
+ Amounts.cmp(
+ Amounts.parseOrThrow("TESTKUDOS:0.99999999"),
+ jAmt(0, 99999999, "TESTKUDOS"),
+ ),
+ 0,
+ );
+ t.throws(() => Amounts.parseOrThrow("TESTKUDOS:0.999999991"));
+ t.pass();
+});
+
+test("amount stringification", (t) => {
+ t.is(Amounts.stringify(jAmt(0, 0, "TESTKUDOS")), "TESTKUDOS:0");
+ t.is(Amounts.stringify(jAmt(4, 94000000, "TESTKUDOS")), "TESTKUDOS:4.94");
+ t.is(Amounts.stringify(jAmt(0, 10000000, "TESTKUDOS")), "TESTKUDOS:0.1");
+ t.is(Amounts.stringify(jAmt(0, 1, "TESTKUDOS")), "TESTKUDOS:0.00000001");
+ t.is(Amounts.stringify(jAmt(5, 0, "TESTKUDOS")), "TESTKUDOS:5");
+ // denormalized
+ t.is(Amounts.stringify(jAmt(1, 100000000, "TESTKUDOS")), "TESTKUDOS:2");
+ t.pass();
+});
+
+test("amount multiplication", (t) => {
+ t.is(Amounts.stringify(Amounts.mult(sAmt("EUR:1.11"), 0).amount), "EUR:0");
+ t.is(Amounts.stringify(Amounts.mult(sAmt("EUR:1.11"), 1).amount), "EUR:1.11");
+ t.is(Amounts.stringify(Amounts.mult(sAmt("EUR:1.11"), 2).amount), "EUR:2.22");
+ t.is(Amounts.stringify(Amounts.mult(sAmt("EUR:1.11"), 3).amount), "EUR:3.33");
+ t.is(Amounts.stringify(Amounts.mult(sAmt("EUR:1.11"), 4).amount), "EUR:4.44");
+ t.is(Amounts.stringify(Amounts.mult(sAmt("EUR:1.11"), 5).amount), "EUR:5.55");
+});
diff --git a/packages/taler-wallet-core/src/util/amounts.d.ts.map b/packages/taler-wallet-core/src/util/amounts.d.ts.map
new file mode 100644
index 000000000..c70d06fb7
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/amounts.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"amounts.d.ts","sourceRoot":"","sources":["amounts.ts"],"names":[],"mappings":"AAgBA;;GAEG;AAEH;;GAEG;AACH,OAAO,EAIL,KAAK,EACN,MAAM,SAAS,CAAC;AAEjB;;GAEG;AACH,eAAO,MAAM,cAAc,YAAM,CAAC;AAElC;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,IAAI,CAAC;AAElC;;GAEG;AACH,eAAO,MAAM,cAAc,QAAU,CAAC;AAEtC;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB;;OAEG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAEvB;;OAEG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAE1B;;OAEG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED,eAAO,MAAM,kBAAkB,QAAO,KAAK,CAAC,UAAU,CAK9B,CAAC;AAEzB;;GAEG;AACH,MAAM,WAAW,MAAM;IACrB;;OAEG;IACH,MAAM,EAAE,UAAU,CAAC;IACnB;;OAEG;IACH,SAAS,EAAE,OAAO,CAAC;CACpB;AAED;;GAEG;AACH,wBAAgB,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,CAMpD;AAED,wBAAgB,GAAG,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG,MAAM,CAKjD;AAED;;;;;;GAMG;AACH,wBAAgB,GAAG,CAAC,KAAK,EAAE,UAAU,EAAE,GAAG,IAAI,EAAE,UAAU,EAAE,GAAG,MAAM,CA8BpE;AAED;;;;;;GAMG;AACH,wBAAgB,GAAG,CAAC,CAAC,EAAE,UAAU,EAAE,GAAG,IAAI,EAAE,UAAU,EAAE,GAAG,MAAM,CAyBhE;AAED;;;GAGG;AACH,wBAAgB,GAAG,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,UAAU,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAsB5D;AAED;;GAEG;AACH,wBAAgB,IAAI,CAAC,CAAC,EAAE,UAAU,GAAG,UAAU,CAM9C;AAED;;GAEG;AACH,wBAAgB,MAAM,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,MAAM,GAAG,UAAU,CAa3D;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,UAAU,GAAG,OAAO,CAEhD;AAED,wBAAgB,MAAM,CAAC,CAAC,EAAE,UAAU,GAAG,OAAO,CAE7C;AAED;;GAEG;AACH,wBAAgB,KAAK,CAAC,CAAC,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS,CAkBvD;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,GAAG,UAAU,CAMlD;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,UAAU,CAMxE;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE,UAAU,GAAG,MAAM,CAkB/C;AAED;;GAEG;AACH,iBAAS,KAAK,CAAC,CAAC,EAAE,GAAG,GAAG,OAAO,CAU9B;AAED,iBAAS,IAAI,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CA8B9C;AAGD,eAAO,MAAM,OAAO;;;;;;;;;;;;;;;;CAgBnB,CAAC"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/util/amounts.ts b/packages/taler-wallet-core/src/util/amounts.ts
new file mode 100644
index 000000000..00f4b17d7
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/amounts.ts
@@ -0,0 +1,384 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Types and helper functions for dealing with Taler amounts.
+ */
+
+/**
+ * Imports.
+ */
+import {
+ makeCodecForObject,
+ codecForString,
+ codecForNumber,
+ Codec,
+} from "./codec";
+
+/**
+ * Number of fractional units that one value unit represents.
+ */
+export const fractionalBase = 1e8;
+
+/**
+ * How many digits behind the comma are required to represent the
+ * fractional value in human readable decimal format? Must match
+ * lg(fractionalBase)
+ */
+export const fractionalLength = 8;
+
+/**
+ * Maximum allowed value field of an amount.
+ */
+export const maxAmountValue = 2 ** 52;
+
+/**
+ * Non-negative financial amount. Fractional values are expressed as multiples
+ * of 1e-8.
+ */
+export interface AmountJson {
+ /**
+ * Value, must be an integer.
+ */
+ readonly value: number;
+
+ /**
+ * Fraction, must be an integer. Represent 1/1e8 of a unit.
+ */
+ readonly fraction: number;
+
+ /**
+ * Currency of the amount.
+ */
+ readonly currency: string;
+}
+
+export const codecForAmountJson = (): Codec<AmountJson> =>
+ makeCodecForObject<AmountJson>()
+ .property("currency", codecForString)
+ .property("value", codecForNumber)
+ .property("fraction", codecForNumber)
+ .build("AmountJson");
+
+/**
+ * Result of a possibly overflowing operation.
+ */
+export interface Result {
+ /**
+ * Resulting, possibly saturated amount.
+ */
+ amount: AmountJson;
+ /**
+ * Was there an over-/underflow?
+ */
+ saturated: boolean;
+}
+
+/**
+ * Get an amount that represents zero units of a currency.
+ */
+export function getZero(currency: string): AmountJson {
+ return {
+ currency,
+ fraction: 0,
+ value: 0,
+ };
+}
+
+export function sum(amounts: AmountJson[]): Result {
+ if (amounts.length <= 0) {
+ throw Error("can't sum zero amounts");
+ }
+ return add(amounts[0], ...amounts.slice(1));
+}
+
+/**
+ * Add two amounts. Return the result and whether
+ * the addition overflowed. The overflow is always handled
+ * by saturating and never by wrapping.
+ *
+ * Throws when currencies don't match.
+ */
+export function add(first: AmountJson, ...rest: AmountJson[]): Result {
+ const currency = first.currency;
+ let value = first.value + Math.floor(first.fraction / fractionalBase);
+ if (value > maxAmountValue) {
+ return {
+ amount: { currency, value: maxAmountValue, fraction: fractionalBase - 1 },
+ saturated: true,
+ };
+ }
+ let fraction = first.fraction % fractionalBase;
+ for (const x of rest) {
+ if (x.currency !== currency) {
+ throw Error(`Mismatched currency: ${x.currency} and ${currency}`);
+ }
+
+ value =
+ value + x.value + Math.floor((fraction + x.fraction) / fractionalBase);
+ fraction = Math.floor((fraction + x.fraction) % fractionalBase);
+ if (value > maxAmountValue) {
+ return {
+ amount: {
+ currency,
+ value: maxAmountValue,
+ fraction: fractionalBase - 1,
+ },
+ saturated: true,
+ };
+ }
+ }
+ return { amount: { currency, value, fraction }, saturated: false };
+}
+
+/**
+ * Subtract two amounts. Return the result and whether
+ * the subtraction overflowed. The overflow is always handled
+ * by saturating and never by wrapping.
+ *
+ * Throws when currencies don't match.
+ */
+export function sub(a: AmountJson, ...rest: AmountJson[]): Result {
+ const currency = a.currency;
+ let value = a.value;
+ let fraction = a.fraction;
+
+ for (const b of rest) {
+ if (b.currency !== currency) {
+ throw Error(`Mismatched currency: ${b.currency} and ${currency}`);
+ }
+ if (fraction < b.fraction) {
+ if (value < 1) {
+ return { amount: { currency, value: 0, fraction: 0 }, saturated: true };
+ }
+ value--;
+ fraction += fractionalBase;
+ }
+ console.assert(fraction >= b.fraction);
+ fraction -= b.fraction;
+ if (value < b.value) {
+ return { amount: { currency, value: 0, fraction: 0 }, saturated: true };
+ }
+ value -= b.value;
+ }
+
+ return { amount: { currency, value, fraction }, saturated: false };
+}
+
+/**
+ * Compare two amounts. Returns 0 when equal, -1 when a < b
+ * and +1 when a > b. Throws when currencies don't match.
+ */
+export function cmp(a: AmountJson, b: AmountJson): -1 | 0 | 1 {
+ if (a.currency !== b.currency) {
+ throw Error(`Mismatched currency: ${a.currency} and ${b.currency}`);
+ }
+ const av = a.value + Math.floor(a.fraction / fractionalBase);
+ const af = a.fraction % fractionalBase;
+ const bv = b.value + Math.floor(b.fraction / fractionalBase);
+ const bf = b.fraction % fractionalBase;
+ switch (true) {
+ case av < bv:
+ return -1;
+ case av > bv:
+ return 1;
+ case af < bf:
+ return -1;
+ case af > bf:
+ return 1;
+ case af === bf:
+ return 0;
+ default:
+ throw Error("assertion failed");
+ }
+}
+
+/**
+ * Create a copy of an amount.
+ */
+export function copy(a: AmountJson): AmountJson {
+ return {
+ currency: a.currency,
+ fraction: a.fraction,
+ value: a.value,
+ };
+}
+
+/**
+ * Divide an amount. Throws on division by zero.
+ */
+export function divide(a: AmountJson, n: number): AmountJson {
+ if (n === 0) {
+ throw Error(`Division by 0`);
+ }
+ if (n === 1) {
+ return { value: a.value, fraction: a.fraction, currency: a.currency };
+ }
+ const r = a.value % n;
+ return {
+ currency: a.currency,
+ fraction: Math.floor((r * fractionalBase + a.fraction) / n),
+ value: Math.floor(a.value / n),
+ };
+}
+
+/**
+ * Check if an amount is non-zero.
+ */
+export function isNonZero(a: AmountJson): boolean {
+ return a.value > 0 || a.fraction > 0;
+}
+
+export function isZero(a: AmountJson): boolean {
+ return a.value === 0 && a.fraction === 0;
+}
+
+/**
+ * Parse an amount like 'EUR:20.5' for 20 Euros and 50 ct.
+ */
+export function parse(s: string): AmountJson | undefined {
+ const res = s.match(/^([a-zA-Z0-9_*-]+):([0-9]+)([.][0-9]+)?$/);
+ if (!res) {
+ return undefined;
+ }
+ const tail = res[3] || ".0";
+ if (tail.length > fractionalLength + 1) {
+ return undefined;
+ }
+ const value = Number.parseInt(res[2]);
+ if (value > maxAmountValue) {
+ return undefined;
+ }
+ return {
+ currency: res[1],
+ fraction: Math.round(fractionalBase * Number.parseFloat(tail)),
+ value,
+ };
+}
+
+/**
+ * Parse amount in standard string form (like 'EUR:20.5'),
+ * throw if the input is not a valid amount.
+ */
+export function parseOrThrow(s: string): AmountJson {
+ const res = parse(s);
+ if (!res) {
+ throw Error(`Can't parse amount: "${s}"`);
+ }
+ return res;
+}
+
+/**
+ * Convert a float to a Taler amount.
+ * Loss of precision possible.
+ */
+export function fromFloat(floatVal: number, currency: string): AmountJson {
+ return {
+ currency,
+ fraction: Math.floor((floatVal - Math.floor(floatVal)) * fractionalBase),
+ value: Math.floor(floatVal),
+ };
+}
+
+/**
+ * Convert to standard human-readable string representation that's
+ * also used in JSON formats.
+ */
+export function stringify(a: AmountJson): string {
+ const av = a.value + Math.floor(a.fraction / fractionalBase);
+ const af = a.fraction % fractionalBase;
+ let s = av.toString();
+
+ if (af) {
+ s = s + ".";
+ let n = af;
+ for (let i = 0; i < fractionalLength; i++) {
+ if (!n) {
+ break;
+ }
+ s = s + Math.floor((n / fractionalBase) * 10).toString();
+ n = (n * 10) % fractionalBase;
+ }
+ }
+
+ return `${a.currency}:${s}`;
+}
+
+/**
+ * Check if the argument is a valid amount in string form.
+ */
+function check(a: any): boolean {
+ if (typeof a !== "string") {
+ return false;
+ }
+ try {
+ const parsedAmount = parse(a);
+ return !!parsedAmount;
+ } catch {
+ return false;
+ }
+}
+
+function mult(a: AmountJson, n: number): Result {
+ if (!Number.isInteger(n)) {
+ throw Error("amount can only be multipied by an integer");
+ }
+ if (n < 0) {
+ throw Error("amount can only be multiplied by a positive integer");
+ }
+ if (n == 0) {
+ return { amount: getZero(a.currency), saturated: false };
+ }
+ let x = a;
+ let acc = getZero(a.currency);
+ while (n > 1) {
+ if (n % 2 == 0) {
+ n = n / 2;
+ } else {
+ n = (n - 1) / 2;
+ const r2 = add(acc, x);
+ if (r2.saturated) {
+ return r2;
+ }
+ acc = r2.amount;
+ }
+ const r2 = add(x, x);
+ if (r2.saturated) {
+ return r2;
+ }
+ x = r2.amount;
+ }
+ return add(acc, x);
+}
+
+// Export all amount-related functions here for better IDE experience.
+export const Amounts = {
+ stringify: stringify,
+ parse: parse,
+ parseOrThrow: parseOrThrow,
+ cmp: cmp,
+ add: add,
+ sum: sum,
+ sub: sub,
+ mult: mult,
+ check: check,
+ getZero: getZero,
+ isZero: isZero,
+ maxAmountValue: maxAmountValue,
+ fromFloat: fromFloat,
+ copy: copy,
+ fractionalBase: fractionalBase,
+};
diff --git a/packages/taler-wallet-core/src/util/assertUnreachable.d.ts.map b/packages/taler-wallet-core/src/util/assertUnreachable.d.ts.map
new file mode 100644
index 000000000..64a1ed8e8
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/assertUnreachable.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"assertUnreachable.d.ts","sourceRoot":"","sources":["assertUnreachable.ts"],"names":[],"mappings":"AAgBA,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,KAAK,GAAG,KAAK,CAEjD"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/util/assertUnreachable.ts b/packages/taler-wallet-core/src/util/assertUnreachable.ts
new file mode 100644
index 000000000..ffdf88f04
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/assertUnreachable.ts
@@ -0,0 +1,19 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 GNUnet e.V.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+export function assertUnreachable(x: never): never {
+ throw new Error("Didn't expect to get here");
+}
diff --git a/packages/taler-wallet-core/src/util/asyncMemo.d.ts.map b/packages/taler-wallet-core/src/util/asyncMemo.d.ts.map
new file mode 100644
index 000000000..0b764b61b
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/asyncMemo.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"asyncMemo.d.ts","sourceRoot":"","sources":["asyncMemo.ts"],"names":[],"mappings":"AAsBA,qBAAa,cAAc,CAAC,CAAC;IAC3B,OAAO,CAAC,CAAC,CAAK;IACd,OAAO,CAAC,OAAO,CAAqC;IAEpD,OAAO,CAAC,OAAO;IAOf,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAiBnD,KAAK,IAAI,IAAI;CAGd;AAED,qBAAa,iBAAiB,CAAC,CAAC;IAC9B,OAAO,CAAC,CAAC,CAAK;IACd,OAAO,CAAC,SAAS,CAA2B;IAE5C,OAAO,CAAC,OAAO;IAMf,IAAI,CAAC,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAkBtC,KAAK,IAAI,IAAI;CAGd"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/util/asyncMemo.ts b/packages/taler-wallet-core/src/util/asyncMemo.ts
new file mode 100644
index 000000000..6e88081b6
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/asyncMemo.ts
@@ -0,0 +1,87 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 GNUnet e.V.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+interface MemoEntry<T> {
+ p: Promise<T>;
+ t: number;
+ n: number;
+}
+
+export class AsyncOpMemoMap<T> {
+ private n = 0;
+ private memoMap: { [k: string]: MemoEntry<T> } = {};
+
+ private cleanUp(key: string, n: number): void {
+ const r = this.memoMap[key];
+ if (r && r.n === n) {
+ delete this.memoMap[key];
+ }
+ }
+
+ memo(key: string, pg: () => Promise<T>): Promise<T> {
+ const res = this.memoMap[key];
+ if (res) {
+ return res.p;
+ }
+ const n = this.n++;
+ // Wrap the operation in case it immediately throws
+ const p = Promise.resolve().then(() => pg());
+ this.memoMap[key] = {
+ p,
+ n,
+ t: new Date().getTime(),
+ };
+ return p.finally(() => {
+ this.cleanUp(key, n);
+ });
+ }
+ clear(): void {
+ this.memoMap = {};
+ }
+}
+
+export class AsyncOpMemoSingle<T> {
+ private n = 0;
+ private memoEntry: MemoEntry<T> | undefined;
+
+ private cleanUp(n: number): void {
+ if (this.memoEntry && this.memoEntry.n === n) {
+ this.memoEntry = undefined;
+ }
+ }
+
+ memo(pg: () => Promise<T>): Promise<T> {
+ const res = this.memoEntry;
+ if (res) {
+ return res.p;
+ }
+ const n = this.n++;
+ // Wrap the operation in case it immediately throws
+ const p = Promise.resolve().then(() => pg());
+ p.finally(() => {
+ this.cleanUp(n);
+ });
+ this.memoEntry = {
+ p,
+ n,
+ t: new Date().getTime(),
+ };
+ return p;
+ }
+ clear(): void {
+ this.memoEntry = undefined;
+ }
+}
diff --git a/packages/taler-wallet-core/src/util/codec-test.ts b/packages/taler-wallet-core/src/util/codec-test.ts
new file mode 100644
index 000000000..b429c318c
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/codec-test.ts
@@ -0,0 +1,78 @@
+/*
+ This file is part of GNU Taler
+ (C) 2018-2019 GNUnet e.V.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Type-safe codecs for converting from/to JSON.
+ */
+
+import test from "ava";
+import {
+ Codec,
+ makeCodecForObject,
+ makeCodecForConstString,
+ codecForString,
+ makeCodecForUnion,
+} from "./codec";
+
+interface MyObj {
+ foo: string;
+}
+
+interface AltOne {
+ type: "one";
+ foo: string;
+}
+
+interface AltTwo {
+ type: "two";
+ bar: string;
+}
+
+type MyUnion = AltOne | AltTwo;
+
+test("basic codec", (t) => {
+ const myObjCodec = makeCodecForObject<MyObj>()
+ .property("foo", codecForString)
+ .build("MyObj");
+ const res = myObjCodec.decode({ foo: "hello" });
+ t.assert(res.foo === "hello");
+
+ t.throws(() => {
+ myObjCodec.decode({ foo: 123 });
+ });
+});
+
+test("union", (t) => {
+ const altOneCodec: Codec<AltOne> = makeCodecForObject<AltOne>()
+ .property("type", makeCodecForConstString("one"))
+ .property("foo", codecForString)
+ .build("AltOne");
+ const altTwoCodec: Codec<AltTwo> = makeCodecForObject<AltTwo>()
+ .property("type", makeCodecForConstString("two"))
+ .property("bar", codecForString)
+ .build("AltTwo");
+ const myUnionCodec: Codec<MyUnion> = makeCodecForUnion<MyUnion>()
+ .discriminateOn("type")
+ .alternative("one", altOneCodec)
+ .alternative("two", altTwoCodec)
+ .build<MyUnion>("MyUnion");
+
+ const res = myUnionCodec.decode({ type: "one", foo: "bla" });
+ t.is(res.type, "one");
+ if (res.type == "one") {
+ t.is(res.foo, "bla");
+ }
+});
diff --git a/packages/taler-wallet-core/src/util/codec.d.ts.map b/packages/taler-wallet-core/src/util/codec.d.ts.map
new file mode 100644
index 000000000..4304f5cef
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/codec.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"codec.d.ts","sourceRoot":"","sources":["codec.ts"],"names":[],"mappings":"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"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/util/codec.ts b/packages/taler-wallet-core/src/util/codec.ts
new file mode 100644
index 000000000..2ce3c2cba
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/codec.ts
@@ -0,0 +1,406 @@
+/*
+ This file is part of GNU Taler
+ (C) 2018-2019 GNUnet e.V.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Type-safe codecs for converting from/to JSON.
+ */
+
+/* eslint-disable @typescript-eslint/ban-types */
+
+/**
+ * Error thrown when decoding fails.
+ */
+export class DecodingError extends Error {
+ constructor(message: string) {
+ super(message);
+ Object.setPrototypeOf(this, DecodingError.prototype);
+ this.name = "DecodingError";
+ }
+}
+
+/**
+ * Context information to show nicer error messages when decoding fails.
+ */
+export interface Context {
+ readonly path?: string[];
+}
+
+export function renderContext(c?: Context): string {
+ const p = c?.path;
+ if (p) {
+ return p.join(".");
+ } else {
+ return "(unknown)";
+ }
+}
+
+function joinContext(c: Context | undefined, part: string): Context {
+ const path = c?.path ?? [];
+ return {
+ path: path.concat([part]),
+ };
+}
+
+/**
+ * A codec converts untyped JSON to a typed object.
+ */
+export interface Codec<V> {
+ /**
+ * Decode untyped JSON to an object of type [[V]].
+ */
+ readonly decode: (x: any, c?: Context) => V;
+}
+
+type SingletonRecord<K extends keyof any, V> = { [Y in K]: V };
+
+interface Prop {
+ name: string;
+ codec: Codec<any>;
+}
+
+interface Alternative {
+ tagValue: any;
+ codec: Codec<any>;
+}
+
+class ObjectCodecBuilder<OutputType, PartialOutputType> {
+ private propList: Prop[] = [];
+
+ /**
+ * Define a property for the object.
+ */
+ property<K extends keyof OutputType & string, V extends OutputType[K]>(
+ x: K,
+ codec: Codec<V>,
+ ): ObjectCodecBuilder<OutputType, PartialOutputType & SingletonRecord<K, V>> {
+ if (!codec) {
+ throw Error("inner codec must be defined");
+ }
+ this.propList.push({ name: x, codec: codec });
+ return this as any;
+ }
+
+ /**
+ * Return the built codec.
+ *
+ * @param objectDisplayName name of the object that this codec operates on,
+ * used in error messages.
+ */
+ build(objectDisplayName: string): Codec<PartialOutputType> {
+ const propList = this.propList;
+ return {
+ decode(x: any, c?: Context): PartialOutputType {
+ if (!c) {
+ c = {
+ path: [`(${objectDisplayName})`],
+ };
+ }
+ if (typeof x !== "object") {
+ throw new DecodingError(
+ `expected object for ${objectDisplayName} at ${renderContext(
+ c,
+ )} but got ${typeof x}`,
+ );
+ }
+ const obj: any = {};
+ for (const prop of propList) {
+ const propRawVal = x[prop.name];
+ const propVal = prop.codec.decode(
+ propRawVal,
+ joinContext(c, prop.name),
+ );
+ obj[prop.name] = propVal;
+ }
+ return obj as PartialOutputType;
+ },
+ };
+ }
+}
+
+class UnionCodecBuilder<
+ TargetType,
+ TagPropertyLabel extends keyof TargetType,
+ CommonBaseType,
+ PartialTargetType
+> {
+ private alternatives = new Map<any, Alternative>();
+
+ constructor(
+ private discriminator: TagPropertyLabel,
+ private baseCodec?: Codec<CommonBaseType>,
+ ) {}
+
+ /**
+ * Define a property for the object.
+ */
+ alternative<V>(
+ tagValue: TargetType[TagPropertyLabel],
+ codec: Codec<V>,
+ ): UnionCodecBuilder<
+ TargetType,
+ TagPropertyLabel,
+ CommonBaseType,
+ PartialTargetType | V
+ > {
+ if (!codec) {
+ throw Error("inner codec must be defined");
+ }
+ this.alternatives.set(tagValue, { codec, tagValue });
+ return this as any;
+ }
+
+ /**
+ * Return the built codec.
+ *
+ * @param objectDisplayName name of the object that this codec operates on,
+ * used in error messages.
+ */
+ build<R extends PartialTargetType & CommonBaseType = never>(
+ objectDisplayName: string,
+ ): Codec<R> {
+ const alternatives = this.alternatives;
+ const discriminator = this.discriminator;
+ const baseCodec = this.baseCodec;
+ return {
+ decode(x: any, c?: Context): R {
+ if (!c) {
+ c = {
+ path: [`(${objectDisplayName})`],
+ };
+ }
+ const d = x[discriminator];
+ if (d === undefined) {
+ throw new DecodingError(
+ `expected tag for ${objectDisplayName} at ${renderContext(
+ c,
+ )}.${discriminator}`,
+ );
+ }
+ const alt = alternatives.get(d);
+ if (!alt) {
+ throw new DecodingError(
+ `unknown tag for ${objectDisplayName} ${d} at ${renderContext(
+ c,
+ )}.${discriminator}`,
+ );
+ }
+ const altDecoded = alt.codec.decode(x);
+ if (baseCodec) {
+ const baseDecoded = baseCodec.decode(x, c);
+ return { ...baseDecoded, ...altDecoded };
+ } else {
+ return altDecoded;
+ }
+ },
+ };
+ }
+}
+
+export class UnionCodecPreBuilder<T> {
+ discriminateOn<D extends keyof T, B = {}>(
+ discriminator: D,
+ baseCodec?: Codec<B>,
+ ): UnionCodecBuilder<T, D, B, never> {
+ return new UnionCodecBuilder<T, D, B, never>(discriminator, baseCodec);
+ }
+}
+
+/**
+ * Return a builder for a codec that decodes an object with properties.
+ */
+export function makeCodecForObject<T>(): ObjectCodecBuilder<T, {}> {
+ return new ObjectCodecBuilder<T, {}>();
+}
+
+export function makeCodecForUnion<T>(): UnionCodecPreBuilder<T> {
+ return new UnionCodecPreBuilder<T>();
+}
+
+/**
+ * Return a codec for a mapping from a string to values described by the inner codec.
+ */
+export function makeCodecForMap<T>(
+ innerCodec: Codec<T>,
+): Codec<{ [x: string]: T }> {
+ if (!innerCodec) {
+ throw Error("inner codec must be defined");
+ }
+ return {
+ decode(x: any, c?: Context): { [x: string]: T } {
+ const map: { [x: string]: T } = {};
+ if (typeof x !== "object") {
+ throw new DecodingError(`expected object at ${renderContext(c)}`);
+ }
+ for (const i in x) {
+ map[i] = innerCodec.decode(x[i], joinContext(c, `[${i}]`));
+ }
+ return map;
+ },
+ };
+}
+
+/**
+ * Return a codec for a list, containing values described by the inner codec.
+ */
+export function makeCodecForList<T>(innerCodec: Codec<T>): Codec<T[]> {
+ if (!innerCodec) {
+ throw Error("inner codec must be defined");
+ }
+ return {
+ decode(x: any, c?: Context): T[] {
+ const arr: T[] = [];
+ if (!Array.isArray(x)) {
+ throw new DecodingError(`expected array at ${renderContext(c)}`);
+ }
+ for (const i in x) {
+ arr.push(innerCodec.decode(x[i], joinContext(c, `[${i}]`)));
+ }
+ return arr;
+ },
+ };
+}
+
+/**
+ * Return a codec for a value that must be a number.
+ */
+export const codecForNumber: Codec<number> = {
+ decode(x: any, c?: Context): number {
+ if (typeof x === "number") {
+ return x;
+ }
+ throw new DecodingError(
+ `expected number at ${renderContext(c)} but got ${typeof x}`,
+ );
+ },
+};
+
+/**
+ * Return a codec for a value that must be a number.
+ */
+export const codecForBoolean: Codec<boolean> = {
+ decode(x: any, c?: Context): boolean {
+ if (typeof x === "boolean") {
+ return x;
+ }
+ throw new DecodingError(
+ `expected boolean at ${renderContext(c)} but got ${typeof x}`,
+ );
+ },
+};
+
+/**
+ * Return a codec for a value that must be a string.
+ */
+export const codecForString: Codec<string> = {
+ decode(x: any, c?: Context): string {
+ if (typeof x === "string") {
+ return x;
+ }
+ throw new DecodingError(
+ `expected string at ${renderContext(c)} but got ${typeof x}`,
+ );
+ },
+};
+
+/**
+ * Codec that allows any value.
+ */
+export const codecForAny: Codec<any> = {
+ decode(x: any, c?: Context): any {
+ return x;
+ },
+};
+
+/**
+ * Return a codec for a value that must be a string.
+ */
+export function makeCodecForConstString<V extends string>(s: V): Codec<V> {
+ return {
+ decode(x: any, c?: Context): V {
+ if (x === s) {
+ return x;
+ }
+ throw new DecodingError(
+ `expected string constant "${s}" at ${renderContext(
+ c,
+ )} but got ${typeof x}`,
+ );
+ },
+ };
+}
+
+/**
+ * Return a codec for a boolean true constant.
+ */
+export function makeCodecForConstTrue(): Codec<true> {
+ return {
+ decode(x: any, c?: Context): true {
+ if (x === true) {
+ return x;
+ }
+ throw new DecodingError(
+ `expected boolean true at ${renderContext(c)} but got ${typeof x}`,
+ );
+ },
+ };
+}
+
+/**
+ * Return a codec for a boolean true constant.
+ */
+export function makeCodecForConstFalse(): Codec<false> {
+ return {
+ decode(x: any, c?: Context): false {
+ if (x === false) {
+ return x;
+ }
+ throw new DecodingError(
+ `expected boolean false at ${renderContext(c)} but got ${typeof x}`,
+ );
+ },
+ };
+}
+
+/**
+ * Return a codec for a value that must be a constant number.
+ */
+export function makeCodecForConstNumber<V extends number>(n: V): Codec<V> {
+ return {
+ decode(x: any, c?: Context): V {
+ if (x === n) {
+ return x;
+ }
+ throw new DecodingError(
+ `expected number constant "${n}" at ${renderContext(
+ c,
+ )} but got ${typeof x}`,
+ );
+ },
+ };
+}
+
+export function makeCodecOptional<V>(
+ innerCodec: Codec<V>,
+): Codec<V | undefined> {
+ return {
+ decode(x: any, c?: Context): V | undefined {
+ if (x === undefined || x === null) {
+ return undefined;
+ }
+ return innerCodec.decode(x, c);
+ },
+ };
+}
diff --git a/packages/taler-wallet-core/src/util/helpers-test.ts b/packages/taler-wallet-core/src/util/helpers-test.ts
new file mode 100644
index 000000000..dbecf14b8
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/helpers-test.ts
@@ -0,0 +1,46 @@
+/*
+ This file is part of TALER
+ (C) 2017 Inria and GNUnet e.V.
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import test from "ava";
+import * as helpers from "./helpers";
+
+test("URL canonicalization", (t) => {
+ // converts to relative, adds https
+ t.is(
+ "https://alice.example.com/exchange/",
+ helpers.canonicalizeBaseUrl("alice.example.com/exchange"),
+ );
+
+ // keeps http, adds trailing slash
+ t.is(
+ "http://alice.example.com/exchange/",
+ helpers.canonicalizeBaseUrl("http://alice.example.com/exchange"),
+ );
+
+ // keeps http, adds trailing slash
+ t.is(
+ "http://alice.example.com/exchange/",
+ helpers.canonicalizeBaseUrl("http://alice.example.com/exchange#foobar"),
+ );
+
+ // Remove search component
+ t.is(
+ "http://alice.example.com/exchange/",
+ helpers.canonicalizeBaseUrl("http://alice.example.com/exchange?foo=bar"),
+ );
+
+ t.pass();
+});
diff --git a/packages/taler-wallet-core/src/util/helpers.d.ts.map b/packages/taler-wallet-core/src/util/helpers.d.ts.map
new file mode 100644
index 000000000..789c5c81c
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/helpers.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["helpers.ts"],"names":[],"mappings":"AAgBA;;GAEG;AAEH;;GAEG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAIvC;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAGzD;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAWvD;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,GAAG,GAAG,MAAM,CAwB9C;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,GAAG,OAAO,CAclD;AAED,wBAAgB,QAAQ,CAAC,CAAC,EAAE,GAAG,GAAG,GAAG,CAGpC;AAED;;;GAGG;AACH,wBAAgB,OAAO,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,EAAE,CAE5D;AAED;;GAEG;AACH,wBAAgB,IAAI,CAAC,GAAG,EAAE,GAAG,GAAG,MAAM,CAarC;AAED;;GAEG;AACH,wBAAgB,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAQrD"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/util/helpers.ts b/packages/taler-wallet-core/src/util/helpers.ts
new file mode 100644
index 000000000..ae4b0359e
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/helpers.ts
@@ -0,0 +1,148 @@
+/*
+ This file is part of TALER
+ (C) 2016 GNUnet e.V.
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Small helper functions that don't fit anywhere else.
+ */
+
+/**
+ * Imports.
+ */
+import { AmountJson } from "./amounts";
+import * as Amounts from "./amounts";
+import { URL } from "./url";
+
+/**
+ * Show an amount in a form suitable for the user.
+ * FIXME: In the future, this should consider currency-specific
+ * settings such as significant digits or currency symbols.
+ */
+export function amountToPretty(amount: AmountJson): string {
+ const x = amount.value + amount.fraction / Amounts.fractionalBase;
+ return `${x} ${amount.currency}`;
+}
+
+/**
+ * Canonicalize a base url, typically for the exchange.
+ *
+ * See http://api.taler.net/wallet.html#general
+ */
+export function canonicalizeBaseUrl(url: string): string {
+ if (!url.startsWith("http") && !url.startsWith("https")) {
+ url = "https://" + url;
+ }
+ const x = new URL(url);
+ if (!x.pathname.endsWith("/")) {
+ x.pathname = x.pathname + "/";
+ }
+ x.search = "";
+ x.hash = "";
+ return x.href;
+}
+
+/**
+ * Convert object to JSON with canonical ordering of keys
+ * and whitespace omitted.
+ */
+export function canonicalJson(obj: any): string {
+ // Check for cycles, etc.
+ JSON.stringify(obj);
+ if (typeof obj === "string" || typeof obj === "number" || obj === null) {
+ return JSON.stringify(obj);
+ }
+ if (Array.isArray(obj)) {
+ const objs: string[] = obj.map((e) => canonicalJson(e));
+ return `[${objs.join(",")}]`;
+ }
+ const keys: string[] = [];
+ for (const key in obj) {
+ keys.push(key);
+ }
+ keys.sort();
+ let s = "{";
+ for (let i = 0; i < keys.length; i++) {
+ const key = keys[i];
+ s += JSON.stringify(key) + ":" + canonicalJson(obj[key]);
+ if (i !== keys.length - 1) {
+ s += ",";
+ }
+ }
+ return s + "}";
+}
+
+/**
+ * Check for deep equality of two objects.
+ * Only arrays, objects and primitives are supported.
+ */
+export function deepEquals(x: any, y: any): boolean {
+ if (x === y) {
+ return true;
+ }
+
+ if (Array.isArray(x) && x.length !== y.length) {
+ return false;
+ }
+
+ const p = Object.keys(x);
+ return (
+ Object.keys(y).every((i) => p.indexOf(i) !== -1) &&
+ p.every((i) => deepEquals(x[i], y[i]))
+ );
+}
+
+export function deepCopy(x: any): any {
+ // FIXME: this has many issues ...
+ return JSON.parse(JSON.stringify(x));
+}
+
+/**
+ * Map from a collection to a list or results and then
+ * concatenate the results.
+ */
+export function flatMap<T, U>(xs: T[], f: (x: T) => U[]): U[] {
+ return xs.reduce((acc: U[], next: T) => [...f(next), ...acc], []);
+}
+
+/**
+ * Compute the hash function of a JSON object.
+ */
+export function hash(val: any): number {
+ const str = canonicalJson(val);
+ // https://github.com/darkskyapp/string-hash
+ let h = 5381;
+ let i = str.length;
+ while (i) {
+ h = (h * 33) ^ str.charCodeAt(--i);
+ }
+
+ /* JavaScript does bitwise operations (like XOR, above) on 32-bit signed
+ * integers. Since we want the results to be always positive, convert the
+ * signed int to an unsigned by doing an unsigned bitshift. */
+ return h >>> 0;
+}
+
+/**
+ * Lexically compare two strings.
+ */
+export function strcmp(s1: string, s2: string): number {
+ if (s1 < s2) {
+ return -1;
+ }
+ if (s1 > s2) {
+ return 1;
+ }
+ return 0;
+}
diff --git a/packages/taler-wallet-core/src/util/http.d.ts.map b/packages/taler-wallet-core/src/util/http.d.ts.map
new file mode 100644
index 000000000..edbe41970
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/http.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["http.ts"],"names":[],"mappings":"AAgBA;;;GAGG;AAEH;;GAEG;AACH,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAOhC;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,IAAI,OAAO,CAAC,GAAG,CAAC,CAAC;IACrB,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;CACzB;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,CAAC,EAAE;QAAE,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;CACtC;AAED,oBAAY,kBAAkB;IAC5B,EAAE,MAAM;IACR,IAAI,MAAM;CACX;AAED;;GAEG;AACH,qBAAa,OAAO;IAClB,OAAO,CAAC,SAAS,CAA6B;IAE9C,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAQhC,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;CASvC;AAED;;;;;GAKG;AACH,MAAM,WAAW,kBAAkB;IACjC;;OAEG;IACH,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IAElE;;OAEG;IACH,QAAQ,CACN,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,GAAG,EACT,GAAG,CAAC,EAAE,kBAAkB,GACvB,OAAO,CAAC,YAAY,CAAC,CAAC;CAC1B;AAED,aAAK,kBAAkB,GAAG;IACxB,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,OAAO,CAAC;AAEZ,aAAK,eAAe,CAAC,CAAC,IAClB;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,QAAQ,EAAE,CAAC,CAAA;CAAE,GAC/B;IAAE,OAAO,EAAE,IAAI,CAAC;IAAC,kBAAkB,EAAE,kBAAkB,CAAA;CAAE,CAAC;AAE9D,wBAAsB,kCAAkC,CAAC,CAAC,EACxD,YAAY,EAAE,YAAY,EAC1B,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,GACd,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,CAuC7B;AAED,wBAAgB,2BAA2B,CACzC,YAAY,EAAE,YAAY,EAC1B,kBAAkB,EAAE,kBAAkB,GACrC,KAAK,CAYP;AAED,wBAAsB,8BAA8B,CAAC,CAAC,EACpD,YAAY,EAAE,YAAY,EAC1B,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,GACd,OAAO,CAAC,CAAC,CAAC,CAMZ;AAGD,wBAAsB,kCAAkC,CAAC,CAAC,EACxD,YAAY,EAAE,YAAY,GACzB,OAAO,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC,CAyBlC;AAED,wBAAsB,2BAA2B,CAC/C,YAAY,EAAE,YAAY,GACzB,OAAO,CAAC,IAAI,CAAC,CAiBf;AAED,wBAAsB,8BAA8B,CAAC,CAAC,EACpD,YAAY,EAAE,YAAY,GACzB,OAAO,CAAC,MAAM,CAAC,CAMjB"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/util/http.ts b/packages/taler-wallet-core/src/util/http.ts
new file mode 100644
index 000000000..ad9f0293c
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/http.ts
@@ -0,0 +1,237 @@
+/*
+ This file is part of TALER
+ (C) 2016 GNUnet e.V.
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Helpers for doing XMLHttpRequest-s that are based on ES6 promises.
+ * Allows for easy mocking for test cases.
+ */
+
+/**
+ * Imports
+ */
+import { Codec } from "./codec";
+import { OperationFailedError, makeErrorDetails } from "../operations/errors";
+import { TalerErrorCode } from "../TalerErrorCode";
+import { Logger } from "./logging";
+
+const logger = new Logger("http.ts");
+
+/**
+ * An HTTP response that is returned by all request methods of this library.
+ */
+export interface HttpResponse {
+ requestUrl: string;
+ status: number;
+ headers: Headers;
+ json(): Promise<any>;
+ text(): Promise<string>;
+}
+
+export interface HttpRequestOptions {
+ headers?: { [name: string]: string };
+}
+
+export enum HttpResponseStatus {
+ Ok = 200,
+ Gone = 210,
+}
+
+/**
+ * Headers, roughly modeled after the fetch API's headers object.
+ */
+export class Headers {
+ private headerMap = new Map<string, string>();
+
+ get(name: string): string | null {
+ const r = this.headerMap.get(name.toLowerCase());
+ if (r) {
+ return r;
+ }
+ return null;
+ }
+
+ set(name: string, value: string): void {
+ const normalizedName = name.toLowerCase();
+ const existing = this.headerMap.get(normalizedName);
+ if (existing !== undefined) {
+ this.headerMap.set(normalizedName, existing + "," + value);
+ } else {
+ this.headerMap.set(normalizedName, value);
+ }
+ }
+}
+
+/**
+ * Interface for the HTTP request library used by the wallet.
+ *
+ * The request library is bundled into an interface to make mocking and
+ * request tunneling easy.
+ */
+export interface HttpRequestLibrary {
+ /**
+ * Make an HTTP GET request.
+ */
+ get(url: string, opt?: HttpRequestOptions): Promise<HttpResponse>;
+
+ /**
+ * Make an HTTP POST request with a JSON body.
+ */
+ postJson(
+ url: string,
+ body: any,
+ opt?: HttpRequestOptions,
+ ): Promise<HttpResponse>;
+}
+
+type TalerErrorResponse = {
+ code: number;
+} & unknown;
+
+type ResponseOrError<T> =
+ | { isError: false; response: T }
+ | { isError: true; talerErrorResponse: TalerErrorResponse };
+
+export async function readSuccessResponseJsonOrErrorCode<T>(
+ httpResponse: HttpResponse,
+ codec: Codec<T>,
+): Promise<ResponseOrError<T>> {
+ if (!(httpResponse.status >= 200 && httpResponse.status < 300)) {
+ const errJson = await httpResponse.json();
+ const talerErrorCode = errJson.code;
+ if (typeof talerErrorCode !== "number") {
+ throw new OperationFailedError(
+ makeErrorDetails(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ "Error response did not contain error code",
+ {
+ requestUrl: httpResponse.requestUrl,
+ },
+ ),
+ );
+ }
+ return {
+ isError: true,
+ talerErrorResponse: errJson,
+ };
+ }
+ const respJson = await httpResponse.json();
+ let parsedResponse: T;
+ try {
+ parsedResponse = codec.decode(respJson);
+ } catch (e) {
+ throw OperationFailedError.fromCode(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ "Response invalid",
+ {
+ requestUrl: httpResponse.requestUrl,
+ httpStatusCode: httpResponse.status,
+ validationError: e.toString(),
+ },
+ );
+ }
+ return {
+ isError: false,
+ response: parsedResponse,
+ };
+}
+
+export function throwUnexpectedRequestError(
+ httpResponse: HttpResponse,
+ talerErrorResponse: TalerErrorResponse,
+): never {
+ throw new OperationFailedError(
+ makeErrorDetails(
+ TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
+ "Unexpected error code in response",
+ {
+ requestUrl: httpResponse.requestUrl,
+ httpStatusCode: httpResponse.status,
+ errorResponse: talerErrorResponse,
+ },
+ ),
+ );
+}
+
+export async function readSuccessResponseJsonOrThrow<T>(
+ httpResponse: HttpResponse,
+ codec: Codec<T>,
+): Promise<T> {
+ const r = await readSuccessResponseJsonOrErrorCode(httpResponse, codec);
+ if (!r.isError) {
+ return r.response;
+ }
+ throwUnexpectedRequestError(httpResponse, r.talerErrorResponse);
+}
+
+export async function readSuccessResponseTextOrErrorCode<T>(
+ httpResponse: HttpResponse,
+): Promise<ResponseOrError<string>> {
+ if (!(httpResponse.status >= 200 && httpResponse.status < 300)) {
+ const errJson = await httpResponse.json();
+ const talerErrorCode = errJson.code;
+ if (typeof talerErrorCode !== "number") {
+ throw new OperationFailedError(
+ makeErrorDetails(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ "Error response did not contain error code",
+ {
+ requestUrl: httpResponse.requestUrl,
+ },
+ ),
+ );
+ }
+ return {
+ isError: true,
+ talerErrorResponse: errJson,
+ };
+ }
+ const respJson = await httpResponse.text();
+ return {
+ isError: false,
+ response: respJson,
+ };
+}
+
+export async function checkSuccessResponseOrThrow(
+ httpResponse: HttpResponse,
+): Promise<void> {
+ if (!(httpResponse.status >= 200 && httpResponse.status < 300)) {
+ const errJson = await httpResponse.json();
+ const talerErrorCode = errJson.code;
+ if (typeof talerErrorCode !== "number") {
+ throw new OperationFailedError(
+ makeErrorDetails(
+ TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+ "Error response did not contain error code",
+ {
+ requestUrl: httpResponse.requestUrl,
+ },
+ ),
+ );
+ }
+ throwUnexpectedRequestError(httpResponse, errJson);
+ }
+}
+
+export async function readSuccessResponseTextOrThrow<T>(
+ httpResponse: HttpResponse,
+): Promise<string> {
+ const r = await readSuccessResponseTextOrErrorCode(httpResponse);
+ if (!r.isError) {
+ return r.response;
+ }
+ throwUnexpectedRequestError(httpResponse, r.talerErrorResponse);
+}
diff --git a/packages/taler-wallet-core/src/util/libtoolVersion-test.ts b/packages/taler-wallet-core/src/util/libtoolVersion-test.ts
new file mode 100644
index 000000000..e58e94759
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/libtoolVersion-test.ts
@@ -0,0 +1,48 @@
+/*
+ This file is part of TALER
+ (C) 2017 GNUnet e.V.
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import * as LibtoolVersion from "./libtoolVersion";
+
+import test from "ava";
+
+test("version comparison", (t) => {
+ t.deepEqual(LibtoolVersion.compare("0:0:0", "0:0:0"), {
+ compatible: true,
+ currentCmp: 0,
+ });
+ t.deepEqual(LibtoolVersion.compare("0:0:0", ""), undefined);
+ t.deepEqual(LibtoolVersion.compare("foo", "0:0:0"), undefined);
+ t.deepEqual(LibtoolVersion.compare("0:0:0", "1:0:1"), {
+ compatible: true,
+ currentCmp: -1,
+ });
+ t.deepEqual(LibtoolVersion.compare("0:0:0", "1:5:1"), {
+ compatible: true,
+ currentCmp: -1,
+ });
+ t.deepEqual(LibtoolVersion.compare("0:0:0", "1:5:0"), {
+ compatible: false,
+ currentCmp: -1,
+ });
+ t.deepEqual(LibtoolVersion.compare("1:0:0", "0:5:0"), {
+ compatible: false,
+ currentCmp: 1,
+ });
+ t.deepEqual(LibtoolVersion.compare("1:0:1", "1:5:1"), {
+ compatible: true,
+ currentCmp: 0,
+ });
+});
diff --git a/packages/taler-wallet-core/src/util/libtoolVersion.d.ts.map b/packages/taler-wallet-core/src/util/libtoolVersion.d.ts.map
new file mode 100644
index 000000000..d0e111aa1
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/libtoolVersion.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"libtoolVersion.d.ts","sourceRoot":"","sources":["libtoolVersion.ts"],"names":[],"mappings":"AAgBA;;;GAGG;AAEH;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC;;OAEG;IACH,UAAU,EAAE,OAAO,CAAC;IACpB;;;OAGG;IACH,UAAU,EAAE,MAAM,CAAC;CACpB;AAQD;;GAEG;AACH,wBAAgB,OAAO,CACrB,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,MAAM,GACZ,kBAAkB,GAAG,SAAS,CAehC"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/util/libtoolVersion.ts b/packages/taler-wallet-core/src/util/libtoolVersion.ts
new file mode 100644
index 000000000..5e9d0b74e
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/libtoolVersion.ts
@@ -0,0 +1,88 @@
+/*
+ This file is part of TALER
+ (C) 2017 GNUnet e.V.
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Semantic versioning, but libtool-style.
+ * See https://www.gnu.org/software/libtool/manual/html_node/Libtool-versioning.html
+ */
+
+/**
+ * Result of comparing two libtool versions.
+ */
+export interface VersionMatchResult {
+ /**
+ * Is the first version compatible with the second?
+ */
+ compatible: boolean;
+ /**
+ * Is the first version older (-1), newser (+1) or
+ * identical (0)?
+ */
+ currentCmp: number;
+}
+
+interface Version {
+ current: number;
+ revision: number;
+ age: number;
+}
+
+/**
+ * Compare two libtool-style version strings.
+ */
+export function compare(
+ me: string,
+ other: string,
+): VersionMatchResult | undefined {
+ const meVer = parseVersion(me);
+ const otherVer = parseVersion(other);
+
+ if (!(meVer && otherVer)) {
+ return undefined;
+ }
+
+ const compatible =
+ meVer.current - meVer.age <= otherVer.current &&
+ meVer.current >= otherVer.current - otherVer.age;
+
+ const currentCmp = Math.sign(meVer.current - otherVer.current);
+
+ return { compatible, currentCmp };
+}
+
+function parseVersion(v: string): Version | undefined {
+ const [currentStr, revisionStr, ageStr, ...rest] = v.split(":");
+ if (rest.length !== 0) {
+ return undefined;
+ }
+ const current = Number.parseInt(currentStr);
+ const revision = Number.parseInt(revisionStr);
+ const age = Number.parseInt(ageStr);
+
+ if (Number.isNaN(current)) {
+ return undefined;
+ }
+
+ if (Number.isNaN(revision)) {
+ return undefined;
+ }
+
+ if (Number.isNaN(age)) {
+ return undefined;
+ }
+
+ return { current, revision, age };
+}
diff --git a/packages/taler-wallet-core/src/util/logging.d.ts.map b/packages/taler-wallet-core/src/util/logging.d.ts.map
new file mode 100644
index 000000000..3e289d866
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/logging.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"logging.d.ts","sourceRoot":"","sources":["logging.ts"],"names":[],"mappings":"AAuCA;;;GAGG;AACH,qBAAa,MAAM;IACL,OAAO,CAAC,GAAG;gBAAH,GAAG,EAAE,MAAM;IAE/B,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI;IAW3C,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI;IAW3C,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI;IAW5C,KAAK,CAAC,OAAO,EAAE,GAAG,EAAE,GAAG,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI;CAU1C"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/util/logging.ts b/packages/taler-wallet-core/src/util/logging.ts
new file mode 100644
index 000000000..e4f3be2ff
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/logging.ts
@@ -0,0 +1,89 @@
+/*
+ This file is part of TALER
+ (C) 2019 GNUnet e.V.
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Check if we are running under nodejs.
+ */
+
+const isNode =
+ typeof process !== "undefined" && process.release.name === "node";
+
+function writeNodeLog(
+ message: string,
+ tag: string,
+ level: string,
+ args: any[],
+): void {
+ process.stderr.write(`${new Date().toISOString()} ${tag} ${level} `);
+ process.stderr.write(message);
+ if (args.length != 0) {
+ process.stderr.write(" ");
+ process.stderr.write(JSON.stringify(args, undefined, 2));
+ }
+ process.stderr.write("\n");
+}
+
+/**
+ * Logger that writes to stderr when running under node,
+ * and uses the corresponding console.* method to log in the browser.
+ */
+export class Logger {
+ constructor(private tag: string) {}
+
+ info(message: string, ...args: any[]): void {
+ if (isNode) {
+ writeNodeLog(message, this.tag, "INFO", args);
+ } else {
+ console.info(
+ `${new Date().toISOString()} ${this.tag} INFO ` + message,
+ ...args,
+ );
+ }
+ }
+
+ warn(message: string, ...args: any[]): void {
+ if (isNode) {
+ writeNodeLog(message, this.tag, "WARN", args);
+ } else {
+ console.warn(
+ `${new Date().toISOString()} ${this.tag} INFO ` + message,
+ ...args,
+ );
+ }
+ }
+
+ error(message: string, ...args: any[]): void {
+ if (isNode) {
+ writeNodeLog(message, this.tag, "ERROR", args);
+ } else {
+ console.info(
+ `${new Date().toISOString()} ${this.tag} ERROR ` + message,
+ ...args,
+ );
+ }
+ }
+
+ trace(message: any, ...args: any[]): void {
+ if (isNode) {
+ writeNodeLog(message, this.tag, "TRACE", args);
+ } else {
+ console.info(
+ `${new Date().toISOString()} ${this.tag} TRACE ` + message,
+ ...args,
+ );
+ }
+ }
+}
diff --git a/packages/taler-wallet-core/src/util/payto-test.ts b/packages/taler-wallet-core/src/util/payto-test.ts
new file mode 100644
index 000000000..01280b650
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/payto-test.ts
@@ -0,0 +1,31 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 GNUnet e.V.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import test from "ava";
+
+import { parsePaytoUri } from "./payto";
+
+test("basic payto parsing", (t) => {
+ const r1 = parsePaytoUri("https://example.com/");
+ t.is(r1, undefined);
+
+ const r2 = parsePaytoUri("payto:blabla");
+ t.is(r2, undefined);
+
+ const r3 = parsePaytoUri("payto://x-taler-bank/123");
+ t.is(r3?.targetType, "x-taler-bank");
+ t.is(r3?.targetPath, "123");
+});
diff --git a/packages/taler-wallet-core/src/util/payto.d.ts.map b/packages/taler-wallet-core/src/util/payto.d.ts.map
new file mode 100644
index 000000000..a23c5f5d4
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/payto.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"payto.d.ts","sourceRoot":"","sources":["payto.ts"],"names":[],"mappings":"AAkBA,UAAU,QAAQ;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE;QAAE,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;CACpC;AAID;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,CAAC,EAAE,MAAM,EACT,MAAM,EAAE;IAAE,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;CAAE,GACjC,MAAM,CAOR;AAED,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS,CA6B7D"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/util/payto.ts b/packages/taler-wallet-core/src/util/payto.ts
new file mode 100644
index 000000000..a1c47eb2f
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/payto.ts
@@ -0,0 +1,71 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 GNUnet e.V.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { URLSearchParams } from "./url";
+
+interface PaytoUri {
+ targetType: string;
+ targetPath: string;
+ params: { [name: string]: string };
+}
+
+const paytoPfx = "payto://";
+
+/**
+ * Add query parameters to a payto URI
+ */
+export function addPaytoQueryParams(
+ s: string,
+ params: { [name: string]: string },
+): string {
+ const [acct, search] = s.slice(paytoPfx.length).split("?");
+ const searchParams = new URLSearchParams(search || "");
+ for (const k of Object.keys(params)) {
+ searchParams.set(k, params[k]);
+ }
+ return paytoPfx + acct + "?" + searchParams.toString();
+}
+
+export function parsePaytoUri(s: string): PaytoUri | undefined {
+ if (!s.startsWith(paytoPfx)) {
+ return undefined;
+ }
+
+ const [acct, search] = s.slice(paytoPfx.length).split("?");
+
+ const firstSlashPos = acct.indexOf("/");
+
+ if (firstSlashPos === -1) {
+ return undefined;
+ }
+
+ const targetType = acct.slice(0, firstSlashPos);
+ const targetPath = acct.slice(firstSlashPos + 1);
+
+ const params: { [k: string]: string } = {};
+
+ const searchParams = new URLSearchParams(search || "");
+
+ searchParams.forEach((v, k) => {
+ params[v] = k;
+ });
+
+ return {
+ targetPath,
+ targetType,
+ params,
+ };
+}
diff --git a/packages/taler-wallet-core/src/util/promiseUtils.d.ts.map b/packages/taler-wallet-core/src/util/promiseUtils.d.ts.map
new file mode 100644
index 000000000..1ca9a4c99
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/promiseUtils.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"promiseUtils.d.ts","sourceRoot":"","sources":["promiseUtils.ts"],"names":[],"mappings":"AAgBA,MAAM,WAAW,aAAa,CAAC,CAAC;IAC9B,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IACpB,OAAO,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,IAAI,CAAC;IAC1B,MAAM,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,CAAC;CAC5B;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,CAAC,KAAK,aAAa,CAAC,CAAC,CAAC,CAYjD;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,YAAY,CAAgB;IACpC,OAAO,CAAC,mBAAmB,CAAsB;;IAOjD,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAIrB,OAAO,IAAI,IAAI;CAMhB"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/util/promiseUtils.ts b/packages/taler-wallet-core/src/util/promiseUtils.ts
new file mode 100644
index 000000000..d409686d9
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/promiseUtils.ts
@@ -0,0 +1,60 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 GNUnet e.V.
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+export interface OpenedPromise<T> {
+ promise: Promise<T>;
+ resolve: (val: T) => void;
+ reject: (err: any) => void;
+}
+
+/**
+ * Get an unresolved promise together with its extracted resolve / reject
+ * function.
+ */
+export function openPromise<T>(): OpenedPromise<T> {
+ let resolve: ((x?: any) => void) | null = null;
+ let reject: ((reason?: any) => void) | null = null;
+ const promise = new Promise<T>((res, rej) => {
+ resolve = res;
+ reject = rej;
+ });
+ if (!(resolve && reject)) {
+ // Never happens, unless JS implementation is broken
+ throw Error();
+ }
+ return { resolve, reject, promise };
+}
+
+export class AsyncCondition {
+ private _waitPromise: Promise<void>;
+ private _resolveWaitPromise: (val: void) => void;
+ constructor() {
+ const op = openPromise<void>();
+ this._waitPromise = op.promise;
+ this._resolveWaitPromise = op.resolve;
+ }
+
+ wait(): Promise<void> {
+ return this._waitPromise;
+ }
+
+ trigger(): void {
+ this._resolveWaitPromise();
+ const op = openPromise<void>();
+ this._waitPromise = op.promise;
+ this._resolveWaitPromise = op.resolve;
+ }
+}
diff --git a/packages/taler-wallet-core/src/util/query.d.ts.map b/packages/taler-wallet-core/src/util/query.d.ts.map
new file mode 100644
index 000000000..4b3fc92ea
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/query.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"query.d.ts","sourceRoot":"","sources":["query.ts"],"names":[],"mappings":"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"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/util/query.ts b/packages/taler-wallet-core/src/util/query.ts
new file mode 100644
index 000000000..53359752e
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/query.ts
@@ -0,0 +1,576 @@
+/*
+ This file is part of TALER
+ (C) 2016 GNUnet e.V.
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Database query abstractions.
+ * @module Query
+ * @author Florian Dold
+ */
+
+/**
+ * Imports.
+ */
+import { openPromise } from "./promiseUtils";
+import type { idbtypes } from "idb-bridge";
+
+/**
+ * Exception that should be thrown by client code to abort a transaction.
+ */
+export const TransactionAbort = Symbol("transaction_abort");
+
+/**
+ * Definition of an object store.
+ */
+export class Store<T> {
+ constructor(
+ public name: string,
+ public storeParams?: idbtypes.IDBObjectStoreParameters,
+ public validator?: (v: T) => T,
+ ) {}
+}
+
+/**
+ * Options for an index.
+ */
+export interface IndexOptions {
+ /**
+ * If true and the path resolves to an array, create an index entry for
+ * each member of the array (instead of one index entry containing the full array).
+ *
+ * Defaults to false.
+ */
+ multiEntry?: boolean;
+}
+
+function requestToPromise(req: idbtypes.IDBRequest): Promise<any> {
+ const stack = Error("Failed request was started here.");
+ return new Promise((resolve, reject) => {
+ req.onsuccess = () => {
+ resolve(req.result);
+ };
+ req.onerror = () => {
+ console.log("error in DB request", req.error);
+ reject(req.error);
+ console.log("Request failed:", stack);
+ };
+ });
+}
+
+function transactionToPromise(tx: idbtypes.IDBTransaction): Promise<void> {
+ const stack = Error("Failed transaction was started here.");
+ return new Promise((resolve, reject) => {
+ tx.onabort = () => {
+ reject(TransactionAbort);
+ };
+ tx.oncomplete = () => {
+ resolve();
+ };
+ tx.onerror = () => {
+ console.error("Transaction failed:", stack);
+ reject(tx.error);
+ };
+ });
+}
+
+function applyMutation<T>(
+ req: idbtypes.IDBRequest,
+ f: (x: T) => T | undefined,
+): Promise<void> {
+ return new Promise((resolve, reject) => {
+ req.onsuccess = () => {
+ const cursor = req.result;
+ if (cursor) {
+ const val = cursor.value;
+ const modVal = f(val);
+ if (modVal !== undefined && modVal !== null) {
+ const req2: idbtypes.IDBRequest = cursor.update(modVal);
+ req2.onerror = () => {
+ reject(req2.error);
+ };
+ req2.onsuccess = () => {
+ cursor.continue();
+ };
+ } else {
+ cursor.continue();
+ }
+ } else {
+ resolve();
+ }
+ };
+ req.onerror = () => {
+ reject(req.error);
+ };
+ });
+}
+
+type CursorResult<T> = CursorEmptyResult<T> | CursorValueResult<T>;
+
+interface CursorEmptyResult<T> {
+ hasValue: false;
+}
+
+interface CursorValueResult<T> {
+ hasValue: true;
+ value: T;
+}
+
+class ResultStream<T> {
+ private currentPromise: Promise<void>;
+ private gotCursorEnd = false;
+ private awaitingResult = false;
+
+ constructor(private req: idbtypes.IDBRequest) {
+ this.awaitingResult = true;
+ let p = openPromise<void>();
+ this.currentPromise = p.promise;
+ req.onsuccess = () => {
+ if (!this.awaitingResult) {
+ throw Error("BUG: invariant violated");
+ }
+ const cursor = req.result;
+ if (cursor) {
+ this.awaitingResult = false;
+ p.resolve();
+ p = openPromise<void>();
+ this.currentPromise = p.promise;
+ } else {
+ this.gotCursorEnd = true;
+ p.resolve();
+ }
+ };
+ req.onerror = () => {
+ p.reject(req.error);
+ };
+ }
+
+ async toArray(): Promise<T[]> {
+ const arr: T[] = [];
+ while (true) {
+ const x = await this.next();
+ if (x.hasValue) {
+ arr.push(x.value);
+ } else {
+ break;
+ }
+ }
+ return arr;
+ }
+
+ async map<R>(f: (x: T) => R): Promise<R[]> {
+ const arr: R[] = [];
+ while (true) {
+ const x = await this.next();
+ if (x.hasValue) {
+ arr.push(f(x.value));
+ } else {
+ break;
+ }
+ }
+ return arr;
+ }
+
+ async forEachAsync(f: (x: T) => Promise<void>): Promise<void> {
+ while (true) {
+ const x = await this.next();
+ if (x.hasValue) {
+ await f(x.value);
+ } else {
+ break;
+ }
+ }
+ }
+
+ async forEach(f: (x: T) => void): Promise<void> {
+ while (true) {
+ const x = await this.next();
+ if (x.hasValue) {
+ f(x.value);
+ } else {
+ break;
+ }
+ }
+ }
+
+ async filter(f: (x: T) => boolean): Promise<T[]> {
+ const arr: T[] = [];
+ while (true) {
+ const x = await this.next();
+ if (x.hasValue) {
+ if (f(x.value)) {
+ arr.push(x.value);
+ }
+ } else {
+ break;
+ }
+ }
+ return arr;
+ }
+
+ async next(): Promise<CursorResult<T>> {
+ if (this.gotCursorEnd) {
+ return { hasValue: false };
+ }
+ if (!this.awaitingResult) {
+ const cursor: idbtypes.IDBCursor | undefined = this.req.result;
+ if (!cursor) {
+ throw Error("assertion failed");
+ }
+ this.awaitingResult = true;
+ cursor.continue();
+ }
+ await this.currentPromise;
+ if (this.gotCursorEnd) {
+ return { hasValue: false };
+ }
+ const cursor = this.req.result;
+ if (!cursor) {
+ throw Error("assertion failed");
+ }
+ return { hasValue: true, value: cursor.value };
+ }
+}
+
+export class TransactionHandle {
+ constructor(private tx: idbtypes.IDBTransaction) {}
+
+ put<T>(store: Store<T>, value: T, key?: any): Promise<any> {
+ const req = this.tx.objectStore(store.name).put(value, key);
+ return requestToPromise(req);
+ }
+
+ add<T>(store: Store<T>, value: T, key?: any): Promise<any> {
+ const req = this.tx.objectStore(store.name).add(value, key);
+ return requestToPromise(req);
+ }
+
+ get<T>(store: Store<T>, key: any): Promise<T | undefined> {
+ const req = this.tx.objectStore(store.name).get(key);
+ return requestToPromise(req);
+ }
+
+ getIndexed<S extends idbtypes.IDBValidKey, T>(
+ index: Index<S, T>,
+ key: any,
+ ): Promise<T | undefined> {
+ const req = this.tx
+ .objectStore(index.storeName)
+ .index(index.indexName)
+ .get(key);
+ return requestToPromise(req);
+ }
+
+ iter<T>(store: Store<T>, key?: any): ResultStream<T> {
+ const req = this.tx.objectStore(store.name).openCursor(key);
+ return new ResultStream<T>(req);
+ }
+
+ iterIndexed<S extends idbtypes.IDBValidKey, T>(
+ index: Index<S, T>,
+ key?: any,
+ ): ResultStream<T> {
+ const req = this.tx
+ .objectStore(index.storeName)
+ .index(index.indexName)
+ .openCursor(key);
+ return new ResultStream<T>(req);
+ }
+
+ delete<T>(store: Store<T>, key: any): Promise<void> {
+ const req = this.tx.objectStore(store.name).delete(key);
+ return requestToPromise(req);
+ }
+
+ mutate<T>(
+ store: Store<T>,
+ key: any,
+ f: (x: T) => T | undefined,
+ ): Promise<void> {
+ const req = this.tx.objectStore(store.name).openCursor(key);
+ return applyMutation(req, f);
+ }
+}
+
+function runWithTransaction<T>(
+ db: idbtypes.IDBDatabase,
+ stores: Store<any>[],
+ f: (t: TransactionHandle) => Promise<T>,
+ mode: "readonly" | "readwrite",
+): Promise<T> {
+ const stack = Error("Failed transaction was started here.");
+ return new Promise((resolve, reject) => {
+ const storeName = stores.map((x) => x.name);
+ const tx = db.transaction(storeName, mode);
+ let funResult: any = undefined;
+ let gotFunResult = false;
+ tx.oncomplete = () => {
+ // This is a fatal error: The transaction completed *before*
+ // the transaction function returned. Likely, the transaction
+ // function waited on a promise that is *not* resolved in the
+ // microtask queue, thus triggering the auto-commit behavior.
+ // Unfortunately, the auto-commit behavior of IDB can't be switched
+ // of. There are some proposals to add this functionality in the future.
+ if (!gotFunResult) {
+ const msg =
+ "BUG: transaction closed before transaction function returned";
+ console.error(msg);
+ reject(Error(msg));
+ }
+ resolve(funResult);
+ };
+ tx.onerror = () => {
+ console.error("error in transaction");
+ console.error(stack);
+ };
+ tx.onabort = () => {
+ if (tx.error) {
+ console.error("Transaction aborted with error:", tx.error);
+ } else {
+ console.log("Trasaction aborted (no error)");
+ }
+ reject(TransactionAbort);
+ };
+ const th = new TransactionHandle(tx);
+ const resP = Promise.resolve().then(() => f(th));
+ resP
+ .then((result) => {
+ gotFunResult = true;
+ funResult = result;
+ })
+ .catch((e) => {
+ if (e == TransactionAbort) {
+ console.info("aborting transaction");
+ } else {
+ console.error("Transaction failed:", e);
+ console.error(stack);
+ tx.abort();
+ }
+ })
+ .catch((e) => {
+ console.error("fatal: aborting transaction failed", e);
+ });
+ });
+}
+
+/**
+ * Definition of an index.
+ */
+export class Index<S extends idbtypes.IDBValidKey, T> {
+ /**
+ * Name of the store that this index is associated with.
+ */
+ storeName: string;
+
+ /**
+ * Options to use for the index.
+ */
+ options: IndexOptions;
+
+ constructor(
+ s: Store<T>,
+ public indexName: string,
+ public keyPath: string | string[],
+ options?: IndexOptions,
+ ) {
+ const defaultOptions = {
+ multiEntry: false,
+ };
+ this.options = { ...defaultOptions, ...(options || {}) };
+ this.storeName = s.name;
+ }
+
+ /**
+ * We want to have the key type parameter in use somewhere,
+ * because otherwise the compiler complains. In iterIndex the
+ * key type is pretty useful.
+ */
+ protected _dummyKey: S | undefined;
+}
+
+/**
+ * Return a promise that resolves
+ * to the taler wallet db.
+ */
+export function openDatabase(
+ idbFactory: idbtypes.IDBFactory,
+ databaseName: string,
+ databaseVersion: number,
+ onVersionChange: () => void,
+ onUpgradeNeeded: (
+ db: idbtypes.IDBDatabase,
+ oldVersion: number,
+ newVersion: number,
+ ) => void,
+): Promise<idbtypes.IDBDatabase> {
+ return new Promise<idbtypes.IDBDatabase>((resolve, reject) => {
+ const req = idbFactory.open(databaseName, databaseVersion);
+ req.onerror = (e) => {
+ console.log("taler database error", e);
+ reject(new Error("database error"));
+ };
+ req.onsuccess = (e) => {
+ req.result.onversionchange = (evt: idbtypes.IDBVersionChangeEvent) => {
+ console.log(
+ `handling live db version change from ${evt.oldVersion} to ${evt.newVersion}`,
+ );
+ req.result.close();
+ onVersionChange();
+ };
+ resolve(req.result);
+ };
+ req.onupgradeneeded = (e) => {
+ const db = req.result;
+ const newVersion = e.newVersion;
+ if (!newVersion) {
+ throw Error("upgrade needed, but new version unknown");
+ }
+ onUpgradeNeeded(db, e.oldVersion, newVersion);
+ };
+ });
+}
+
+export class Database {
+ constructor(private db: idbtypes.IDBDatabase) {}
+
+ static deleteDatabase(idbFactory: idbtypes.IDBFactory, dbName: string): void {
+ idbFactory.deleteDatabase(dbName);
+ }
+
+ async exportDatabase(): Promise<any> {
+ const db = this.db;
+ const dump = {
+ name: db.name,
+ stores: {} as { [s: string]: any },
+ version: db.version,
+ };
+
+ return new Promise((resolve, reject) => {
+ const tx = db.transaction(Array.from(db.objectStoreNames));
+ tx.addEventListener("complete", () => {
+ resolve(dump);
+ });
+ // tslint:disable-next-line:prefer-for-of
+ for (let i = 0; i < db.objectStoreNames.length; i++) {
+ const name = db.objectStoreNames[i];
+ const storeDump = {} as { [s: string]: any };
+ dump.stores[name] = storeDump;
+ tx.objectStore(name)
+ .openCursor()
+ .addEventListener("success", (e: idbtypes.Event) => {
+ const cursor = (e.target as any).result;
+ if (cursor) {
+ storeDump[cursor.key] = cursor.value;
+ cursor.continue();
+ }
+ });
+ }
+ });
+ }
+
+ importDatabase(dump: any): Promise<void> {
+ const db = this.db;
+ console.log("importing db", dump);
+ return new Promise<void>((resolve, reject) => {
+ const tx = db.transaction(Array.from(db.objectStoreNames), "readwrite");
+ if (dump.stores) {
+ for (const storeName in dump.stores) {
+ const objects = [];
+ const dumpStore = dump.stores[storeName];
+ for (const key in dumpStore) {
+ objects.push(dumpStore[key]);
+ }
+ console.log(`importing ${objects.length} records into ${storeName}`);
+ const store = tx.objectStore(storeName);
+ for (const obj of objects) {
+ store.put(obj);
+ }
+ }
+ }
+ tx.addEventListener("complete", () => {
+ resolve();
+ });
+ });
+ }
+
+ async get<T>(store: Store<T>, key: any): Promise<T | undefined> {
+ const tx = this.db.transaction([store.name], "readonly");
+ const req = tx.objectStore(store.name).get(key);
+ const v = await requestToPromise(req);
+ await transactionToPromise(tx);
+ return v;
+ }
+
+ async getIndexed<S extends idbtypes.IDBValidKey, T>(
+ index: Index<S, T>,
+ key: any,
+ ): Promise<T | undefined> {
+ const tx = this.db.transaction([index.storeName], "readonly");
+ const req = tx.objectStore(index.storeName).index(index.indexName).get(key);
+ const v = await requestToPromise(req);
+ await transactionToPromise(tx);
+ return v;
+ }
+
+ async put<T>(store: Store<T>, value: T, key?: any): Promise<any> {
+ const tx = this.db.transaction([store.name], "readwrite");
+ const req = tx.objectStore(store.name).put(value, key);
+ const v = await requestToPromise(req);
+ await transactionToPromise(tx);
+ return v;
+ }
+
+ async mutate<T>(
+ store: Store<T>,
+ key: any,
+ f: (x: T) => T | undefined,
+ ): Promise<void> {
+ const tx = this.db.transaction([store.name], "readwrite");
+ const req = tx.objectStore(store.name).openCursor(key);
+ await applyMutation(req, f);
+ await transactionToPromise(tx);
+ }
+
+ iter<T>(store: Store<T>): ResultStream<T> {
+ const tx = this.db.transaction([store.name], "readonly");
+ const req = tx.objectStore(store.name).openCursor();
+ return new ResultStream<T>(req);
+ }
+
+ iterIndex<S extends idbtypes.IDBValidKey, T>(
+ index: Index<S, T>,
+ query?: any,
+ ): ResultStream<T> {
+ const tx = this.db.transaction([index.storeName], "readonly");
+ const req = tx
+ .objectStore(index.storeName)
+ .index(index.indexName)
+ .openCursor(query);
+ return new ResultStream<T>(req);
+ }
+
+ async runWithReadTransaction<T>(
+ stores: Store<any>[],
+ f: (t: TransactionHandle) => Promise<T>,
+ ): Promise<T> {
+ return runWithTransaction<T>(this.db, stores, f, "readonly");
+ }
+
+ async runWithWriteTransaction<T>(
+ stores: Store<any>[],
+ f: (t: TransactionHandle) => Promise<T>,
+ ): Promise<T> {
+ return runWithTransaction<T>(this.db, stores, f, "readwrite");
+ }
+}
diff --git a/packages/taler-wallet-core/src/util/reserveHistoryUtil-test.ts b/packages/taler-wallet-core/src/util/reserveHistoryUtil-test.ts
new file mode 100644
index 000000000..79022de77
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/reserveHistoryUtil-test.ts
@@ -0,0 +1,285 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import test from "ava";
+import {
+ reconcileReserveHistory,
+ summarizeReserveHistory,
+} from "./reserveHistoryUtil";
+import {
+ WalletReserveHistoryItem,
+ WalletReserveHistoryItemType,
+} from "../types/dbTypes";
+import {
+ ReserveTransaction,
+ ReserveTransactionType,
+} from "../types/ReserveTransaction";
+import { Amounts } from "./amounts";
+
+test("basics", (t) => {
+ const r = reconcileReserveHistory([], []);
+ t.deepEqual(r.updatedLocalHistory, []);
+});
+
+test("unmatched credit", (t) => {
+ const localHistory: WalletReserveHistoryItem[] = [];
+ const remoteHistory: ReserveTransaction[] = [
+ {
+ type: ReserveTransactionType.Credit,
+ amount: "TESTKUDOS:100",
+ sender_account_url: "payto://void/",
+ timestamp: { t_ms: 42 },
+ wire_reference: "ABC01",
+ },
+ ];
+ const r = reconcileReserveHistory(localHistory, remoteHistory);
+ const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS");
+ t.deepEqual(r.updatedLocalHistory.length, 1);
+ t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:100");
+ t.deepEqual(Amounts.stringify(s.awaitedReserveAmount), "TESTKUDOS:0");
+ t.deepEqual(Amounts.stringify(s.unclaimedReserveAmount), "TESTKUDOS:100");
+});
+
+test("unmatched credit #2", (t) => {
+ const localHistory: WalletReserveHistoryItem[] = [];
+ const remoteHistory: ReserveTransaction[] = [
+ {
+ type: ReserveTransactionType.Credit,
+ amount: "TESTKUDOS:100",
+ sender_account_url: "payto://void/",
+ timestamp: { t_ms: 42 },
+ wire_reference: "ABC01",
+ },
+ {
+ type: ReserveTransactionType.Credit,
+ amount: "TESTKUDOS:50",
+ sender_account_url: "payto://void/",
+ timestamp: { t_ms: 42 },
+ wire_reference: "ABC02",
+ },
+ ];
+ const r = reconcileReserveHistory(localHistory, remoteHistory);
+ const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS");
+ t.deepEqual(r.updatedLocalHistory.length, 2);
+ t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:150");
+ t.deepEqual(Amounts.stringify(s.awaitedReserveAmount), "TESTKUDOS:0");
+ t.deepEqual(Amounts.stringify(s.unclaimedReserveAmount), "TESTKUDOS:150");
+});
+
+test("matched credit", (t) => {
+ const localHistory: WalletReserveHistoryItem[] = [
+ {
+ type: WalletReserveHistoryItemType.Credit,
+ expectedAmount: Amounts.parseOrThrow("TESTKUDOS:100"),
+ matchedExchangeTransaction: {
+ type: ReserveTransactionType.Credit,
+ amount: "TESTKUDOS:100",
+ sender_account_url: "payto://void/",
+ timestamp: { t_ms: 42 },
+ wire_reference: "ABC01",
+ },
+ },
+ ];
+ const remoteHistory: ReserveTransaction[] = [
+ {
+ type: ReserveTransactionType.Credit,
+ amount: "TESTKUDOS:100",
+ sender_account_url: "payto://void/",
+ timestamp: { t_ms: 42 },
+ wire_reference: "ABC01",
+ },
+ {
+ type: ReserveTransactionType.Credit,
+ amount: "TESTKUDOS:50",
+ sender_account_url: "payto://void/",
+ timestamp: { t_ms: 42 },
+ wire_reference: "ABC02",
+ },
+ ];
+ const r = reconcileReserveHistory(localHistory, remoteHistory);
+ const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS");
+ t.deepEqual(r.updatedLocalHistory.length, 2);
+ t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:150");
+ t.deepEqual(Amounts.stringify(s.awaitedReserveAmount), "TESTKUDOS:0");
+ t.deepEqual(Amounts.stringify(s.unclaimedReserveAmount), "TESTKUDOS:150");
+});
+
+test("fulfilling credit", (t) => {
+ const localHistory: WalletReserveHistoryItem[] = [
+ {
+ type: WalletReserveHistoryItemType.Credit,
+ expectedAmount: Amounts.parseOrThrow("TESTKUDOS:100"),
+ },
+ ];
+ const remoteHistory: ReserveTransaction[] = [
+ {
+ type: ReserveTransactionType.Credit,
+ amount: "TESTKUDOS:100",
+ sender_account_url: "payto://void/",
+ timestamp: { t_ms: 42 },
+ wire_reference: "ABC01",
+ },
+ {
+ type: ReserveTransactionType.Credit,
+ amount: "TESTKUDOS:50",
+ sender_account_url: "payto://void/",
+ timestamp: { t_ms: 42 },
+ wire_reference: "ABC02",
+ },
+ ];
+ const r = reconcileReserveHistory(localHistory, remoteHistory);
+ const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS");
+ t.deepEqual(r.updatedLocalHistory.length, 2);
+ t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:150");
+});
+
+test("unfulfilled credit", (t) => {
+ const localHistory: WalletReserveHistoryItem[] = [
+ {
+ type: WalletReserveHistoryItemType.Credit,
+ expectedAmount: Amounts.parseOrThrow("TESTKUDOS:100"),
+ },
+ ];
+ const remoteHistory: ReserveTransaction[] = [
+ {
+ type: ReserveTransactionType.Credit,
+ amount: "TESTKUDOS:100",
+ sender_account_url: "payto://void/",
+ timestamp: { t_ms: 42 },
+ wire_reference: "ABC01",
+ },
+ {
+ type: ReserveTransactionType.Credit,
+ amount: "TESTKUDOS:50",
+ sender_account_url: "payto://void/",
+ timestamp: { t_ms: 42 },
+ wire_reference: "ABC02",
+ },
+ ];
+ const r = reconcileReserveHistory(localHistory, remoteHistory);
+ const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS");
+ t.deepEqual(r.updatedLocalHistory.length, 2);
+ t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:150");
+});
+
+test("awaited credit", (t) => {
+ const localHistory: WalletReserveHistoryItem[] = [
+ {
+ type: WalletReserveHistoryItemType.Credit,
+ expectedAmount: Amounts.parseOrThrow("TESTKUDOS:50"),
+ },
+ {
+ type: WalletReserveHistoryItemType.Credit,
+ expectedAmount: Amounts.parseOrThrow("TESTKUDOS:100"),
+ },
+ ];
+ const remoteHistory: ReserveTransaction[] = [
+ {
+ type: ReserveTransactionType.Credit,
+ amount: "TESTKUDOS:100",
+ sender_account_url: "payto://void/",
+ timestamp: { t_ms: 42 },
+ wire_reference: "ABC01",
+ },
+ ];
+ const r = reconcileReserveHistory(localHistory, remoteHistory);
+ const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS");
+ t.deepEqual(r.updatedLocalHistory.length, 2);
+ t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:100");
+ t.deepEqual(Amounts.stringify(s.awaitedReserveAmount), "TESTKUDOS:50");
+ t.deepEqual(Amounts.stringify(s.unclaimedReserveAmount), "TESTKUDOS:100");
+});
+
+test("withdrawal new match", (t) => {
+ const localHistory: WalletReserveHistoryItem[] = [
+ {
+ type: WalletReserveHistoryItemType.Credit,
+ expectedAmount: Amounts.parseOrThrow("TESTKUDOS:100"),
+ matchedExchangeTransaction: {
+ type: ReserveTransactionType.Credit,
+ amount: "TESTKUDOS:100",
+ sender_account_url: "payto://void/",
+ timestamp: { t_ms: 42 },
+ wire_reference: "ABC01",
+ },
+ },
+ {
+ type: WalletReserveHistoryItemType.Withdraw,
+ expectedAmount: Amounts.parseOrThrow("TESTKUDOS:5"),
+ },
+ ];
+ const remoteHistory: ReserveTransaction[] = [
+ {
+ type: ReserveTransactionType.Credit,
+ amount: "TESTKUDOS:100",
+ sender_account_url: "payto://void/",
+ timestamp: { t_ms: 42 },
+ wire_reference: "ABC01",
+ },
+ {
+ type: ReserveTransactionType.Withdraw,
+ amount: "TESTKUDOS:5",
+ h_coin_envelope: "foobar",
+ h_denom_pub: "foobar",
+ reserve_sig: "foobar",
+ withdraw_fee: "TESTKUDOS:0.1",
+ },
+ ];
+ const r = reconcileReserveHistory(localHistory, remoteHistory);
+ const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS");
+ t.deepEqual(r.updatedLocalHistory.length, 2);
+ t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:95");
+ t.deepEqual(Amounts.stringify(s.awaitedReserveAmount), "TESTKUDOS:0");
+ t.deepEqual(Amounts.stringify(s.unclaimedReserveAmount), "TESTKUDOS:95");
+});
+
+test("claimed but now arrived", (t) => {
+ const localHistory: WalletReserveHistoryItem[] = [
+ {
+ type: WalletReserveHistoryItemType.Credit,
+ expectedAmount: Amounts.parseOrThrow("TESTKUDOS:100"),
+ matchedExchangeTransaction: {
+ type: ReserveTransactionType.Credit,
+ amount: "TESTKUDOS:100",
+ sender_account_url: "payto://void/",
+ timestamp: { t_ms: 42 },
+ wire_reference: "ABC01",
+ },
+ },
+ {
+ type: WalletReserveHistoryItemType.Withdraw,
+ expectedAmount: Amounts.parseOrThrow("TESTKUDOS:5"),
+ },
+ ];
+ const remoteHistory: ReserveTransaction[] = [
+ {
+ type: ReserveTransactionType.Credit,
+ amount: "TESTKUDOS:100",
+ sender_account_url: "payto://void/",
+ timestamp: { t_ms: 42 },
+ wire_reference: "ABC01",
+ },
+ ];
+ const r = reconcileReserveHistory(localHistory, remoteHistory);
+ const s = summarizeReserveHistory(r.updatedLocalHistory, "TESTKUDOS");
+ t.deepEqual(r.updatedLocalHistory.length, 2);
+ t.deepEqual(Amounts.stringify(s.computedReserveBalance), "TESTKUDOS:100");
+ t.deepEqual(Amounts.stringify(s.awaitedReserveAmount), "TESTKUDOS:0");
+ t.deepEqual(Amounts.stringify(s.unclaimedReserveAmount), "TESTKUDOS:95");
+});
diff --git a/packages/taler-wallet-core/src/util/reserveHistoryUtil.d.ts.map b/packages/taler-wallet-core/src/util/reserveHistoryUtil.d.ts.map
new file mode 100644
index 000000000..aec8f0715
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/reserveHistoryUtil.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"reserveHistoryUtil.d.ts","sourceRoot":"","sources":["reserveHistoryUtil.ts"],"names":[],"mappings":"AAgBA;;GAEG;AACH,OAAO,EACL,wBAAwB,EAEzB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,kBAAkB,EAEnB,MAAM,6BAA6B,CAAC;AACrC,OAAO,KAAK,OAAO,MAAM,iBAAiB,CAAC;AAK3C;;;;GAIG;AAEH,MAAM,WAAW,2BAA2B;IAC1C;;OAEG;IACH,mBAAmB,EAAE,wBAAwB,EAAE,CAAC;IAEhD;;;OAGG;IACH,aAAa,EAAE,wBAAwB,EAAE,CAAC;IAE1C;;;OAGG;IACH,eAAe,EAAE,wBAAwB,EAAE,CAAC;CAC7C;AAED;;;GAGG;AACH,MAAM,WAAW,qBAAqB;IACpC;;;OAGG;IACH,sBAAsB,EAAE,OAAO,CAAC,UAAU,CAAC;IAE3C;;OAEG;IACH,sBAAsB,EAAE,OAAO,CAAC,UAAU,CAAC;IAE3C;;OAEG;IACH,oBAAoB,EAAE,OAAO,CAAC,UAAU,CAAC;IAEzC;;;OAGG;IACH,eAAe,EAAE,OAAO,CAAC,UAAU,CAAC;CACrC;AA6BD;;GAEG;AACH,wBAAgB,yBAAyB,CACvC,EAAE,EAAE,wBAAwB,EAC5B,EAAE,EAAE,kBAAkB,GACrB,OAAO,CAwBT;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CACrC,YAAY,EAAE,wBAAwB,EAAE,EACxC,QAAQ,EAAE,MAAM,GACf,qBAAqB,CAmFvB;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,YAAY,EAAE,wBAAwB,EAAE,EACxC,aAAa,EAAE,kBAAkB,EAAE,GAClC,2BAA2B,CAqH7B"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/util/reserveHistoryUtil.ts b/packages/taler-wallet-core/src/util/reserveHistoryUtil.ts
new file mode 100644
index 000000000..855b71a3d
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/reserveHistoryUtil.ts
@@ -0,0 +1,360 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ WalletReserveHistoryItem,
+ WalletReserveHistoryItemType,
+} from "../types/dbTypes";
+import {
+ ReserveTransaction,
+ ReserveTransactionType,
+} from "../types/ReserveTransaction";
+import * as Amounts from "../util/amounts";
+import { timestampCmp } from "./time";
+import { deepCopy } from "./helpers";
+import { AmountJson } from "../util/amounts";
+
+/**
+ * Helpers for dealing with reserve histories.
+ *
+ * @author Florian Dold <dold@taler.net>
+ */
+
+export interface ReserveReconciliationResult {
+ /**
+ * The wallet's local history reconciled with the exchange's reserve history.
+ */
+ updatedLocalHistory: WalletReserveHistoryItem[];
+
+ /**
+ * History items that were newly created, subset of the
+ * updatedLocalHistory items.
+ */
+ newAddedItems: WalletReserveHistoryItem[];
+
+ /**
+ * History items that were newly matched, subset of the
+ * updatedLocalHistory items.
+ */
+ newMatchedItems: WalletReserveHistoryItem[];
+}
+
+/**
+ * Various totals computed from the wallet's view
+ * on the reserve history.
+ */
+export interface ReserveHistorySummary {
+ /**
+ * Balance computed by the wallet, should match the balance
+ * computed by the reserve.
+ */
+ computedReserveBalance: Amounts.AmountJson;
+
+ /**
+ * Reserve balance that is still available for withdrawal.
+ */
+ unclaimedReserveAmount: Amounts.AmountJson;
+
+ /**
+ * Amount that we're still expecting to come into the reserve.
+ */
+ awaitedReserveAmount: Amounts.AmountJson;
+
+ /**
+ * Amount withdrawn from the reserve so far. Only counts
+ * finished withdrawals, not withdrawals in progress.
+ */
+ withdrawnAmount: Amounts.AmountJson;
+}
+
+/**
+ * Check if two reserve history items (exchange's version) match.
+ */
+function isRemoteHistoryMatch(
+ t1: ReserveTransaction,
+ t2: ReserveTransaction,
+): boolean {
+ switch (t1.type) {
+ case ReserveTransactionType.Closing: {
+ return t1.type === t2.type && t1.wtid == t2.wtid;
+ }
+ case ReserveTransactionType.Credit: {
+ return t1.type === t2.type && t1.wire_reference === t2.wire_reference;
+ }
+ case ReserveTransactionType.Recoup: {
+ return (
+ t1.type === t2.type &&
+ t1.coin_pub === t2.coin_pub &&
+ timestampCmp(t1.timestamp, t2.timestamp) === 0
+ );
+ }
+ case ReserveTransactionType.Withdraw: {
+ return t1.type === t2.type && t1.h_coin_envelope === t2.h_coin_envelope;
+ }
+ }
+}
+
+/**
+ * Check a local reserve history item and a remote history item are a match.
+ */
+export function isLocalRemoteHistoryMatch(
+ t1: WalletReserveHistoryItem,
+ t2: ReserveTransaction,
+): boolean {
+ switch (t1.type) {
+ case WalletReserveHistoryItemType.Credit: {
+ return (
+ t2.type === ReserveTransactionType.Credit &&
+ !!t1.expectedAmount &&
+ Amounts.cmp(t1.expectedAmount, Amounts.parseOrThrow(t2.amount)) === 0
+ );
+ }
+ case WalletReserveHistoryItemType.Withdraw:
+ return (
+ t2.type === ReserveTransactionType.Withdraw &&
+ !!t1.expectedAmount &&
+ Amounts.cmp(t1.expectedAmount, Amounts.parseOrThrow(t2.amount)) === 0
+ );
+ case WalletReserveHistoryItemType.Recoup: {
+ return (
+ t2.type === ReserveTransactionType.Recoup &&
+ !!t1.expectedAmount &&
+ Amounts.cmp(t1.expectedAmount, Amounts.parseOrThrow(t2.amount)) === 0
+ );
+ }
+ }
+ return false;
+}
+
+/**
+ * Compute totals for the wallet's view of the reserve history.
+ */
+export function summarizeReserveHistory(
+ localHistory: WalletReserveHistoryItem[],
+ currency: string,
+): ReserveHistorySummary {
+ const posAmounts: AmountJson[] = [];
+ const negAmounts: AmountJson[] = [];
+ const expectedPosAmounts: AmountJson[] = [];
+ const expectedNegAmounts: AmountJson[] = [];
+ const withdrawnAmounts: AmountJson[] = [];
+
+ for (const item of localHistory) {
+ switch (item.type) {
+ case WalletReserveHistoryItemType.Credit:
+ if (item.matchedExchangeTransaction) {
+ posAmounts.push(
+ Amounts.parseOrThrow(item.matchedExchangeTransaction.amount),
+ );
+ } else if (item.expectedAmount) {
+ expectedPosAmounts.push(item.expectedAmount);
+ }
+ break;
+ case WalletReserveHistoryItemType.Recoup:
+ if (item.matchedExchangeTransaction) {
+ if (item.matchedExchangeTransaction) {
+ posAmounts.push(
+ Amounts.parseOrThrow(item.matchedExchangeTransaction.amount),
+ );
+ } else if (item.expectedAmount) {
+ expectedPosAmounts.push(item.expectedAmount);
+ } else {
+ throw Error("invariant failed");
+ }
+ }
+ break;
+ case WalletReserveHistoryItemType.Closing:
+ if (item.matchedExchangeTransaction) {
+ negAmounts.push(
+ Amounts.parseOrThrow(item.matchedExchangeTransaction.amount),
+ );
+ } else {
+ throw Error("invariant failed");
+ }
+ break;
+ case WalletReserveHistoryItemType.Withdraw:
+ if (item.matchedExchangeTransaction) {
+ negAmounts.push(
+ Amounts.parseOrThrow(item.matchedExchangeTransaction.amount),
+ );
+ withdrawnAmounts.push(
+ Amounts.parseOrThrow(item.matchedExchangeTransaction.amount),
+ );
+ } else if (item.expectedAmount) {
+ expectedNegAmounts.push(item.expectedAmount);
+ } else {
+ throw Error("invariant failed");
+ }
+ break;
+ }
+ }
+
+ const z = Amounts.getZero(currency);
+
+ const computedBalance = Amounts.sub(
+ Amounts.add(z, ...posAmounts).amount,
+ ...negAmounts,
+ ).amount;
+
+ const unclaimedReserveAmount = Amounts.sub(
+ Amounts.add(z, ...posAmounts).amount,
+ ...negAmounts,
+ ...expectedNegAmounts,
+ ).amount;
+
+ const awaitedReserveAmount = Amounts.sub(
+ Amounts.add(z, ...expectedPosAmounts).amount,
+ ...expectedNegAmounts,
+ ).amount;
+
+ const withdrawnAmount = Amounts.add(z, ...withdrawnAmounts).amount;
+
+ return {
+ computedReserveBalance: computedBalance,
+ unclaimedReserveAmount: unclaimedReserveAmount,
+ awaitedReserveAmount: awaitedReserveAmount,
+ withdrawnAmount,
+ };
+}
+
+/**
+ * Reconcile the wallet's local model of the reserve history
+ * with the reserve history of the exchange.
+ */
+export function reconcileReserveHistory(
+ localHistory: WalletReserveHistoryItem[],
+ remoteHistory: ReserveTransaction[],
+): ReserveReconciliationResult {
+ const updatedLocalHistory: WalletReserveHistoryItem[] = deepCopy(
+ localHistory,
+ );
+ const newMatchedItems: WalletReserveHistoryItem[] = [];
+ const newAddedItems: WalletReserveHistoryItem[] = [];
+
+ const remoteMatched = remoteHistory.map(() => false);
+ const localMatched = localHistory.map(() => false);
+
+ // Take care of deposits
+
+ // First, see which pairs are already a definite match.
+ for (let remoteIndex = 0; remoteIndex < remoteHistory.length; remoteIndex++) {
+ const rhi = remoteHistory[remoteIndex];
+ for (let localIndex = 0; localIndex < localHistory.length; localIndex++) {
+ if (localMatched[localIndex]) {
+ continue;
+ }
+ const lhi = localHistory[localIndex];
+ if (!lhi.matchedExchangeTransaction) {
+ continue;
+ }
+ if (isRemoteHistoryMatch(rhi, lhi.matchedExchangeTransaction)) {
+ localMatched[localIndex] = true;
+ remoteMatched[remoteIndex] = true;
+ break;
+ }
+ }
+ }
+
+ // Check that all previously matched items are still matched
+ for (let localIndex = 0; localIndex < localHistory.length; localIndex++) {
+ if (localMatched[localIndex]) {
+ continue;
+ }
+ const lhi = localHistory[localIndex];
+ if (lhi.matchedExchangeTransaction) {
+ // Don't use for further matching
+ localMatched[localIndex] = true;
+ // FIXME: emit some error here!
+ throw Error("previously matched reserve history item now unmatched");
+ }
+ }
+
+ // Next, find out if there are any exact new matches between local and remote
+ // history items
+ for (let localIndex = 0; localIndex < localHistory.length; localIndex++) {
+ if (localMatched[localIndex]) {
+ continue;
+ }
+ const lhi = localHistory[localIndex];
+ for (
+ let remoteIndex = 0;
+ remoteIndex < remoteHistory.length;
+ remoteIndex++
+ ) {
+ const rhi = remoteHistory[remoteIndex];
+ if (remoteMatched[remoteIndex]) {
+ continue;
+ }
+ if (isLocalRemoteHistoryMatch(lhi, rhi)) {
+ localMatched[localIndex] = true;
+ remoteMatched[remoteIndex] = true;
+ updatedLocalHistory[localIndex].matchedExchangeTransaction = rhi as any;
+ newMatchedItems.push(lhi);
+ break;
+ }
+ }
+ }
+
+ // Finally we add new history items
+ for (let remoteIndex = 0; remoteIndex < remoteHistory.length; remoteIndex++) {
+ if (remoteMatched[remoteIndex]) {
+ continue;
+ }
+ const rhi = remoteHistory[remoteIndex];
+ let newItem: WalletReserveHistoryItem;
+ switch (rhi.type) {
+ case ReserveTransactionType.Closing: {
+ newItem = {
+ type: WalletReserveHistoryItemType.Closing,
+ matchedExchangeTransaction: rhi,
+ };
+ break;
+ }
+ case ReserveTransactionType.Credit: {
+ newItem = {
+ type: WalletReserveHistoryItemType.Credit,
+ matchedExchangeTransaction: rhi,
+ };
+ break;
+ }
+ case ReserveTransactionType.Recoup: {
+ newItem = {
+ type: WalletReserveHistoryItemType.Recoup,
+ matchedExchangeTransaction: rhi,
+ };
+ break;
+ }
+ case ReserveTransactionType.Withdraw: {
+ newItem = {
+ type: WalletReserveHistoryItemType.Withdraw,
+ matchedExchangeTransaction: rhi,
+ };
+ break;
+ }
+ }
+ updatedLocalHistory.push(newItem);
+ newAddedItems.push(newItem);
+ }
+
+ return {
+ updatedLocalHistory,
+ newAddedItems,
+ newMatchedItems,
+ };
+}
diff --git a/packages/taler-wallet-core/src/util/talerconfig.ts b/packages/taler-wallet-core/src/util/talerconfig.ts
new file mode 100644
index 000000000..ec08c352f
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/talerconfig.ts
@@ -0,0 +1,120 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Utilities to handle Taler-style configuration files.
+ *
+ * @author Florian Dold <dold@taler.net>
+ */
+
+/**
+ * Imports
+ */
+import { AmountJson } from "./amounts";
+import * as Amounts from "./amounts";
+
+export class ConfigError extends Error {
+ constructor(message: string) {
+ super();
+ Object.setPrototypeOf(this, ConfigError.prototype);
+ this.name = "ConfigError";
+ this.message = message;
+ }
+}
+
+type OptionMap = { [optionName: string]: string };
+type SectionMap = { [sectionName: string]: OptionMap };
+
+export class ConfigValue<T> {
+ constructor(
+ private sectionName: string,
+ private optionName: string,
+ private val: string | undefined,
+ private converter: (x: string) => T,
+ ) {}
+
+ required(): T {
+ if (!this.val) {
+ throw new ConfigError(
+ `required option [${this.sectionName}]/${this.optionName} not found`,
+ );
+ }
+ return this.converter(this.val);
+ }
+}
+
+export class Configuration {
+ private sectionMap: SectionMap = {};
+
+ loadFromString(s: string): void {
+ const reComment = /^\s*#.*$/;
+ const reSection = /^\s*\[\s*([^\]]*)\s*\]\s*$/;
+ const reParam = /^\s*([^=]+?)\s*=\s*(.*?)\s*$/;
+ const reEmptyLine = /^\s*$/;
+
+ let currentSection: string | undefined = undefined;
+
+ const lines = s.split("\n");
+ for (const line of lines) {
+ console.log("parsing line", JSON.stringify(line));
+ if (reEmptyLine.test(line)) {
+ continue;
+ }
+ if (reComment.test(line)) {
+ continue;
+ }
+ const secMatch = line.match(reSection);
+ if (secMatch) {
+ currentSection = secMatch[1];
+ console.log("setting section to", currentSection);
+ continue;
+ }
+ if (currentSection === undefined) {
+ throw Error("invalid configuration, expected section header");
+ }
+ const paramMatch = line.match(reParam);
+ if (paramMatch) {
+ const optName = paramMatch[1];
+ let val = paramMatch[2];
+ if (val.startsWith('"') && val.endsWith('"')) {
+ val = val.slice(1, val.length - 1);
+ }
+ const sec = this.sectionMap[currentSection] ?? {};
+ this.sectionMap[currentSection] = Object.assign(sec, {
+ [optName]: val,
+ });
+ continue;
+ }
+ throw Error(
+ "invalid configuration, expected section header or option assignment",
+ );
+ }
+
+ console.log("parsed config", JSON.stringify(this.sectionMap, undefined, 2));
+ }
+
+ getString(section: string, option: string): ConfigValue<string> {
+ const val = (this.sectionMap[section] ?? {})[option];
+ return new ConfigValue(section, option, val, (x) => x);
+ }
+
+ getAmount(section: string, option: string): ConfigValue<AmountJson> {
+ const val = (this.sectionMap[section] ?? {})[option];
+ return new ConfigValue(section, option, val, (x) =>
+ Amounts.parseOrThrow(x),
+ );
+ }
+}
diff --git a/packages/taler-wallet-core/src/util/taleruri-test.ts b/packages/taler-wallet-core/src/util/taleruri-test.ts
new file mode 100644
index 000000000..b6c326119
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/taleruri-test.ts
@@ -0,0 +1,184 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 GNUnet e.V.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import test from "ava";
+import {
+ parsePayUri,
+ parseWithdrawUri,
+ parseRefundUri,
+ parseTipUri,
+} from "./taleruri";
+
+test("taler pay url parsing: wrong scheme", (t) => {
+ const url1 = "talerfoo://";
+ const r1 = parsePayUri(url1);
+ t.is(r1, undefined);
+
+ const url2 = "taler://refund/a/b/c/d/e/f";
+ const r2 = parsePayUri(url2);
+ t.is(r2, undefined);
+});
+
+test("taler pay url parsing: defaults", (t) => {
+ const url1 = "taler://pay/example.com/myorder/";
+ const r1 = parsePayUri(url1);
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.is(r1.merchantBaseUrl, "https://example.com/");
+ t.is(r1.sessionId, "");
+
+ const url2 = "taler://pay/example.com/myorder/mysession";
+ const r2 = parsePayUri(url2);
+ if (!r2) {
+ t.fail();
+ return;
+ }
+ t.is(r2.merchantBaseUrl, "https://example.com/");
+ t.is(r2.sessionId, "mysession");
+});
+
+test("taler pay url parsing: instance", (t) => {
+ const url1 = "taler://pay/example.com/instances/myinst/myorder/";
+ const r1 = parsePayUri(url1);
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.is(r1.merchantBaseUrl, "https://example.com/instances/myinst/");
+ t.is(r1.orderId, "myorder");
+});
+
+test("taler pay url parsing (claim token)", (t) => {
+ const url1 = "taler://pay/example.com/instances/myinst/myorder/?c=ASDF";
+ const r1 = parsePayUri(url1);
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.is(r1.merchantBaseUrl, "https://example.com/instances/myinst/");
+ t.is(r1.orderId, "myorder");
+ t.is(r1.claimToken, "ASDF");
+});
+
+test("taler refund uri parsing: non-https #1", (t) => {
+ const url1 = "taler+http://refund/example.com/myorder";
+ const r1 = parseRefundUri(url1);
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.is(r1.merchantBaseUrl, "http://example.com/");
+ t.is(r1.orderId, "myorder");
+});
+
+test("taler pay uri parsing: non-https", (t) => {
+ const url1 = "taler+http://pay/example.com/myorder/";
+ const r1 = parsePayUri(url1);
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.is(r1.merchantBaseUrl, "http://example.com/");
+ t.is(r1.orderId, "myorder");
+});
+
+test("taler pay uri parsing: missing session component", (t) => {
+ const url1 = "taler+http://pay/example.com/myorder";
+ const r1 = parsePayUri(url1);
+ if (r1) {
+ t.fail();
+ return;
+ }
+ t.pass();
+});
+
+test("taler withdraw uri parsing", (t) => {
+ const url1 = "taler://withdraw/bank.example.com/12345";
+ const r1 = parseWithdrawUri(url1);
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.is(r1.withdrawalOperationId, "12345");
+ t.is(r1.bankIntegrationApiBaseUrl, "https://bank.example.com/");
+});
+
+test("taler withdraw uri parsing (http)", (t) => {
+ const url1 = "taler+http://withdraw/bank.example.com/12345";
+ const r1 = parseWithdrawUri(url1);
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.is(r1.withdrawalOperationId, "12345");
+ t.is(r1.bankIntegrationApiBaseUrl, "http://bank.example.com/");
+});
+
+test("taler refund uri parsing", (t) => {
+ const url1 = "taler://refund/merchant.example.com/1234";
+ const r1 = parseRefundUri(url1);
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.is(r1.merchantBaseUrl, "https://merchant.example.com/");
+ t.is(r1.orderId, "1234");
+});
+
+test("taler refund uri parsing with instance", (t) => {
+ const url1 = "taler://refund/merchant.example.com/instances/myinst/1234";
+ const r1 = parseRefundUri(url1);
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.is(r1.orderId, "1234");
+ t.is(r1.merchantBaseUrl, "https://merchant.example.com/instances/myinst/");
+});
+
+test("taler tip pickup uri", (t) => {
+ const url1 = "taler://tip/merchant.example.com/tipid";
+ const r1 = parseTipUri(url1);
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.is(r1.merchantBaseUrl, "https://merchant.example.com/");
+});
+
+test("taler tip pickup uri with instance", (t) => {
+ const url1 = "taler://tip/merchant.example.com/instances/tipm/tipid";
+ const r1 = parseTipUri(url1);
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.is(r1.merchantBaseUrl, "https://merchant.example.com/instances/tipm/");
+ t.is(r1.merchantTipId, "tipid");
+});
+
+test("taler tip pickup uri with instance and prefix", (t) => {
+ const url1 = "taler://tip/merchant.example.com/my/pfx/tipm/tipid";
+ const r1 = parseTipUri(url1);
+ if (!r1) {
+ t.fail();
+ return;
+ }
+ t.is(r1.merchantBaseUrl, "https://merchant.example.com/my/pfx/tipm/");
+ t.is(r1.merchantTipId, "tipid");
+});
diff --git a/packages/taler-wallet-core/src/util/taleruri.d.ts.map b/packages/taler-wallet-core/src/util/taleruri.d.ts.map
new file mode 100644
index 000000000..36c16c889
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/taleruri.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"taleruri.d.ts","sourceRoot":"","sources":["taleruri.ts"],"names":[],"mappings":"AAkBA,MAAM,WAAW,YAAY;IAC3B,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;CAChC;AAED,MAAM,WAAW,iBAAiB;IAChC,yBAAyB,EAAE,MAAM,CAAC;IAClC,qBAAqB,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,eAAe;IAC9B,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,MAAM,GAAG,iBAAiB,GAAG,SAAS,CAoBzE;AAED,0BAAkB,YAAY;IAC5B,QAAQ,cAAc;IACtB,aAAa,mBAAmB;IAChC,QAAQ,cAAc;IACtB,WAAW,iBAAiB;IAC5B,kBAAkB,yBAAyB;IAC3C,OAAO,YAAY;CACpB;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,MAAM,GAAG,YAAY,CA2BxD;AA0BD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAyB/D;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAoB/D;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAoBrE"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/util/taleruri.ts b/packages/taler-wallet-core/src/util/taleruri.ts
new file mode 100644
index 000000000..43a869afe
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/taleruri.ts
@@ -0,0 +1,215 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019-2020 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { URLSearchParams } from "./url";
+
+export interface PayUriResult {
+ merchantBaseUrl: string;
+ orderId: string;
+ sessionId: string;
+ claimToken: string | undefined;
+}
+
+export interface WithdrawUriResult {
+ bankIntegrationApiBaseUrl: string;
+ withdrawalOperationId: string;
+}
+
+export interface RefundUriResult {
+ merchantBaseUrl: string;
+ orderId: string;
+}
+
+export interface TipUriResult {
+ merchantTipId: string;
+ merchantBaseUrl: string;
+}
+
+/**
+ * Parse a taler[+http]://withdraw URI.
+ * Return undefined if not passed a valid URI.
+ */
+export function parseWithdrawUri(s: string): WithdrawUriResult | undefined {
+ const pi = parseProtoInfo(s, "withdraw");
+ if (!pi) {
+ return undefined;
+ }
+ const parts = pi.rest.split("/");
+
+ if (parts.length < 2) {
+ return undefined;
+ }
+
+ const host = parts[0].toLowerCase();
+ const pathSegments = parts.slice(1, parts.length - 1);
+ const withdrawId = parts[parts.length - 1];
+ const p = [host, ...pathSegments].join("/");
+
+ return {
+ bankIntegrationApiBaseUrl: `${pi.innerProto}://${p}/`,
+ withdrawalOperationId: withdrawId,
+ };
+}
+
+export const enum TalerUriType {
+ TalerPay = "taler-pay",
+ TalerWithdraw = "taler-withdraw",
+ TalerTip = "taler-tip",
+ TalerRefund = "taler-refund",
+ TalerNotifyReserve = "taler-notify-reserve",
+ Unknown = "unknown",
+}
+
+/**
+ * Classify a taler:// URI.
+ */
+export function classifyTalerUri(s: string): TalerUriType {
+ const sl = s.toLowerCase();
+ if (sl.startsWith("taler://pay/")) {
+ return TalerUriType.TalerPay;
+ }
+ if (sl.startsWith("taler+http://pay/")) {
+ return TalerUriType.TalerPay;
+ }
+ if (sl.startsWith("taler://tip/")) {
+ return TalerUriType.TalerTip;
+ }
+ if (sl.startsWith("taler+http://tip/")) {
+ return TalerUriType.TalerTip;
+ }
+ if (sl.startsWith("taler://refund/")) {
+ return TalerUriType.TalerRefund;
+ }
+ if (sl.startsWith("taler+http://refund/")) {
+ return TalerUriType.TalerRefund;
+ }
+ if (sl.startsWith("taler://withdraw/")) {
+ return TalerUriType.TalerWithdraw;
+ }
+ if (sl.startsWith("taler://notify-reserve/")) {
+ return TalerUriType.TalerNotifyReserve;
+ }
+ return TalerUriType.Unknown;
+}
+
+interface TalerUriProtoInfo {
+ innerProto: "http" | "https";
+ rest: string;
+}
+
+function parseProtoInfo(
+ s: string,
+ action: string,
+): TalerUriProtoInfo | undefined {
+ const pfxPlain = `taler://${action}/`;
+ const pfxHttp = `taler+http://${action}/`;
+ if (s.toLowerCase().startsWith(pfxPlain)) {
+ return {
+ innerProto: "https",
+ rest: s.substring(pfxPlain.length),
+ };
+ } else if (s.toLowerCase().startsWith(pfxHttp)) {
+ return {
+ innerProto: "http",
+ rest: s.substring(pfxHttp.length),
+ };
+ } else {
+ return undefined;
+ }
+}
+
+/**
+ * Parse a taler[+http]://pay URI.
+ * Return undefined if not passed a valid URI.
+ */
+export function parsePayUri(s: string): PayUriResult | undefined {
+ const pi = parseProtoInfo(s, "pay");
+ if (!pi) {
+ return undefined;
+ }
+ const c = pi?.rest.split("?");
+ const q = new URLSearchParams(c[1] ?? "");
+ const claimToken = q.get("c") ?? undefined;
+ const parts = c[0].split("/");
+ if (parts.length < 3) {
+ return undefined;
+ }
+ const host = parts[0].toLowerCase();
+ const sessionId = parts[parts.length - 1];
+ const orderId = parts[parts.length - 2];
+ const pathSegments = parts.slice(1, parts.length - 2);
+ const p = [host, ...pathSegments].join("/");
+ const merchantBaseUrl = `${pi.innerProto}://${p}/`;
+
+ return {
+ merchantBaseUrl,
+ orderId,
+ sessionId: sessionId,
+ claimToken,
+ };
+}
+
+/**
+ * Parse a taler[+http]://tip URI.
+ * Return undefined if not passed a valid URI.
+ */
+export function parseTipUri(s: string): TipUriResult | undefined {
+ const pi = parseProtoInfo(s, "tip");
+ if (!pi) {
+ return undefined;
+ }
+ const c = pi?.rest.split("?");
+ const parts = c[0].split("/");
+ if (parts.length < 2) {
+ return undefined;
+ }
+ const host = parts[0].toLowerCase();
+ const tipId = parts[parts.length - 1];
+ const pathSegments = parts.slice(1, parts.length - 1);
+ const p = [host, ...pathSegments].join("/");
+ const merchantBaseUrl = `${pi.innerProto}://${p}/`;
+
+ return {
+ merchantBaseUrl,
+ merchantTipId: tipId,
+ };
+}
+
+/**
+ * Parse a taler[+http]://refund URI.
+ * Return undefined if not passed a valid URI.
+ */
+export function parseRefundUri(s: string): RefundUriResult | undefined {
+ const pi = parseProtoInfo(s, "refund");
+ if (!pi) {
+ return undefined;
+ }
+ const c = pi?.rest.split("?");
+ const parts = c[0].split("/");
+ if (parts.length < 2) {
+ return undefined;
+ }
+ const host = parts[0].toLowerCase();
+ const orderId = parts[parts.length - 1];
+ const pathSegments = parts.slice(1, parts.length - 1);
+ const p = [host, ...pathSegments].join("/");
+ const merchantBaseUrl = `${pi.innerProto}://${p}/`;
+
+ return {
+ merchantBaseUrl,
+ orderId,
+ };
+}
diff --git a/packages/taler-wallet-core/src/util/testvectors.ts b/packages/taler-wallet-core/src/util/testvectors.ts
new file mode 100644
index 000000000..57ac6e992
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/testvectors.ts
@@ -0,0 +1,36 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports
+ */
+import {
+ setupRefreshPlanchet,
+ encodeCrock,
+ getRandomBytes,
+} from "../crypto/talerCrypto";
+
+export function printTestVectors() {
+ const secretSeed = getRandomBytes(64);
+ const coinIndex = Math.ceil(Math.random() * 100);
+ const p = setupRefreshPlanchet(secretSeed, coinIndex);
+ console.log("setupRefreshPlanchet");
+ console.log(` (in) secret seed: ${encodeCrock(secretSeed)}`);
+ console.log(` (in) coin index: ${coinIndex}`);
+ console.log(` (out) blinding secret: ${encodeCrock(p.bks)}`);
+ console.log(` (out) coin priv: ${encodeCrock(p.coinPriv)}`);
+ console.log(` (out) coin pub: ${encodeCrock(p.coinPub)}`);
+}
diff --git a/packages/taler-wallet-core/src/util/time.d.ts.map b/packages/taler-wallet-core/src/util/time.d.ts.map
new file mode 100644
index 000000000..c38a23356
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/time.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"time.d.ts","sourceRoot":"","sources":["time.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAA0B,MAAM,SAAS,CAAC;AAkBxD;;GAEG;AAEH,qBAAa,SAAS;IACpB;;OAEG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;CACjC;AAED,MAAM,WAAW,QAAQ;IACvB;;OAEG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;CACnC;AAID,wBAAgB,sBAAsB,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAEvD;AAED,wBAAgB,eAAe,IAAI,SAAS,CAI3C;AAED,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,SAAS,EACnB,GAAG,YAAoB,GACtB,QAAQ,CAWV;AAED,wBAAgB,YAAY,CAAC,EAAE,EAAE,SAAS,EAAE,EAAE,EAAE,SAAS,GAAG,SAAS,CAQpE;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,EAAE,EAAE,SAAS,GAAG,SAAS,CAOlE;AAED,wBAAgB,WAAW,CAAC,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,QAAQ,GAAG,QAAQ,CAQhE;AAED,wBAAgB,YAAY,CAAC,EAAE,EAAE,SAAS,EAAE,EAAE,EAAE,SAAS,GAAG,MAAM,CAiBjE;AAED,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,SAAS,EAAE,CAAC,EAAE,QAAQ,GAAG,SAAS,CAK1E;AAED,wBAAgB,0BAA0B,CACxC,EAAE,EAAE,SAAS,EACb,CAAC,EAAE,QAAQ,GACV,SAAS,CAQX;AAED,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,SAAS,GAAG,MAAM,CAKvD;AAED,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,SAAS,EAAE,EAAE,EAAE,SAAS,GAAG,QAAQ,CAQ1E;AAED,wBAAgB,kBAAkB,CAChC,CAAC,EAAE,SAAS,EACZ,KAAK,EAAE,SAAS,EAChB,GAAG,EAAE,SAAS,GACb,OAAO,CAQT;AAED,eAAO,MAAM,iBAAiB,EAAE,KAAK,CAAC,SAAS,CAc9C,CAAC;AAEF,eAAO,MAAM,gBAAgB,EAAE,KAAK,CAAC,QAAQ,CAc5C,CAAC"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/util/time.ts b/packages/taler-wallet-core/src/util/time.ts
new file mode 100644
index 000000000..5c2f49d12
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/time.ts
@@ -0,0 +1,198 @@
+import { Codec, renderContext, Context } from "./codec";
+
+/*
+ This file is part of GNU Taler
+ (C) 2017-2019 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Helpers for relative and absolute time.
+ */
+
+export class Timestamp {
+ /**
+ * Timestamp in milliseconds.
+ */
+ readonly t_ms: number | "never";
+}
+
+export interface Duration {
+ /**
+ * Duration in milliseconds.
+ */
+ readonly d_ms: number | "forever";
+}
+
+let timeshift = 0;
+
+export function setDangerousTimetravel(dt: number): void {
+ timeshift = dt;
+}
+
+export function getTimestampNow(): Timestamp {
+ return {
+ t_ms: new Date().getTime() + timeshift,
+ };
+}
+
+export function getDurationRemaining(
+ deadline: Timestamp,
+ now = getTimestampNow(),
+): Duration {
+ if (deadline.t_ms === "never") {
+ return { d_ms: "forever" };
+ }
+ if (now.t_ms === "never") {
+ throw Error("invalid argument for 'now'");
+ }
+ if (deadline.t_ms < now.t_ms) {
+ return { d_ms: 0 };
+ }
+ return { d_ms: deadline.t_ms - now.t_ms };
+}
+
+export function timestampMin(t1: Timestamp, t2: Timestamp): Timestamp {
+ if (t1.t_ms === "never") {
+ return { t_ms: t2.t_ms };
+ }
+ if (t2.t_ms === "never") {
+ return { t_ms: t2.t_ms };
+ }
+ return { t_ms: Math.min(t1.t_ms, t2.t_ms) };
+}
+
+/**
+ * Truncate a timestamp so that that it represents a multiple
+ * of seconds. The timestamp is always rounded down.
+ */
+export function timestampTruncateToSecond(t1: Timestamp): Timestamp {
+ if (t1.t_ms === "never") {
+ return { t_ms: "never" };
+ }
+ return {
+ t_ms: Math.floor(t1.t_ms / 1000) * 1000,
+ };
+}
+
+export function durationMin(d1: Duration, d2: Duration): Duration {
+ if (d1.d_ms === "forever") {
+ return { d_ms: d2.d_ms };
+ }
+ if (d2.d_ms === "forever") {
+ return { d_ms: d2.d_ms };
+ }
+ return { d_ms: Math.min(d1.d_ms, d2.d_ms) };
+}
+
+export function timestampCmp(t1: Timestamp, t2: Timestamp): number {
+ if (t1.t_ms === "never") {
+ if (t2.t_ms === "never") {
+ return 0;
+ }
+ return 1;
+ }
+ if (t2.t_ms === "never") {
+ return -1;
+ }
+ if (t1.t_ms == t2.t_ms) {
+ return 0;
+ }
+ if (t1.t_ms > t2.t_ms) {
+ return 1;
+ }
+ return -1;
+}
+
+export function timestampAddDuration(t1: Timestamp, d: Duration): Timestamp {
+ if (t1.t_ms === "never" || d.d_ms === "forever") {
+ return { t_ms: "never" };
+ }
+ return { t_ms: t1.t_ms + d.d_ms };
+}
+
+export function timestampSubtractDuraction(
+ t1: Timestamp,
+ d: Duration,
+): Timestamp {
+ if (t1.t_ms === "never") {
+ return { t_ms: "never" };
+ }
+ if (d.d_ms === "forever") {
+ return { t_ms: 0 };
+ }
+ return { t_ms: Math.max(0, t1.t_ms - d.d_ms) };
+}
+
+export function stringifyTimestamp(t: Timestamp): string {
+ if (t.t_ms === "never") {
+ return "never";
+ }
+ return new Date(t.t_ms).toISOString();
+}
+
+export function timestampDifference(t1: Timestamp, t2: Timestamp): Duration {
+ if (t1.t_ms === "never") {
+ return { d_ms: "forever" };
+ }
+ if (t2.t_ms === "never") {
+ return { d_ms: "forever" };
+ }
+ return { d_ms: Math.abs(t1.t_ms - t2.t_ms) };
+}
+
+export function timestampIsBetween(
+ t: Timestamp,
+ start: Timestamp,
+ end: Timestamp,
+): boolean {
+ if (timestampCmp(t, start) < 0) {
+ return false;
+ }
+ if (timestampCmp(t, end) > 0) {
+ return false;
+ }
+ return true;
+}
+
+export const codecForTimestamp: Codec<Timestamp> = {
+ decode(x: any, c?: Context): Timestamp {
+ const t_ms = x.t_ms;
+ if (typeof t_ms === "string") {
+ if (t_ms === "never") {
+ return { t_ms: "never" };
+ }
+ throw Error(`expected timestamp at ${renderContext(c)}`);
+ }
+ if (typeof t_ms === "number") {
+ return { t_ms };
+ }
+ throw Error(`expected timestamp at ${renderContext(c)}`);
+ },
+};
+
+export const codecForDuration: Codec<Duration> = {
+ decode(x: any, c?: Context): Duration {
+ const d_ms = x.d_ms;
+ if (typeof d_ms === "string") {
+ if (d_ms === "forever") {
+ return { d_ms: "forever" };
+ }
+ throw Error(`expected duration at ${renderContext(c)}`);
+ }
+ if (typeof d_ms === "number") {
+ return { d_ms };
+ }
+ throw Error(`expected duration at ${renderContext(c)}`);
+ },
+};
diff --git a/packages/taler-wallet-core/src/util/timer.d.ts.map b/packages/taler-wallet-core/src/util/timer.d.ts.map
new file mode 100644
index 000000000..c2b5e536e
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/timer.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"timer.d.ts","sourceRoot":"","sources":["timer.ts"],"names":[],"mappings":"AAgBA;;;;;GAKG;AAEH;;GAEG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAKlC;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,KAAK,IAAI,IAAI,CAAC;CACf;AAkBD;;GAEG;AACH,eAAO,MAAM,cAAc,EAAE,MAAM,MAa/B,CAAC;AAEL;;GAEG;AACH,wBAAgB,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,WAAW,CAExE;AAED;;GAEG;AACH,wBAAgB,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,WAAW,CAExE;AASD;;GAEG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,OAAO,CAAS;IAExB,OAAO,CAAC,QAAQ,CAAwC;IAExD,OAAO,CAAC,KAAK,CAAK;IAElB,0BAA0B,IAAI,IAAI;IAWlC,YAAY,CAAC,OAAO,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAU9C,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,WAAW;IAmBzD,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,WAAW;CAkB1D"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/util/timer.ts b/packages/taler-wallet-core/src/util/timer.ts
new file mode 100644
index 000000000..8eab1399c
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/timer.ts
@@ -0,0 +1,165 @@
+/*
+ This file is part of GNU Taler
+ (C) 2017-2019 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Cross-platform timers.
+ *
+ * NodeJS and the browser use slightly different timer API,
+ * this abstracts over these differences.
+ */
+
+/**
+ * Imports.
+ */
+import { Duration } from "./time";
+import { Logger } from "./logging";
+
+const logger = new Logger("timer.ts");
+
+/**
+ * Cancelable timer.
+ */
+export interface TimerHandle {
+ clear(): void;
+}
+
+class IntervalHandle {
+ constructor(public h: any) {}
+
+ clear(): void {
+ clearInterval(this.h);
+ }
+}
+
+class TimeoutHandle {
+ constructor(public h: any) {}
+
+ clear(): void {
+ clearTimeout(this.h);
+ }
+}
+
+/**
+ * Get a performance counter in milliseconds.
+ */
+export const performanceNow: () => number = (() => {
+ // @ts-ignore
+ if (typeof process !== "undefined" && process.hrtime) {
+ return () => {
+ const t = process.hrtime();
+ return t[0] * 1e9 + t[1];
+ };
+ }
+
+ // @ts-ignore
+ if (typeof performance !== "undefined") {
+ // @ts-ignore
+ return () => performance.now();
+ }
+
+ return () => 0;
+})();
+
+/**
+ * Call a function every time the delay given in milliseconds passes.
+ */
+export function every(delayMs: number, callback: () => void): TimerHandle {
+ return new IntervalHandle(setInterval(callback, delayMs));
+}
+
+/**
+ * Call a function after the delay given in milliseconds passes.
+ */
+export function after(delayMs: number, callback: () => void): TimerHandle {
+ return new TimeoutHandle(setTimeout(callback, delayMs));
+}
+
+const nullTimerHandle = {
+ clear() {
+ // do nothing
+ return;
+ },
+};
+
+/**
+ * Group of timers that can be destroyed at once.
+ */
+export class TimerGroup {
+ private stopped = false;
+
+ private timerMap: { [index: number]: TimerHandle } = {};
+
+ private idGen = 1;
+
+ stopCurrentAndFutureTimers(): void {
+ this.stopped = true;
+ for (const x in this.timerMap) {
+ if (!this.timerMap.hasOwnProperty(x)) {
+ continue;
+ }
+ this.timerMap[x].clear();
+ delete this.timerMap[x];
+ }
+ }
+
+ resolveAfter(delayMs: Duration): Promise<void> {
+ return new Promise<void>((resolve, reject) => {
+ if (delayMs.d_ms !== "forever") {
+ this.after(delayMs.d_ms, () => {
+ resolve();
+ });
+ }
+ });
+ }
+
+ after(delayMs: number, callback: () => void): TimerHandle {
+ if (this.stopped) {
+ logger.warn("dropping timer since timer group is stopped");
+ return nullTimerHandle;
+ }
+ const h = after(delayMs, callback);
+ const myId = this.idGen++;
+ this.timerMap[myId] = h;
+
+ const tm = this.timerMap;
+
+ return {
+ clear() {
+ h.clear();
+ delete tm[myId];
+ },
+ };
+ }
+
+ every(delayMs: number, callback: () => void): TimerHandle {
+ if (this.stopped) {
+ logger.warn("dropping timer since timer group is stopped");
+ return nullTimerHandle;
+ }
+ const h = every(delayMs, callback);
+ const myId = this.idGen++;
+ this.timerMap[myId] = h;
+
+ const tm = this.timerMap;
+
+ return {
+ clear() {
+ h.clear();
+ delete tm[myId];
+ },
+ };
+ }
+}
diff --git a/packages/taler-wallet-core/src/util/url.d.ts.map b/packages/taler-wallet-core/src/util/url.d.ts.map
new file mode 100644
index 000000000..f238a9b5a
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/url.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"url.d.ts","sourceRoot":"","sources":["url.ts"],"names":[],"mappings":"AAgBA,UAAU,GAAG;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,IAAI,MAAM,CAAC;IACnB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,YAAY,EAAE,eAAe,CAAC;IACvC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,IAAI,MAAM,CAAC;CAClB;AAED,UAAU,eAAe;IACvB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1C,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IACjC,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC/B,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAC3B,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACvC,IAAI,IAAI,IAAI,CAAC;IACb,QAAQ,IAAI,MAAM,CAAC;IACnB,OAAO,CACL,UAAU,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,KAAK,IAAI,EACzE,OAAO,CAAC,EAAE,GAAG,GACZ,IAAI,CAAC;CACT;AAED,MAAM,WAAW,mBAAmB;IAClC,KAAI,IAAI,CAAC,EAAE,MAAM,EAAE,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,GAAG,eAAe,GAAG,eAAe,CAAC;CAC7F;AAED,MAAM,WAAW,OAAO;IACtB,KAAI,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,GAAG,GAAG,GAAG,CAAC;CAC5C;AAQD,eAAO,MAAM,GAAG,EAAE,OAAc,CAAC;AASjC,eAAO,MAAM,eAAe,EAAE,mBAAsC,CAAC"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/util/url.ts b/packages/taler-wallet-core/src/util/url.ts
new file mode 100644
index 000000000..b50b4b466
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/url.ts
@@ -0,0 +1,74 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+interface URL {
+ hash: string;
+ host: string;
+ hostname: string;
+ href: string;
+ toString(): string;
+ readonly origin: string;
+ password: string;
+ pathname: string;
+ port: string;
+ protocol: string;
+ search: string;
+ readonly searchParams: URLSearchParams;
+ username: string;
+ toJSON(): string;
+}
+
+interface URLSearchParams {
+ append(name: string, value: string): void;
+ delete(name: string): void;
+ get(name: string): string | null;
+ getAll(name: string): string[];
+ has(name: string): boolean;
+ set(name: string, value: string): void;
+ sort(): void;
+ toString(): string;
+ forEach(
+ callbackfn: (value: string, key: string, parent: URLSearchParams) => void,
+ thisArg?: any,
+ ): void;
+}
+
+export interface URLSearchParamsCtor {
+ new (
+ init?: string[][] | Record<string, string> | string | URLSearchParams,
+ ): URLSearchParams;
+}
+
+export interface URLCtor {
+ new (url: string, base?: string | URL): URL;
+}
+
+// @ts-ignore
+const _URL = globalThis.URL;
+if (!_URL) {
+ throw Error("FATAL: URL not available");
+}
+
+export const URL: URLCtor = _URL;
+
+// @ts-ignore
+const _URLSearchParams = globalThis.URLSearchParams;
+
+if (!_URLSearchParams) {
+ throw Error("FATAL: URLSearchParams not available");
+}
+
+export const URLSearchParams: URLSearchParamsCtor = _URLSearchParams;
diff --git a/packages/taler-wallet-core/src/util/wire.ts b/packages/taler-wallet-core/src/util/wire.ts
new file mode 100644
index 000000000..95e324f3c
--- /dev/null
+++ b/packages/taler-wallet-core/src/util/wire.ts
@@ -0,0 +1,51 @@
+/*
+ This file is part of TALER
+ (C) 2017 GNUnet e.V.
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Display and manipulate wire information.
+ *
+ * Right now, all types are hard-coded. In the future, there might be plugins / configurable
+ * methods or support for the "payto://" URI scheme.
+ */
+
+/**
+ * Imports.
+ */
+import * as i18n from "../i18n";
+
+/**
+ * Short summary of the wire information.
+ *
+ * Might abbreviate and return the same summary for different
+ * wire details.
+ */
+export function summarizeWire(w: any): string {
+ if (!w.type) {
+ return i18n.str`Invalid Wire`;
+ }
+ switch (w.type.toLowerCase()) {
+ case "test":
+ if (!w.account_number && w.account_number !== 0) {
+ return i18n.str`Invalid Test Wire Detail`;
+ }
+ if (!w.bank_uri) {
+ return i18n.str`Invalid Test Wire Detail`;
+ }
+ return i18n.str`Test Wire Acct #${w.account_number} on ${w.bank_uri}`;
+ default:
+ return i18n.str`Unknown Wire Detail`;
+ }
+}
diff --git a/packages/taler-wallet-core/src/wallet-test.ts b/packages/taler-wallet-core/src/wallet-test.ts
new file mode 100644
index 000000000..4b06accf2
--- /dev/null
+++ b/packages/taler-wallet-core/src/wallet-test.ts
@@ -0,0 +1,121 @@
+/*
+ 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 { AmountJson } from "./util/amounts";
+import * as Amounts from "./util/amounts";
+import { selectPayCoins, AvailableCoinInfo } from "./operations/pay";
+
+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),
+ };
+}
+
+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(acis, a("EUR:2.0"), a("EUR:0"), a("EUR:0.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(acis, a("EUR:2.0"), a("EUR:0"), a("EUR:0.5"));
+ 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(acis, a("EUR:2.0"), a("EUR:0"), a("EUR:0.5"));
+ 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(acis, a("EUR:2.0"), a("EUR:0"), a("EUR:0.5"));
+ 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(acis, a("EUR:4.0"), a("EUR:0"), a("EUR:0.2"));
+ 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(acis, a("EUR:2.0"), a("EUR:0"), a("EUR:0.2"));
+ t.true(!res);
+ t.pass();
+});
diff --git a/packages/taler-wallet-core/src/wallet.d.ts.map b/packages/taler-wallet-core/src/wallet.d.ts.map
new file mode 100644
index 000000000..4c6b1964c
--- /dev/null
+++ b/packages/taler-wallet-core/src/wallet.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"wallet.d.ts","sourceRoot":"","sources":["wallet.ts"],"names":[],"mappings":"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"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts
new file mode 100644
index 000000000..4a409f58d
--- /dev/null
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -0,0 +1,882 @@
+/*
+ This file is part of GNU Taler
+ (C) 2015-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/>
+ */
+
+/**
+ * High-level wallet operations that should be indepentent from the underlying
+ * browser extension interface.
+ */
+
+/**
+ * Imports.
+ */
+import { CryptoWorkerFactory } from "./crypto/workers/cryptoApi";
+import { HttpRequestLibrary } from "./util/http";
+import { Database } from "./util/query";
+
+import { Amounts, AmountJson } from "./util/amounts";
+
+import {
+ getExchangeWithdrawalInfo,
+ getWithdrawalDetailsForUri,
+} from "./operations/withdraw";
+
+import {
+ preparePayForUri,
+ refuseProposal,
+ confirmPay,
+ processDownloadProposal,
+ processPurchasePay,
+} from "./operations/pay";
+
+import {
+ CoinRecord,
+ CurrencyRecord,
+ DenominationRecord,
+ ExchangeRecord,
+ PurchaseRecord,
+ ReserveRecord,
+ Stores,
+ ReserveRecordStatus,
+ CoinSourceType,
+ RefundState,
+} from "./types/dbTypes";
+import { CoinDumpJson, WithdrawUriInfoResponse } from "./types/talerTypes";
+import {
+ BenchmarkResult,
+ ConfirmPayResult,
+ ReturnCoinsRequest,
+ SenderWireInfos,
+ TipStatus,
+ PreparePayResult,
+ AcceptWithdrawalResponse,
+ PurchaseDetails,
+ RefreshReason,
+ ExchangeListItem,
+ ExchangesListRespose,
+ ManualWithdrawalDetails,
+ GetExchangeTosResult,
+ AcceptManualWithdrawalResult,
+ BalancesResponse,
+} from "./types/walletTypes";
+import { Logger } from "./util/logging";
+
+import { assertUnreachable } from "./util/assertUnreachable";
+
+import {
+ updateExchangeFromUrl,
+ getExchangeTrust,
+ getExchangePaytoUri,
+ acceptExchangeTermsOfService,
+} from "./operations/exchanges";
+import {
+ processReserve,
+ createTalerWithdrawReserve,
+ forceQueryReserve,
+ getFundingPaytoUris,
+} from "./operations/reserves";
+
+import { InternalWalletState } from "./operations/state";
+import { createReserve } from "./operations/reserves";
+import { processRefreshGroup, createRefreshGroup } from "./operations/refresh";
+import { processWithdrawGroup } from "./operations/withdraw";
+import { getPendingOperations } from "./operations/pending";
+import { getBalances } from "./operations/balance";
+import { acceptTip, getTipStatus, processTip } from "./operations/tip";
+import { TimerGroup } from "./util/timer";
+import { AsyncCondition } from "./util/promiseUtils";
+import { AsyncOpMemoSingle } from "./util/asyncMemo";
+import {
+ PendingOperationInfo,
+ PendingOperationsResponse,
+ PendingOperationType,
+} from "./types/pending";
+import { WalletNotification, NotificationType } from "./types/notifications";
+import { processPurchaseQueryRefund, applyRefund } from "./operations/refund";
+import { durationMin, Duration } from "./util/time";
+import { processRecoupGroup } from "./operations/recoup";
+import { OperationFailedAndReportedError } from "./operations/errors";
+import {
+ TransactionsRequest,
+ TransactionsResponse,
+} from "./types/transactions";
+import { getTransactions } from "./operations/transactions";
+import { withdrawTestBalance } from "./operations/testing";
+
+const builtinCurrencies: CurrencyRecord[] = [
+ {
+ auditors: [
+ {
+ auditorPub: "BW9DC48PHQY4NH011SHHX36DZZ3Q22Y6X7FZ1VD1CMZ2PTFZ6PN0",
+ baseUrl: "https://auditor.demo.taler.net/",
+ expirationStamp: new Date(2027, 1).getTime(),
+ },
+ ],
+ exchanges: [],
+ fractionalDigits: 2,
+ name: "KUDOS",
+ },
+];
+
+const logger = new Logger("wallet.ts");
+
+/**
+ * The platform-independent wallet implementation.
+ */
+export class Wallet {
+ private ws: InternalWalletState;
+ private timerGroup: TimerGroup = new TimerGroup();
+ private latch = new AsyncCondition();
+ private stopped = false;
+ private memoRunRetryLoop = new AsyncOpMemoSingle<void>();
+
+ get db(): Database {
+ return this.ws.db;
+ }
+
+ constructor(
+ db: Database,
+ http: HttpRequestLibrary,
+ cryptoWorkerFactory: CryptoWorkerFactory,
+ ) {
+ this.ws = new InternalWalletState(db, http, cryptoWorkerFactory);
+ }
+
+ getExchangePaytoUri(
+ exchangeBaseUrl: string,
+ supportedTargetTypes: string[],
+ ): Promise<string> {
+ return getExchangePaytoUri(this.ws, exchangeBaseUrl, supportedTargetTypes);
+ }
+
+ async getWithdrawalDetailsForAmount(
+ exchangeBaseUrl: string,
+ amount: AmountJson,
+ ): Promise<ManualWithdrawalDetails> {
+ const wi = await getExchangeWithdrawalInfo(
+ this.ws,
+ exchangeBaseUrl,
+ amount,
+ );
+ const paytoUris = wi.exchangeInfo.wireInfo?.accounts.map(
+ (x) => x.payto_uri,
+ );
+ if (!paytoUris) {
+ throw Error("exchange is in invalid state");
+ }
+ return {
+ amountRaw: Amounts.stringify(amount),
+ amountEffective: Amounts.stringify(wi.selectedDenoms.totalCoinValue),
+ paytoUris,
+ tosAccepted: wi.termsOfServiceAccepted,
+ };
+ }
+
+ addNotificationListener(f: (n: WalletNotification) => void): void {
+ this.ws.addNotificationListener(f);
+ }
+
+ /**
+ * Execute one operation based on the pending operation info record.
+ */
+ async processOnePendingOperation(
+ pending: PendingOperationInfo,
+ forceNow = false,
+ ): Promise<void> {
+ logger.trace(`running pending ${JSON.stringify(pending, undefined, 2)}`);
+ switch (pending.type) {
+ case PendingOperationType.Bug:
+ // Nothing to do, will just be displayed to the user
+ return;
+ case PendingOperationType.ExchangeUpdate:
+ await updateExchangeFromUrl(this.ws, pending.exchangeBaseUrl, forceNow);
+ break;
+ case PendingOperationType.Refresh:
+ await processRefreshGroup(this.ws, pending.refreshGroupId, forceNow);
+ break;
+ case PendingOperationType.Reserve:
+ await processReserve(this.ws, pending.reservePub, forceNow);
+ break;
+ case PendingOperationType.Withdraw:
+ await processWithdrawGroup(
+ this.ws,
+ pending.withdrawalGroupId,
+ forceNow,
+ );
+ break;
+ case PendingOperationType.ProposalChoice:
+ // Nothing to do, user needs to accept/reject
+ break;
+ case PendingOperationType.ProposalDownload:
+ await processDownloadProposal(this.ws, pending.proposalId, forceNow);
+ break;
+ case PendingOperationType.TipChoice:
+ // Nothing to do, user needs to accept/reject
+ break;
+ case PendingOperationType.TipPickup:
+ await processTip(this.ws, pending.tipId, forceNow);
+ break;
+ case PendingOperationType.Pay:
+ await processPurchasePay(this.ws, pending.proposalId, forceNow);
+ break;
+ case PendingOperationType.RefundQuery:
+ await processPurchaseQueryRefund(this.ws, pending.proposalId, forceNow);
+ break;
+ case PendingOperationType.Recoup:
+ await processRecoupGroup(this.ws, pending.recoupGroupId, forceNow);
+ break;
+ default:
+ assertUnreachable(pending);
+ }
+ }
+
+ /**
+ * Process pending operations.
+ */
+ public async runPending(forceNow = false): Promise<void> {
+ const onlyDue = !forceNow;
+ const pendingOpsResponse = await this.getPendingOperations({ onlyDue });
+ for (const p of pendingOpsResponse.pendingOperations) {
+ try {
+ await this.processOnePendingOperation(p, forceNow);
+ } catch (e) {
+ if (e instanceof OperationFailedAndReportedError) {
+ console.error(
+ "Operation failed:",
+ JSON.stringify(e.operationError, undefined, 2),
+ );
+ } else {
+ console.error(e);
+ }
+ }
+ }
+ }
+
+ /**
+ * Run the wallet until there are no more pending operations that give
+ * liveness left. The wallet will be in a stopped state when this function
+ * returns without resolving to an exception.
+ */
+ public async runUntilDone(): Promise<void> {
+ let done = false;
+ const p = new Promise((resolve, reject) => {
+ // Run this asynchronously
+ this.addNotificationListener((n) => {
+ if (done) {
+ return;
+ }
+ if (
+ n.type === NotificationType.WaitingForRetry &&
+ n.numGivingLiveness == 0
+ ) {
+ done = true;
+ logger.trace("no liveness-giving operations left");
+ resolve();
+ }
+ });
+ this.runRetryLoop().catch((e) => {
+ console.log("exception in wallet retry loop");
+ reject(e);
+ });
+ });
+ await p;
+ }
+
+ /**
+ * Run the wallet until there are no more pending operations that give
+ * liveness left. The wallet will be in a stopped state when this function
+ * returns without resolving to an exception.
+ */
+ public async runUntilDoneAndStop(): Promise<void> {
+ await this.runUntilDone();
+ logger.trace("stopping after liveness-giving operations done");
+ this.stop();
+ }
+
+ /**
+ * Process pending operations and wait for scheduled operations in
+ * a loop until the wallet is stopped explicitly.
+ */
+ public async runRetryLoop(): Promise<void> {
+ // Make sure we only run one main loop at a time.
+ return this.memoRunRetryLoop.memo(async () => {
+ try {
+ await this.runRetryLoopImpl();
+ } catch (e) {
+ console.error("error during retry loop execution", e);
+ throw e;
+ }
+ });
+ }
+
+ private async runRetryLoopImpl(): Promise<void> {
+ while (!this.stopped) {
+ const pending = await this.getPendingOperations({ onlyDue: true });
+ if (pending.pendingOperations.length === 0) {
+ const allPending = await this.getPendingOperations({ onlyDue: false });
+ let numPending = 0;
+ let numGivingLiveness = 0;
+ for (const p of allPending.pendingOperations) {
+ numPending++;
+ if (p.givesLifeness) {
+ numGivingLiveness++;
+ }
+ }
+ let dt: Duration;
+ if (
+ allPending.pendingOperations.length === 0 ||
+ allPending.nextRetryDelay.d_ms === Number.MAX_SAFE_INTEGER
+ ) {
+ // Wait for 5 seconds
+ dt = { d_ms: 5000 };
+ } else {
+ dt = durationMin({ d_ms: 5000 }, allPending.nextRetryDelay);
+ }
+ const timeout = this.timerGroup.resolveAfter(dt);
+ this.ws.notify({
+ type: NotificationType.WaitingForRetry,
+ numGivingLiveness,
+ numPending,
+ });
+ await Promise.race([timeout, this.latch.wait()]);
+ console.log("timeout done");
+ } else {
+ // FIXME: maybe be a bit smarter about executing these
+ // operations in parallel?
+ for (const p of pending.pendingOperations) {
+ try {
+ await this.processOnePendingOperation(p);
+ } catch (e) {
+ if (e instanceof OperationFailedAndReportedError) {
+ logger.warn("operation processed resulted in reported error");
+ } else {
+ console.error("Uncaught exception", e);
+ this.ws.notify({
+ type: NotificationType.InternalError,
+ message: "uncaught exception",
+ exception: e,
+ });
+ }
+ }
+ this.ws.notify({
+ type: NotificationType.PendingOperationProcessed,
+ });
+ }
+ }
+ }
+ logger.trace("exiting wallet retry loop");
+ }
+
+ /**
+ * Insert the hard-coded defaults for exchanges, coins and
+ * auditors into the database, unless these defaults have
+ * already been applied.
+ */
+ async fillDefaults(): Promise<void> {
+ await this.db.runWithWriteTransaction(
+ [Stores.config, Stores.currencies],
+ async (tx) => {
+ let applied = false;
+ await tx.iter(Stores.config).forEach((x) => {
+ if (x.key == "currencyDefaultsApplied" && x.value == true) {
+ applied = true;
+ }
+ });
+ if (!applied) {
+ for (const c of builtinCurrencies) {
+ await tx.put(Stores.currencies, c);
+ }
+ }
+ },
+ );
+ }
+
+ /**
+ * Check if a payment for the given taler://pay/ URI is possible.
+ *
+ * If the payment is possible, the signature are already generated but not
+ * yet send to the merchant.
+ */
+ async preparePayForUri(talerPayUri: string): Promise<PreparePayResult> {
+ return preparePayForUri(this.ws, talerPayUri);
+ }
+
+ /**
+ * Add a contract to the wallet and sign coins, and send them.
+ */
+ async confirmPay(
+ proposalId: string,
+ sessionIdOverride: string | undefined,
+ ): Promise<ConfirmPayResult> {
+ try {
+ return await confirmPay(this.ws, proposalId, sessionIdOverride);
+ } finally {
+ this.latch.trigger();
+ }
+ }
+
+ /**
+ * First fetch information requred to withdraw from the reserve,
+ * then deplete the reserve, withdrawing coins until it is empty.
+ *
+ * The returned promise resolves once the reserve is set to the
+ * state DORMANT.
+ */
+ async processReserve(reservePub: string): Promise<void> {
+ try {
+ return await processReserve(this.ws, reservePub);
+ } finally {
+ this.latch.trigger();
+ }
+ }
+
+ /**
+ * Create a reserve, but do not flag it as confirmed yet.
+ *
+ * Adds the corresponding exchange as a trusted exchange if it is neither
+ * audited nor trusted already.
+ */
+ async acceptManualWithdrawal(
+ exchangeBaseUrl: string,
+ amount: AmountJson,
+ ): Promise<AcceptManualWithdrawalResult> {
+ try {
+ const resp = await createReserve(this.ws, {
+ amount,
+ exchange: exchangeBaseUrl,
+ });
+ const exchangePaytoUris = await this.db.runWithReadTransaction(
+ [Stores.exchanges, Stores.reserves],
+ (tx) => getFundingPaytoUris(tx, resp.reservePub),
+ );
+ return {
+ reservePub: resp.reservePub,
+ exchangePaytoUris,
+ };
+ } finally {
+ this.latch.trigger();
+ }
+ }
+
+ /**
+ * Check if and how an exchange is trusted and/or audited.
+ */
+ async getExchangeTrust(
+ exchangeInfo: ExchangeRecord,
+ ): Promise<{ isTrusted: boolean; isAudited: boolean }> {
+ return getExchangeTrust(this.ws, exchangeInfo);
+ }
+
+ async getWithdrawalDetailsForUri(
+ talerWithdrawUri: string,
+ ): Promise<WithdrawUriInfoResponse> {
+ return getWithdrawalDetailsForUri(this.ws, talerWithdrawUri);
+ }
+
+ /**
+ * Update or add exchange DB entry by fetching the /keys and /wire information.
+ * Optionally link the reserve entry to the new or existing
+ * exchange entry in then DB.
+ */
+ async updateExchangeFromUrl(
+ baseUrl: string,
+ force = false,
+ ): Promise<ExchangeRecord> {
+ try {
+ return updateExchangeFromUrl(this.ws, baseUrl, force);
+ } finally {
+ this.latch.trigger();
+ }
+ }
+
+ async getExchangeTos(exchangeBaseUrl: string): Promise<GetExchangeTosResult> {
+ const exchange = await this.updateExchangeFromUrl(exchangeBaseUrl);
+ const tos = exchange.termsOfServiceText;
+ const currentEtag = exchange.termsOfServiceLastEtag;
+ if (!tos || !currentEtag) {
+ throw Error("exchange is in invalid state");
+ }
+ return {
+ acceptedEtag: exchange.termsOfServiceAcceptedEtag,
+ currentEtag,
+ tos,
+ };
+ }
+
+ /**
+ * Get detailed balance information, sliced by exchange and by currency.
+ */
+ async getBalances(): Promise<BalancesResponse> {
+ return this.ws.memoGetBalance.memo(() => getBalances(this.ws));
+ }
+
+ async refresh(oldCoinPub: string): Promise<void> {
+ try {
+ const refreshGroupId = await this.db.runWithWriteTransaction(
+ [Stores.refreshGroups],
+ async (tx) => {
+ return await createRefreshGroup(
+ this.ws,
+ tx,
+ [{ coinPub: oldCoinPub }],
+ RefreshReason.Manual,
+ );
+ },
+ );
+ await processRefreshGroup(this.ws, refreshGroupId.refreshGroupId);
+ } catch (e) {
+ this.latch.trigger();
+ }
+ }
+
+ async findExchange(
+ exchangeBaseUrl: string,
+ ): Promise<ExchangeRecord | undefined> {
+ return await this.db.get(Stores.exchanges, exchangeBaseUrl);
+ }
+
+ async getPendingOperations({ onlyDue = false } = {}): Promise<
+ PendingOperationsResponse
+ > {
+ return this.ws.memoGetPending.memo(() =>
+ getPendingOperations(this.ws, { onlyDue }),
+ );
+ }
+
+ async acceptExchangeTermsOfService(
+ exchangeBaseUrl: string,
+ etag: string | undefined,
+ ): Promise<void> {
+ return acceptExchangeTermsOfService(this.ws, exchangeBaseUrl, etag);
+ }
+
+ async getDenoms(exchangeUrl: string): Promise<DenominationRecord[]> {
+ const denoms = await this.db
+ .iterIndex(Stores.denominations.exchangeBaseUrlIndex, exchangeUrl)
+ .toArray();
+ return denoms;
+ }
+
+ /**
+ * Get all exchanges known to the exchange.
+ *
+ * @deprecated Use getExchanges instead
+ */
+ async getExchangeRecords(): Promise<ExchangeRecord[]> {
+ return await this.db.iter(Stores.exchanges).toArray();
+ }
+
+ async getExchanges(): Promise<ExchangesListRespose> {
+ const exchanges: (ExchangeListItem | undefined)[] = await this.db
+ .iter(Stores.exchanges)
+ .map((x) => {
+ const details = x.details;
+ if (!details) {
+ return undefined;
+ }
+ if (!x.addComplete) {
+ return undefined;
+ }
+ if (!x.wireInfo) {
+ return undefined;
+ }
+ return {
+ exchangeBaseUrl: x.baseUrl,
+ currency: details.currency,
+ paytoUris: x.wireInfo.accounts.map((x) => x.payto_uri),
+ };
+ });
+ return {
+ exchanges: exchanges.filter((x) => !!x) as ExchangeListItem[],
+ };
+ }
+
+ async getCurrencies(): Promise<CurrencyRecord[]> {
+ return await this.db.iter(Stores.currencies).toArray();
+ }
+
+ async updateCurrency(currencyRecord: CurrencyRecord): Promise<void> {
+ logger.trace("updating currency to", currencyRecord);
+ await this.db.put(Stores.currencies, currencyRecord);
+ }
+
+ async getReserves(exchangeBaseUrl?: string): Promise<ReserveRecord[]> {
+ if (exchangeBaseUrl) {
+ return await this.db
+ .iter(Stores.reserves)
+ .filter((r) => r.exchangeBaseUrl === exchangeBaseUrl);
+ } else {
+ return await this.db.iter(Stores.reserves).toArray();
+ }
+ }
+
+ async getCoinsForExchange(exchangeBaseUrl: string): Promise<CoinRecord[]> {
+ return await this.db
+ .iter(Stores.coins)
+ .filter((c) => c.exchangeBaseUrl === exchangeBaseUrl);
+ }
+
+ async getCoins(): Promise<CoinRecord[]> {
+ return await this.db.iter(Stores.coins).toArray();
+ }
+
+ /**
+ * Stop ongoing processing.
+ */
+ stop(): void {
+ this.stopped = true;
+ this.timerGroup.stopCurrentAndFutureTimers();
+ this.ws.cryptoApi.stop();
+ }
+
+ async getSenderWireInfos(): Promise<SenderWireInfos> {
+ const m: { [url: string]: Set<string> } = {};
+
+ await this.db.iter(Stores.exchanges).forEach((x) => {
+ const wi = x.wireInfo;
+ if (!wi) {
+ return;
+ }
+ const s = (m[x.baseUrl] = m[x.baseUrl] || new Set());
+ Object.keys(wi.feesForType).map((k) => s.add(k));
+ });
+
+ const exchangeWireTypes: { [url: string]: string[] } = {};
+ Object.keys(m).map((e) => {
+ exchangeWireTypes[e] = Array.from(m[e]);
+ });
+
+ const senderWiresSet: Set<string> = new Set();
+ await this.db.iter(Stores.senderWires).forEach((x) => {
+ senderWiresSet.add(x.paytoUri);
+ });
+
+ const senderWires: string[] = Array.from(senderWiresSet);
+
+ return {
+ exchangeWireTypes,
+ senderWires,
+ };
+ }
+
+ /**
+ * Trigger paying coins back into the user's account.
+ */
+ async returnCoins(req: ReturnCoinsRequest): Promise<void> {
+ throw Error("not implemented");
+ }
+
+ /**
+ * Accept a refund, return the contract hash for the contract
+ * that was involved in the refund.
+ */
+ async applyRefund(
+ talerRefundUri: string,
+ ): Promise<{ contractTermsHash: string; proposalId: string }> {
+ return applyRefund(this.ws, talerRefundUri);
+ }
+
+ async getPurchase(
+ contractTermsHash: string,
+ ): Promise<PurchaseRecord | undefined> {
+ return this.db.get(Stores.purchases, contractTermsHash);
+ }
+
+ async acceptTip(talerTipUri: string): Promise<void> {
+ try {
+ return acceptTip(this.ws, talerTipUri);
+ } catch (e) {
+ this.latch.trigger();
+ }
+ }
+
+ async getTipStatus(talerTipUri: string): Promise<TipStatus> {
+ return getTipStatus(this.ws, talerTipUri);
+ }
+
+ async abortFailedPayment(contractTermsHash: string): Promise<void> {
+ throw Error("not implemented");
+ }
+
+ /**
+ * Inform the wallet that the status of a reserve has changed (e.g. due to a
+ * confirmation from the bank.).
+ */
+ public async handleNotifyReserve(): Promise<void> {
+ const reserves = await this.db.iter(Stores.reserves).toArray();
+ for (const r of reserves) {
+ if (r.reserveStatus === ReserveRecordStatus.WAIT_CONFIRM_BANK) {
+ try {
+ this.processReserve(r.reservePub);
+ } catch (e) {
+ console.error(e);
+ }
+ }
+ }
+ }
+
+ /**
+ * Remove unreferenced / expired data from the wallet's database
+ * based on the current system time.
+ */
+ async collectGarbage(): Promise<void> {
+ // FIXME(#5845)
+ // We currently do not garbage-collect the wallet database. This might change
+ // after the feature has been properly re-designed, and we have come up with a
+ // strategy to test it.
+ }
+
+ async acceptWithdrawal(
+ talerWithdrawUri: string,
+ selectedExchange: string,
+ ): Promise<AcceptWithdrawalResponse> {
+ try {
+ return createTalerWithdrawReserve(
+ this.ws,
+ talerWithdrawUri,
+ selectedExchange,
+ );
+ } finally {
+ this.latch.trigger();
+ }
+ }
+
+ async updateReserve(reservePub: string): Promise<ReserveRecord | undefined> {
+ await forceQueryReserve(this.ws, reservePub);
+ return await this.ws.db.get(Stores.reserves, reservePub);
+ }
+
+ async getReserve(reservePub: string): Promise<ReserveRecord | undefined> {
+ return await this.ws.db.get(Stores.reserves, reservePub);
+ }
+
+ async refuseProposal(proposalId: string): Promise<void> {
+ return refuseProposal(this.ws, proposalId);
+ }
+
+ async getPurchaseDetails(proposalId: string): Promise<PurchaseDetails> {
+ const purchase = await this.db.get(Stores.purchases, proposalId);
+ if (!purchase) {
+ throw Error("unknown purchase");
+ }
+ const refundsDoneAmounts = Object.values(purchase.refunds)
+ .filter((x) => x.type === RefundState.Applied)
+ .map((x) => x.refundAmount);
+
+ const refundsPendingAmounts = Object.values(purchase.refunds)
+ .filter((x) => x.type === RefundState.Pending)
+ .map((x) => x.refundAmount);
+ const totalRefundAmount = Amounts.sum([
+ ...refundsDoneAmounts,
+ ...refundsPendingAmounts,
+ ]).amount;
+ const refundsDoneFees = Object.values(purchase.refunds)
+ .filter((x) => x.type === RefundState.Applied)
+ .map((x) => x.refundFee);
+ const refundsPendingFees = Object.values(purchase.refunds)
+ .filter((x) => x.type === RefundState.Pending)
+ .map((x) => x.refundFee);
+ const totalRefundFees = Amounts.sum([
+ ...refundsDoneFees,
+ ...refundsPendingFees,
+ ]).amount;
+ const totalFees = totalRefundFees;
+ return {
+ contractTerms: JSON.parse(purchase.contractTermsRaw),
+ hasRefund: purchase.timestampLastRefundStatus !== undefined,
+ totalRefundAmount: totalRefundAmount,
+ totalRefundAndRefreshFees: totalFees,
+ };
+ }
+
+ benchmarkCrypto(repetitions: number): Promise<BenchmarkResult> {
+ return this.ws.cryptoApi.benchmark(repetitions);
+ }
+
+ async setCoinSuspended(coinPub: string, suspended: boolean): Promise<void> {
+ await this.db.runWithWriteTransaction([Stores.coins], async (tx) => {
+ const c = await tx.get(Stores.coins, coinPub);
+ if (!c) {
+ logger.warn(`coin ${coinPub} not found, won't suspend`);
+ return;
+ }
+ c.suspended = suspended;
+ await tx.put(Stores.coins, c);
+ });
+ }
+
+ /**
+ * Dump the public information of coins we have in an easy-to-process format.
+ */
+ async dumpCoins(): Promise<CoinDumpJson> {
+ const coins = await this.db.iter(Stores.coins).toArray();
+ const coinsJson: CoinDumpJson = { coins: [] };
+ for (const c of coins) {
+ const denom = await this.db.get(Stores.denominations, [
+ c.exchangeBaseUrl,
+ c.denomPub,
+ ]);
+ if (!denom) {
+ console.error("no denom session found for coin");
+ continue;
+ }
+ const cs = c.coinSource;
+ let refreshParentCoinPub: string | undefined;
+ if (cs.type == CoinSourceType.Refresh) {
+ refreshParentCoinPub = cs.oldCoinPub;
+ }
+ let withdrawalReservePub: string | undefined;
+ if (cs.type == CoinSourceType.Withdraw) {
+ const ws = await this.db.get(
+ Stores.withdrawalGroups,
+ cs.withdrawalGroupId,
+ );
+ if (!ws) {
+ console.error("no withdrawal session found for coin");
+ continue;
+ }
+ if (ws.source.type == "reserve") {
+ withdrawalReservePub = ws.source.reservePub;
+ }
+ }
+ coinsJson.coins.push({
+ coin_pub: c.coinPub,
+ denom_pub: c.denomPub,
+ denom_pub_hash: c.denomPubHash,
+ denom_value: Amounts.stringify(denom.value),
+ exchange_base_url: c.exchangeBaseUrl,
+ refresh_parent_coin_pub: refreshParentCoinPub,
+ remaining_value: Amounts.stringify(c.currentAmount),
+ withdrawal_reserve_pub: withdrawalReservePub,
+ coin_suspended: c.suspended,
+ });
+ }
+ return coinsJson;
+ }
+
+ async getTransactions(
+ request: TransactionsRequest,
+ ): Promise<TransactionsResponse> {
+ return getTransactions(this.ws, request);
+ }
+
+ async withdrawTestBalance(
+ amount = "TESTKUDOS:10",
+ bankBaseUrl = "https://bank.test.taler.net/",
+ exchangeBaseUrl = "https://exchange.test.taler.net/",
+ ): Promise<void> {
+ await withdrawTestBalance(this.ws, amount, bankBaseUrl, exchangeBaseUrl);
+ }
+}
diff --git a/packages/taler-wallet-core/src/walletCoreApiHandler.d.ts.map b/packages/taler-wallet-core/src/walletCoreApiHandler.d.ts.map
new file mode 100644
index 000000000..e7ab1011a
--- /dev/null
+++ b/packages/taler-wallet-core/src/walletCoreApiHandler.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"walletCoreApiHandler.d.ts","sourceRoot":"","sources":["walletCoreApiHandler.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAelC,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AA+N5D,oBAAY,eAAe,GACxB,sBAAsB,GACtB,oBAAoB,CAAC;AAExB,oBAAY,eAAe,GACxB,eAAe,GACf,mBAAmB,CAAC;AAEvB,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,cAAc,CAAC;IACrB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,sBAAsB;IAErC,IAAI,EAAE,UAAU,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,oBAAoB;IAEnC,IAAI,EAAE,OAAO,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,qBAAqB,CAAC;CAC9B;AAED;;GAEG;AACH,wBAAsB,oBAAoB,CACxC,CAAC,EAAE,MAAM,EACT,SAAS,EAAE,MAAM,EACjB,EAAE,EAAE,MAAM,EACV,OAAO,EAAE,OAAO,GACf,OAAO,CAAC,eAAe,CAAC,CAiC1B"} \ No newline at end of file
diff --git a/packages/taler-wallet-core/src/walletCoreApiHandler.ts b/packages/taler-wallet-core/src/walletCoreApiHandler.ts
new file mode 100644
index 000000000..a0b205485
--- /dev/null
+++ b/packages/taler-wallet-core/src/walletCoreApiHandler.ts
@@ -0,0 +1,318 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { Wallet } from "./wallet";
+import {
+ OperationFailedError,
+ OperationFailedAndReportedError,
+ makeErrorDetails,
+} from "./operations/errors";
+import { TalerErrorCode } from "./TalerErrorCode";
+import { codecForTransactionsRequest } from "./types/transactions";
+import {
+ makeCodecForObject,
+ codecForString,
+ Codec,
+ makeCodecOptional,
+} from "./util/codec";
+import { Amounts } from "./util/amounts";
+import { OperationErrorDetails } from "./types/walletTypes";
+
+interface AddExchangeRequest {
+ exchangeBaseUrl: string;
+}
+
+const codecForAddExchangeRequest = (): Codec<AddExchangeRequest> =>
+ makeCodecForObject<AddExchangeRequest>()
+ .property("exchangeBaseUrl", codecForString)
+ .build("AddExchangeRequest");
+
+interface GetExchangeTosRequest {
+ exchangeBaseUrl: string;
+}
+
+const codecForGetExchangeTosRequest = (): Codec<GetExchangeTosRequest> =>
+ makeCodecForObject<GetExchangeTosRequest>()
+ .property("exchangeBaseUrl", codecForString)
+ .build("GetExchangeTosRequest");
+
+interface AcceptManualWithdrawalRequest {
+ exchangeBaseUrl: string;
+ amount: string;
+}
+
+const codecForAcceptManualWithdrawalRequet = (): Codec<
+ AcceptManualWithdrawalRequest
+> =>
+ makeCodecForObject<AcceptManualWithdrawalRequest>()
+ .property("exchangeBaseUrl", codecForString)
+ .property("amount", codecForString)
+ .build("AcceptManualWithdrawalRequest");
+
+interface GetWithdrawalDetailsForAmountRequest {
+ exchangeBaseUrl: string;
+ amount: string;
+}
+
+interface AcceptBankIntegratedWithdrawalRequest {
+ talerWithdrawUri: string;
+ exchangeBaseUrl: string;
+}
+
+const codecForAcceptBankIntegratedWithdrawalRequest = (): Codec<
+ AcceptBankIntegratedWithdrawalRequest
+> =>
+ makeCodecForObject<AcceptBankIntegratedWithdrawalRequest>()
+ .property("exchangeBaseUrl", codecForString)
+ .property("talerWithdrawUri", codecForString)
+ .build("AcceptBankIntegratedWithdrawalRequest");
+
+const codecForGetWithdrawalDetailsForAmountRequest = (): Codec<
+ GetWithdrawalDetailsForAmountRequest
+> =>
+ makeCodecForObject<GetWithdrawalDetailsForAmountRequest>()
+ .property("exchangeBaseUrl", codecForString)
+ .property("amount", codecForString)
+ .build("GetWithdrawalDetailsForAmountRequest");
+
+interface AcceptExchangeTosRequest {
+ exchangeBaseUrl: string;
+ etag: string;
+}
+
+const codecForAcceptExchangeTosRequest = (): Codec<AcceptExchangeTosRequest> =>
+ makeCodecForObject<AcceptExchangeTosRequest>()
+ .property("exchangeBaseUrl", codecForString)
+ .property("etag", codecForString)
+ .build("AcceptExchangeTosRequest");
+
+interface ApplyRefundRequest {
+ talerRefundUri: string;
+}
+
+const codecForApplyRefundRequest = (): Codec<ApplyRefundRequest> =>
+ makeCodecForObject<ApplyRefundRequest>()
+ .property("talerRefundUri", codecForString)
+ .build("ApplyRefundRequest");
+
+interface GetWithdrawalDetailsForUriRequest {
+ talerWithdrawUri: string;
+}
+
+const codecForGetWithdrawalDetailsForUri = (): Codec<
+ GetWithdrawalDetailsForUriRequest
+> =>
+ makeCodecForObject<GetWithdrawalDetailsForUriRequest>()
+ .property("talerWithdrawUri", codecForString)
+ .build("GetWithdrawalDetailsForUriRequest");
+
+interface AbortProposalRequest {
+ proposalId: string;
+}
+
+const codecForAbortProposalRequest = (): Codec<AbortProposalRequest> =>
+ makeCodecForObject<AbortProposalRequest>()
+ .property("proposalId", codecForString)
+ .build("AbortProposalRequest");
+
+interface PreparePayRequest {
+ talerPayUri: string;
+}
+
+const codecForPreparePayRequest = (): Codec<PreparePayRequest> =>
+ makeCodecForObject<PreparePayRequest>()
+ .property("talerPayUri", codecForString)
+ .build("PreparePay");
+
+interface ConfirmPayRequest {
+ proposalId: string;
+ sessionId?: string;
+}
+
+const codecForConfirmPayRequest = (): Codec<ConfirmPayRequest> =>
+ makeCodecForObject<ConfirmPayRequest>()
+ .property("proposalId", codecForString)
+ .property("sessionId", makeCodecOptional(codecForString))
+ .build("ConfirmPay");
+
+/**
+ * Implementation of the "wallet-core" API.
+ */
+
+async function dispatchRequestInternal(
+ wallet: Wallet,
+ operation: string,
+ payload: unknown,
+): Promise<Record<string, any>> {
+ switch (operation) {
+ case "withdrawTestkudos":
+ await wallet.withdrawTestBalance();
+ return {};
+ case "getTransactions": {
+ const req = codecForTransactionsRequest().decode(payload);
+ return await wallet.getTransactions(req);
+ }
+ case "addExchange": {
+ const req = codecForAddExchangeRequest().decode(payload);
+ await wallet.updateExchangeFromUrl(req.exchangeBaseUrl);
+ return {};
+ }
+ case "listExchanges": {
+ return await wallet.getExchanges();
+ }
+ case "getWithdrawalDetailsForUri": {
+ const req = codecForGetWithdrawalDetailsForUri().decode(payload);
+ return await wallet.getWithdrawalDetailsForUri(req.talerWithdrawUri);
+ }
+ case "acceptManualWithdrawal": {
+ const req = codecForAcceptManualWithdrawalRequet().decode(payload);
+ const res = await wallet.acceptManualWithdrawal(
+ req.exchangeBaseUrl,
+ Amounts.parseOrThrow(req.amount),
+ );
+ return res;
+ }
+ case "getWithdrawalDetailsForAmount": {
+ const req = codecForGetWithdrawalDetailsForAmountRequest().decode(
+ payload,
+ );
+ return await wallet.getWithdrawalDetailsForAmount(
+ req.exchangeBaseUrl,
+ Amounts.parseOrThrow(req.amount),
+ );
+ }
+ case "getBalances": {
+ return await wallet.getBalances();
+ }
+ case "getPendingOperations": {
+ return await wallet.getPendingOperations();
+ }
+ case "setExchangeTosAccepted": {
+ const req = codecForAcceptExchangeTosRequest().decode(payload);
+ await wallet.acceptExchangeTermsOfService(req.exchangeBaseUrl, req.etag);
+ return {};
+ }
+ case "applyRefund": {
+ const req = codecForApplyRefundRequest().decode(payload);
+ return await wallet.applyRefund(req.talerRefundUri);
+ }
+ case "acceptBankIntegratedWithdrawal": {
+ const req = codecForAcceptBankIntegratedWithdrawalRequest().decode(
+ payload,
+ );
+ return await wallet.acceptWithdrawal(
+ req.talerWithdrawUri,
+ req.exchangeBaseUrl,
+ );
+ }
+ case "getExchangeTos": {
+ const req = codecForGetExchangeTosRequest().decode(payload);
+ return wallet.getExchangeTos(req.exchangeBaseUrl);
+ }
+ case "abortProposal": {
+ const req = codecForAbortProposalRequest().decode(payload);
+ await wallet.refuseProposal(req.proposalId);
+ return {};
+ }
+ case "retryPendingNow": {
+ await wallet.runPending(true);
+ return {};
+ }
+ case "preparePay": {
+ const req = codecForPreparePayRequest().decode(payload);
+ return await wallet.preparePayForUri(req.talerPayUri);
+ }
+ case "confirmPay": {
+ const req = codecForConfirmPayRequest().decode(payload);
+ return await wallet.confirmPay(req.proposalId, req.sessionId);
+ }
+ }
+ throw OperationFailedError.fromCode(
+ TalerErrorCode.WALLET_CORE_API_OPERATION_UNKNOWN,
+ "unknown operation",
+ {
+ operation,
+ },
+ );
+}
+
+export type CoreApiResponse = CoreApiResponseSuccess | CoreApiResponseError;
+
+export type CoreApiEnvelope = CoreApiResponse | CoreApiNotification;
+
+export interface CoreApiNotification {
+ type: "notification";
+ payload: unknown;
+}
+
+export interface CoreApiResponseSuccess {
+ // To distinguish the message from notifications
+ type: "response";
+ operation: string;
+ id: string;
+ result: unknown;
+}
+
+export interface CoreApiResponseError {
+ // To distinguish the message from notifications
+ type: "error";
+ operation: string;
+ id: string;
+ error: OperationErrorDetails;
+}
+
+/**
+ * Handle a request to the wallet-core API.
+ */
+export async function handleCoreApiRequest(
+ w: Wallet,
+ operation: string,
+ id: string,
+ payload: unknown,
+): Promise<CoreApiResponse> {
+ try {
+ const result = await dispatchRequestInternal(w, operation, payload);
+ return {
+ type: "response",
+ operation,
+ id,
+ result,
+ };
+ } catch (e) {
+ if (
+ e instanceof OperationFailedError ||
+ e instanceof OperationFailedAndReportedError
+ ) {
+ return {
+ type: "error",
+ operation,
+ id,
+ error: e.operationError,
+ };
+ } else {
+ return {
+ type: "error",
+ operation,
+ id,
+ error: makeErrorDetails(
+ TalerErrorCode.WALLET_UNEXPECTED_EXCEPTION,
+ `unexpected exception: ${e}`,
+ {},
+ ),
+ };
+ }
+ }
+}
diff --git a/packages/taler-wallet-core/tsconfig.json b/packages/taler-wallet-core/tsconfig.json
new file mode 100644
index 000000000..4e8b71d50
--- /dev/null
+++ b/packages/taler-wallet-core/tsconfig.json
@@ -0,0 +1,31 @@
+{
+ "compileOnSave": true,
+ "compilerOptions": {
+ "composite": true,
+ "declaration": true,
+ "declarationMap": false,
+ "target": "ES6",
+ "module": "ESNext",
+ "moduleResolution": "node",
+ "sourceMap": true,
+ "lib": ["es6"],
+ "types": ["node"],
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "strict": true,
+ "strictPropertyInitialization": false,
+ "outDir": "lib",
+ "noImplicitAny": true,
+ "noImplicitThis": true,
+ "incremental": true,
+ "esModuleInterop": true,
+ "importHelpers": true,
+ "rootDir": "./src"
+ },
+ "references": [
+ {
+ "path": "../idb-bridge/"
+ }
+ ],
+ "include": ["src/**/*"]
+}
diff --git a/packages/taler-wallet-webextension/package.json b/packages/taler-wallet-webextension/package.json
new file mode 100644
index 000000000..b60d4ea98
--- /dev/null
+++ b/packages/taler-wallet-webextension/package.json
@@ -0,0 +1,41 @@
+{
+ "name": "taler-wallet-webextension",
+ "version": "0.0.15",
+ "description": "GNU Taler Wallet browser extension",
+ "main": "./build/index.js",
+ "types": "./build/index.d.ts",
+ "author": "Florian Dold",
+ "license": "AGPL-3.0-or-later",
+ "private": false,
+ "scripts": {
+ "test": "tsc && ava",
+ "compile": "tsc"
+ },
+ "dependencies": {
+ "moment": "^2.27.0",
+ "taler-wallet-core": "workspace:*",
+ "tslib": "^2.0.0"
+ },
+ "devDependencies": {
+ "@rollup/plugin-commonjs": "^14.0.0",
+ "@rollup/plugin-json": "^4.1.0",
+ "@rollup/plugin-node-resolve": "^8.4.0",
+ "@rollup/plugin-replace": "^2.3.3",
+ "@rollup/plugin-typescript": "^5.0.2",
+ "@types/chrome": "^0.0.103",
+ "@types/enzyme": "^3.10.5",
+ "@types/enzyme-adapter-react-16": "^1.0.6",
+ "@types/node": "^14.0.27",
+ "@types/react": "^16.9.44",
+ "@types/react-dom": "^16.9.8",
+ "ava": "3.11.0",
+ "enzyme": "^3.11.0",
+ "enzyme-adapter-react-16": "^1.15.2",
+ "react": "^16.13.1",
+ "react-dom": "^16.13.1",
+ "rollup": "^2.23.0",
+ "rollup-plugin-sourcemaps": "^0.6.2",
+ "rollup-plugin-terser": "^6.1.0",
+ "typescript": "^3.9.7"
+ }
+}
diff --git a/packages/taler-wallet-webextension/rollup.config.js b/packages/taler-wallet-webextension/rollup.config.js
new file mode 100644
index 000000000..25ce768b4
--- /dev/null
+++ b/packages/taler-wallet-webextension/rollup.config.js
@@ -0,0 +1,212 @@
+// rollup.config.js
+import commonjs from "@rollup/plugin-commonjs";
+import nodeResolve from "@rollup/plugin-node-resolve";
+import json from "@rollup/plugin-json";
+import replace from "@rollup/plugin-replace";
+import builtins from "builtin-modules";
+import { terser } from "rollup-plugin-terser";
+import typescript from "@rollup/plugin-typescript";
+
+// Base settings to use
+const baseTypescriptCompilerSettings = {
+ target: "ES6",
+ jsx: "react",
+ reactNamespace: "React",
+ moduleResolution: "node",
+ sourceMap: true,
+ lib: ["es6", "dom"],
+ noImplicitReturns: true,
+ noFallthroughCasesInSwitch: true,
+ strict: true,
+ strictPropertyInitialization: false,
+ noImplicitAny: true,
+ noImplicitThis: true,
+ allowJs: true,
+ checkJs: true,
+ incremental: false,
+ esModuleInterop: true,
+ importHelpers: true,
+ module: "ESNext",
+ include: ["src/**/*.+(ts|tsx)"],
+ rootDir: "./src",
+};
+
+const walletCli = {
+ input: "src/headless/taler-wallet-cli.ts",
+ output: {
+ file: "dist/standalone/taler-wallet-cli.js",
+ format: "cjs",
+ },
+ external: builtins,
+ plugins: [
+ nodeResolve({
+ preferBuiltins: true,
+ }),
+
+ commonjs({
+ include: ["node_modules/**", "dist/node/**"],
+ extensions: [".js", ".ts"],
+ ignoreGlobal: false, // Default: false
+ sourceMap: false,
+ ignore: ["taler-wallet"],
+ }),
+
+ json(),
+
+ typescript({
+ tsconfig: false,
+ ...baseTypescriptCompilerSettings,
+ sourceMap: false,
+ }),
+ ],
+};
+
+const walletAndroid = {
+ input: "src/android/index.ts",
+ output: {
+ //dir: "dist/standalone",
+ file: "dist/standalone/taler-wallet-android.js",
+ format: "cjs",
+ exports: "named",
+ },
+ external: builtins,
+ plugins: [
+ json(),
+
+ nodeResolve({
+ preferBuiltins: true,
+ }),
+
+ commonjs({
+ include: ["node_modules/**"],
+ extensions: [".js"],
+ sourceMap: false,
+ ignore: ["taler-wallet"],
+ }),
+
+ typescript({
+ tsconfig: false,
+ ...baseTypescriptCompilerSettings,
+ sourceMap: false,
+ }),
+ ],
+};
+
+const webExtensionPageEntryPoint = {
+ input: "src/webex/pageEntryPoint.ts",
+ output: {
+ file: "dist/webextension/pageEntryPoint.js",
+ format: "iife",
+ exports: "none",
+ name: "webExtensionPageEntry",
+ },
+ external: builtins,
+ plugins: [
+ json(),
+
+ nodeResolve({
+ preferBuiltins: true,
+ }),
+
+ terser(),
+
+ replace({
+ "process.env.NODE_ENV": JSON.stringify("production"),
+ }),
+
+ commonjs({
+ include: ["node_modules/**", "dist/node/**"],
+ extensions: [".js"],
+ sourceMap: false,
+ ignore: ["taler-wallet"],
+ }),
+
+ typescript({
+ tsconfig: false,
+ ...baseTypescriptCompilerSettings,
+ sourceMap: false,
+ }),
+ ],
+};
+
+const webExtensionBackgroundPageScript = {
+ input: "src/webex/background.ts",
+ output: {
+ file: "dist/webextension/background.js",
+ format: "iife",
+ exports: "none",
+ name: "webExtensionBackgroundScript",
+ },
+ external: builtins,
+ plugins: [
+ json(),
+
+ nodeResolve({
+ preferBuiltins: true,
+ }),
+
+ terser(),
+
+ replace({
+ "process.env.NODE_ENV": JSON.stringify("production"),
+ }),
+
+ commonjs({
+ include: ["node_modules/**", "dist/node/**"],
+ extensions: [".js"],
+ sourceMap: false,
+ ignore: ["taler-wallet", "crypto"],
+ }),
+
+ typescript({
+ tsconfig: false,
+ ...baseTypescriptCompilerSettings,
+ sourceMap: false,
+ }),
+ ],
+};
+
+const webExtensionCryptoWorker = {
+ input: "src/crypto/workers/browserWorkerEntry.ts",
+ output: {
+ file: "dist/webextension/browserWorkerEntry.js",
+ format: "iife",
+ exports: "none",
+ name: "webExtensionCryptoWorker",
+ },
+ external: builtins,
+ plugins: [
+ json(),
+
+ nodeResolve({
+ preferBuiltins: true,
+ }),
+
+ terser(),
+
+ replace({
+ "process.env.NODE_ENV": JSON.stringify("production"),
+ }),
+
+ commonjs({
+ include: ["node_modules/**", "dist/node/**"],
+ extensions: [".js"],
+ sourceMap: false,
+ ignore: ["taler-wallet", "crypto"],
+ }),
+
+ typescript({
+ tsconfig: false,
+ ...baseTypescriptCompilerSettings,
+ sourceMap: false,
+ }),
+ ],
+};
+
+export default [
+ walletCli,
+ walletAndroid,
+ webExtensionPageEntryPoint,
+ webExtensionBackgroundPageScript,
+ webExtensionCryptoWorker,
+];
diff --git a/packages/taler-wallet-webextension/src/background.ts b/packages/taler-wallet-webextension/src/background.ts
new file mode 100644
index 000000000..dbc540df4
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/background.ts
@@ -0,0 +1,30 @@
+/*
+ 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/>
+ */
+
+/**
+ * Entry point for the background page.
+ *
+ * @author Florian Dold
+ */
+
+/**
+ * Imports.
+ */
+import { wxMain } from "./wxBackend";
+
+window.addEventListener("load", () => {
+ wxMain();
+});
diff --git a/packages/taler-wallet-webextension/src/browserCryptoWorkerFactory.js b/packages/taler-wallet-webextension/src/browserCryptoWorkerFactory.js
new file mode 100644
index 000000000..e9492a2fb
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/browserCryptoWorkerFactory.js
@@ -0,0 +1,44 @@
+"use strict";
+/*
+ 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/>
+ */
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.BrowserCryptoWorkerFactory = void 0;
+/**
+ * API to access the Taler crypto worker thread.
+ * @author Florian Dold
+ */
+class BrowserCryptoWorkerFactory {
+ startWorker() {
+ const workerCtor = Worker;
+ const workerPath = "/browserWorkerEntry.js";
+ return new workerCtor(workerPath);
+ }
+ getConcurrency() {
+ let concurrency = 2;
+ try {
+ // only works in the browser
+ // tslint:disable-next-line:no-string-literal
+ concurrency = navigator["hardwareConcurrency"];
+ concurrency = Math.max(1, Math.ceil(concurrency / 2));
+ }
+ catch (e) {
+ concurrency = 2;
+ }
+ return concurrency;
+ }
+}
+exports.BrowserCryptoWorkerFactory = BrowserCryptoWorkerFactory;
+//# sourceMappingURL=browserCryptoWorkerFactory.js.map \ No newline at end of file
diff --git a/packages/taler-wallet-webextension/src/browserCryptoWorkerFactory.js.map b/packages/taler-wallet-webextension/src/browserCryptoWorkerFactory.js.map
new file mode 100644
index 000000000..db56d4451
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/browserCryptoWorkerFactory.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"browserCryptoWorkerFactory.js","sourceRoot":"","sources":["browserCryptoWorkerFactory.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;GAcG;;;AAEH;;;GAGG;AAEH,MAAa,0BAA0B;IACrC,WAAW;QACT,MAAM,UAAU,GAAG,MAAM,CAAC;QAC1B,MAAM,UAAU,GAAG,wBAAwB,CAAC;QAC5C,OAAO,IAAI,UAAU,CAAC,UAAU,CAAiB,CAAC;IACpD,CAAC;IAED,cAAc;QACZ,IAAI,WAAW,GAAG,CAAC,CAAC;QACpB,IAAI;YACF,4BAA4B;YAC5B,6CAA6C;YAC7C,WAAW,GAAI,SAAiB,CAAC,qBAAqB,CAAC,CAAC;YACxD,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,WAAW,GAAG,CAAC,CAAC,CAAC,CAAC;SACvD;QAAC,OAAO,CAAC,EAAE;YACV,WAAW,GAAG,CAAC,CAAC;SACjB;QACD,OAAO,WAAW,CAAC;IACrB,CAAC;CACF;AAnBD,gEAmBC"} \ No newline at end of file
diff --git a/packages/taler-wallet-webextension/src/browserCryptoWorkerFactory.ts b/packages/taler-wallet-webextension/src/browserCryptoWorkerFactory.ts
new file mode 100644
index 000000000..b91f49f17
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/browserCryptoWorkerFactory.ts
@@ -0,0 +1,43 @@
+/*
+ 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/>
+ */
+
+/**
+ * API to access the Taler crypto worker thread.
+ * @author Florian Dold
+ */
+
+import type { CryptoWorker, CryptoWorkerFactory } from "taler-wallet-core";
+
+export class BrowserCryptoWorkerFactory implements CryptoWorkerFactory {
+ startWorker(): CryptoWorker {
+ const workerCtor = Worker;
+ const workerPath = "/browserWorkerEntry.js";
+ return new workerCtor(workerPath) as CryptoWorker;
+ }
+
+ getConcurrency(): number {
+ let concurrency = 2;
+ try {
+ // only works in the browser
+ // tslint:disable-next-line:no-string-literal
+ concurrency = (navigator as any)["hardwareConcurrency"];
+ concurrency = Math.max(1, Math.ceil(concurrency / 2));
+ } catch (e) {
+ concurrency = 2;
+ }
+ return concurrency;
+ }
+}
diff --git a/packages/taler-wallet-webextension/src/browserHttpLib.ts b/packages/taler-wallet-webextension/src/browserHttpLib.ts
new file mode 100644
index 000000000..2782e4a14
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/browserHttpLib.ts
@@ -0,0 +1,129 @@
+
+import { httpLib, OperationFailedError, Logger } from "taler-wallet-core";
+import { TalerErrorCode } from "taler-wallet-core/lib/TalerErrorCode";
+
+const logger = new Logger("browserHttpLib");
+
+/**
+ * An implementation of the [[HttpRequestLibrary]] using the
+ * browser's XMLHttpRequest.
+ */
+export class BrowserHttpLib implements httpLib.HttpRequestLibrary {
+ private req(
+ method: string,
+ url: string,
+ requestBody?: any,
+ options?: httpLib.HttpRequestOptions,
+ ): Promise<httpLib.HttpResponse> {
+ return new Promise<httpLib.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: httpLib.Headers = new httpLib.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: httpLib.HttpResponse = {
+ requestUrl: url,
+ status: myRequest.status,
+ headers: headerMap,
+ json: makeJson,
+ text: async () => myRequest.responseText,
+ };
+ resolve(resp);
+ }
+ });
+ });
+ }
+
+ get(url: string, opt?: httpLib.HttpRequestOptions): Promise<httpLib.HttpResponse> {
+ return this.req("get", url, undefined, opt);
+ }
+
+ postJson(
+ url: string,
+ body: unknown,
+ opt?: httpLib.HttpRequestOptions,
+ ): Promise<httpLib.HttpResponse> {
+ return this.req("post", url, JSON.stringify(body), opt);
+ }
+
+ stop(): void {
+ // Nothing to do
+ }
+} \ No newline at end of file
diff --git a/packages/taler-wallet-webextension/src/browserWorkerEntry.ts b/packages/taler-wallet-webextension/src/browserWorkerEntry.ts
new file mode 100644
index 000000000..77c38fda9
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/browserWorkerEntry.ts
@@ -0,0 +1,74 @@
+/*
+ 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/>
+*/
+
+/**
+ * Web worker for crypto operations.
+ */
+
+/**
+ * Imports.
+ */
+
+import { CryptoImplementation } from "taler-wallet-core";
+
+const worker: Worker = (self as any) as Worker;
+
+async function handleRequest(
+ operation: string,
+ id: number,
+ args: string[],
+): Promise<void> {
+ const impl = new CryptoImplementation();
+
+ if (!(operation in impl)) {
+ console.error(`crypto operation '${operation}' not found`);
+ return;
+ }
+
+ try {
+ const result = (impl as any)[operation](...args);
+ worker.postMessage({ result, id });
+ } catch (e) {
+ console.log("error during operation", e);
+ return;
+ }
+}
+
+worker.onmessage = (msg: MessageEvent) => {
+ const args = msg.data.args;
+ if (!Array.isArray(args)) {
+ console.error("args must be array");
+ return;
+ }
+ const id = msg.data.id;
+ if (typeof id !== "number") {
+ console.error("RPC id must be number");
+ return;
+ }
+ const operation = msg.data.operation;
+ if (typeof operation !== "string") {
+ console.error("RPC operation must be string");
+ return;
+ }
+
+ if (CryptoImplementation.enableTracing) {
+ console.log("onmessage with", operation);
+ }
+
+ handleRequest(operation, id, args).catch((e) => {
+ console.error("error in browsere worker", e);
+ });
+};
diff --git a/packages/taler-wallet-webextension/src/chromeBadge.ts b/packages/taler-wallet-webextension/src/chromeBadge.ts
new file mode 100644
index 000000000..7bc5d368d
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/chromeBadge.ts
@@ -0,0 +1,288 @@
+/*
+ This file is part of TALER
+ (C) 2016 INRIA
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { isFirefox } from "./compat";
+
+/**
+ * Polyfill for requestAnimationFrame, which
+ * doesn't work from a background page.
+ */
+function rAF(cb: (ts: number) => void): void {
+ window.setTimeout(() => {
+ cb(performance.now());
+ }, 100 /* 100 ms delay between frames */);
+}
+
+/**
+ * Badge for Chrome that renders a Taler logo with a rotating ring if some
+ * background activity is happening.
+ */
+export class ChromeBadge {
+ private canvas: HTMLCanvasElement;
+ private ctx: CanvasRenderingContext2D;
+ /**
+ * True if animation running. The animation
+ * might still be running even if we're not busy anymore,
+ * just to transition to the "normal" state in a animated way.
+ */
+ private animationRunning = false;
+
+ /**
+ * Is the wallet still busy? Note that we do not stop the
+ * animation immediately when the wallet goes idle, but
+ * instead slowly close the gap.
+ */
+ private isBusy = false;
+
+ /**
+ * Current rotation angle, ranges from 0 to rotationAngleMax.
+ */
+ private rotationAngle = 0;
+
+ /**
+ * While animating, how wide is the current gap in the circle?
+ * Ranges from 0 to openMax.
+ */
+ private gapWidth = 0;
+
+ /**
+ * Should we show the notification dot?
+ */
+ private hasNotification = false;
+
+ /**
+ * Maximum value for our rotationAngle, corresponds to 2 Pi.
+ */
+ static rotationAngleMax = 1000;
+
+ /**
+ * How fast do we rotate? Given in rotation angle (relative to rotationAngleMax) per millisecond.
+ */
+ static rotationSpeed = 0.5;
+
+ /**
+ * How fast to we open? Given in rotation angle (relative to rotationAngleMax) per millisecond.
+ */
+ static openSpeed = 0.15;
+
+ /**
+ * How fast to we close? Given as a multiplication factor per frame update.
+ */
+ static closeSpeed = 0.7;
+
+ /**
+ * How far do we open? Given relative to rotationAngleMax.
+ */
+ static openMax = 100;
+
+ constructor(window?: Window) {
+ // Allow injecting another window for testing
+ const bg = window || chrome.extension.getBackgroundPage();
+ if (!bg) {
+ throw Error("no window available");
+ }
+ this.canvas = bg.document.createElement("canvas");
+ // Note: changing the width here means changing the font
+ // size in draw() as well!
+ this.canvas.width = 32;
+ this.canvas.height = 32;
+ const ctx = this.canvas.getContext("2d");
+ if (!ctx) {
+ throw Error("unable to get canvas context");
+ }
+ this.ctx = ctx;
+ this.draw();
+ }
+
+ /**
+ * Draw the badge based on the current state.
+ */
+ private draw(): void {
+ this.ctx.setTransform(1, 0, 0, 1, 0, 0);
+ this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
+
+ this.ctx.translate(this.canvas.width / 2, this.canvas.height / 2);
+
+ this.ctx.beginPath();
+ this.ctx.arc(0, 0, this.canvas.width / 2 - 2, 0, 2 * Math.PI);
+ this.ctx.fillStyle = "white";
+ this.ctx.fill();
+
+ // move into the center, off by 2 for aligning the "T" with the bottom
+ // of the circle.
+ this.ctx.translate(0, 2);
+
+ // pick sans-serif font; note: 14px is based on the 32px width above!
+ this.ctx.font = "bold 24px sans-serif";
+ // draw the "T" perfectly centered (x and y) to the current position
+ this.ctx.textAlign = "center";
+ this.ctx.textBaseline = "middle";
+ this.ctx.fillStyle = "black";
+ this.ctx.fillText("T", 0, 0);
+ // now move really into the center
+ this.ctx.translate(0, -2);
+ // start drawing the (possibly open) circle
+ this.ctx.beginPath();
+ this.ctx.lineWidth = 2.5;
+ if (this.animationRunning) {
+ /* Draw circle around the "T" with an opening of this.gapWidth */
+ const aMax = ChromeBadge.rotationAngleMax;
+ const startAngle = (this.rotationAngle / aMax) * Math.PI * 2;
+ const stopAngle =
+ ((this.rotationAngle + aMax - this.gapWidth) / aMax) * Math.PI * 2;
+ this.ctx.arc(
+ 0,
+ 0,
+ this.canvas.width / 2 - 2,
+ /* radius */ startAngle,
+ stopAngle,
+ false,
+ );
+ } else {
+ /* Draw full circle */
+ this.ctx.arc(
+ 0,
+ 0,
+ this.canvas.width / 2 - 2 /* radius */,
+ 0,
+ Math.PI * 2,
+ false,
+ );
+ }
+ this.ctx.stroke();
+ // go back to the origin
+ this.ctx.translate(-this.canvas.width / 2, -this.canvas.height / 2);
+
+ if (this.hasNotification) {
+ // We draw a circle with a soft border in the
+ // lower right corner.
+ const r = 8;
+ const cw = this.canvas.width;
+ const ch = this.canvas.height;
+ this.ctx.beginPath();
+ this.ctx.arc(cw - r, ch - r, r, 0, 2 * Math.PI, false);
+ const gradient = this.ctx.createRadialGradient(
+ cw - r,
+ ch - r,
+ r,
+ cw - r,
+ ch - r,
+ 5,
+ );
+ gradient.addColorStop(0, "rgba(255, 255, 255, 1)");
+ gradient.addColorStop(1, "blue");
+ this.ctx.fillStyle = gradient;
+ this.ctx.fill();
+ }
+
+ // Allow running outside the extension for testing
+ // tslint:disable-next-line:no-string-literal
+ if (window["chrome"] && window.chrome["browserAction"]) {
+ try {
+ const imageData = this.ctx.getImageData(
+ 0,
+ 0,
+ this.canvas.width,
+ this.canvas.height,
+ );
+ chrome.browserAction.setIcon({ imageData });
+ } catch (e) {
+ // Might fail if browser has over-eager canvas fingerprinting countermeasures.
+ // There's nothing we can do then ...
+ }
+ }
+ }
+
+ private animate(): void {
+ if (this.animationRunning) {
+ return;
+ }
+ if (isFirefox()) {
+ // Firefox does not support badge animations properly
+ return;
+ }
+ this.animationRunning = true;
+ let start: number | undefined;
+ const step = (timestamp: number): void => {
+ if (!this.animationRunning) {
+ return;
+ }
+ if (!start) {
+ start = timestamp;
+ }
+ if (!this.isBusy && 0 === this.gapWidth) {
+ // stop if we're close enough to origin
+ this.rotationAngle = 0;
+ } else {
+ this.rotationAngle =
+ (this.rotationAngle +
+ (timestamp - start) * ChromeBadge.rotationSpeed) %
+ ChromeBadge.rotationAngleMax;
+ }
+ if (this.isBusy) {
+ if (this.gapWidth < ChromeBadge.openMax) {
+ this.gapWidth += ChromeBadge.openSpeed * (timestamp - start);
+ }
+ if (this.gapWidth > ChromeBadge.openMax) {
+ this.gapWidth = ChromeBadge.openMax;
+ }
+ } else {
+ if (this.gapWidth > 0) {
+ this.gapWidth--;
+ this.gapWidth *= ChromeBadge.closeSpeed;
+ }
+ }
+
+ if (this.isBusy || this.gapWidth > 0) {
+ start = timestamp;
+ rAF(step);
+ } else {
+ this.animationRunning = false;
+ }
+ this.draw();
+ };
+ rAF(step);
+ }
+
+ /**
+ * Draw the badge such that it shows the
+ * user that something happened (balance changed).
+ */
+ showNotification(): void {
+ this.hasNotification = true;
+ this.draw();
+ }
+
+ /**
+ * Draw the badge without the notification mark.
+ */
+ clearNotification(): void {
+ this.hasNotification = false;
+ this.draw();
+ }
+
+ startBusy(): void {
+ if (this.isBusy) {
+ return;
+ }
+ this.isBusy = true;
+ this.animate();
+ }
+
+ stopBusy(): void {
+ this.isBusy = false;
+ }
+}
diff --git a/packages/taler-wallet-webextension/src/compat.js b/packages/taler-wallet-webextension/src/compat.js
new file mode 100644
index 000000000..fdfcbd4b9
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/compat.js
@@ -0,0 +1,61 @@
+"use strict";
+/*
+ This file is part of TALER
+ (C) 2017 INRIA
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.getPermissionsApi = exports.isNode = exports.isFirefox = void 0;
+/**
+ * Compatibility helpers needed for browsers that don't implement
+ * WebExtension APIs consistently.
+ */
+function isFirefox() {
+ const rt = chrome.runtime;
+ if (typeof rt.getBrowserInfo === "function") {
+ return true;
+ }
+ return false;
+}
+exports.isFirefox = isFirefox;
+/**
+ * Check if we are running under nodejs.
+ */
+function isNode() {
+ return typeof process !== "undefined" && process.release.name === "node";
+}
+exports.isNode = isNode;
+function getPermissionsApi() {
+ const myBrowser = globalThis.browser;
+ if (typeof myBrowser === "object" &&
+ typeof myBrowser.permissions === "object") {
+ return {
+ addPermissionsListener: () => {
+ // Not supported yet.
+ },
+ contains: myBrowser.permissions.contains,
+ request: myBrowser.permissions.request,
+ remove: myBrowser.permissions.remove,
+ };
+ }
+ else {
+ return {
+ addPermissionsListener: chrome.permissions.onAdded.addListener.bind(chrome.permissions.onAdded),
+ contains: chrome.permissions.contains,
+ request: chrome.permissions.request,
+ remove: chrome.permissions.remove,
+ };
+ }
+}
+exports.getPermissionsApi = getPermissionsApi;
+//# sourceMappingURL=compat.js.map \ No newline at end of file
diff --git a/packages/taler-wallet-webextension/src/compat.ts b/packages/taler-wallet-webextension/src/compat.ts
new file mode 100644
index 000000000..4635abd80
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/compat.ts
@@ -0,0 +1,85 @@
+/*
+ This file is part of TALER
+ (C) 2017 INRIA
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Compatibility helpers needed for browsers that don't implement
+ * WebExtension APIs consistently.
+ */
+
+export function isFirefox(): boolean {
+ const rt = chrome.runtime as any;
+ if (typeof rt.getBrowserInfo === "function") {
+ return true;
+ }
+ return false;
+}
+
+/**
+ * Check if we are running under nodejs.
+ */
+export function isNode(): boolean {
+ return typeof process !== "undefined" && process.release.name === "node";
+}
+
+/**
+ * Compatibility API that works on multiple browsers.
+ */
+export interface CrossBrowserPermissionsApi {
+ contains(
+ permissions: chrome.permissions.Permissions,
+ callback: (result: boolean) => void,
+ ): void;
+
+ addPermissionsListener(
+ callback: (permissions: chrome.permissions.Permissions) => void,
+ ): void;
+
+ request(
+ permissions: chrome.permissions.Permissions,
+ callback?: (granted: boolean) => void,
+ ): void;
+
+ remove(
+ permissions: chrome.permissions.Permissions,
+ callback?: (removed: boolean) => void,
+ ): void;
+}
+
+export function getPermissionsApi(): CrossBrowserPermissionsApi {
+ const myBrowser = (globalThis as any).browser;
+ if (
+ typeof myBrowser === "object" &&
+ typeof myBrowser.permissions === "object"
+ ) {
+ return {
+ addPermissionsListener: () => {
+ // Not supported yet.
+ },
+ contains: myBrowser.permissions.contains,
+ request: myBrowser.permissions.request,
+ remove: myBrowser.permissions.remove,
+ };
+ } else {
+ return {
+ addPermissionsListener: chrome.permissions.onAdded.addListener.bind(
+ chrome.permissions.onAdded,
+ ),
+ contains: chrome.permissions.contains,
+ request: chrome.permissions.request,
+ remove: chrome.permissions.remove,
+ };
+ }
+}
diff --git a/packages/taler-wallet-webextension/src/i18n-test.tsx b/packages/taler-wallet-webextension/src/i18n-test.tsx
new file mode 100644
index 000000000..e17a455ce
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/i18n-test.tsx
@@ -0,0 +1,68 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 Taler Systems SA
+
+ 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 { internalSetStrings, str, Translate, strings } from "./i18n";
+import React from "react";
+import { render } from "enzyme";
+import { configure } from "enzyme";
+import Adapter from "enzyme-adapter-react-16";
+
+configure({ adapter: new Adapter() });
+
+const testStrings = {
+ domain: "messages",
+ locale_data: {
+ messages: {
+ str1: ["foo1"],
+ str2: [""],
+ "str3 %1$s / %2$s": ["foo3 %2$s ; %1$s"],
+ "": {
+ domain: "messages",
+ plural_forms: "nplurals=2; plural=(n != 1);",
+ lang: "",
+ },
+ },
+ },
+};
+
+test("str translation", (t) => {
+ // Alias, so we nly use the function for lookups, not for string extranction.
+ const strAlias = str;
+ const TranslateAlias = Translate;
+ internalSetStrings(testStrings);
+ t.is(strAlias`str1`, "foo1");
+ t.is(strAlias`str2`, "str2");
+ const a = "a";
+ const b = "b";
+ t.is(strAlias`str3 ${a} / ${b}`, "foo3 b ; a");
+ const r = render(<TranslateAlias>str1</TranslateAlias>);
+ t.is(r.text(), "foo1");
+
+ const r2 = render(
+ <TranslateAlias>
+ str3 <span>{a}</span> / <span>{b}</span>
+ </TranslateAlias>,
+ );
+ t.is(r2.text(), "foo3 b ; a");
+
+ t.pass();
+});
+
+test("existing str translation", (t) => {
+ internalSetStrings(strings);
+ t.pass();
+});
diff --git a/packages/taler-wallet-webextension/src/i18n.tsx b/packages/taler-wallet-webextension/src/i18n.tsx
new file mode 100644
index 000000000..afbb0e278
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/i18n.tsx
@@ -0,0 +1,206 @@
+/*
+ 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/>
+ */
+
+/**
+ * Translation helpers for React components and template literals.
+ */
+
+/**
+ * Imports
+ */
+import React from "react";
+
+import { i18n as i18nCore } from "taler-wallet-core";
+/**
+ * Convert template strings to a msgid
+ */
+function toI18nString(stringSeq: ReadonlyArray<string>): string {
+ let s = "";
+ for (let i = 0; i < stringSeq.length; i++) {
+ s += stringSeq[i];
+ if (i < stringSeq.length - 1) {
+ s += `%${i + 1}$s`;
+ }
+ }
+ return s;
+}
+
+
+export const str = i18nCore.str;
+export const internalSetStrings = i18nCore.internalSetStrings;
+export const strings = i18nCore.strings;
+
+
+interface TranslateSwitchProps {
+ target: number;
+}
+
+function stringifyChildren(children: any): string {
+ let n = 1;
+ const ss = React.Children.map(children, (c) => {
+ if (typeof c === "string") {
+ return c;
+ }
+ return `%${n++}$s`;
+ });
+ const s = ss.join("").replace(/ +/g, " ").trim();
+ console.log("translation lookup", JSON.stringify(s));
+ return s;
+}
+
+interface TranslateProps {
+ /**
+ * Component that the translated element should be wrapped in.
+ * Defaults to "div".
+ */
+ wrap?: any;
+
+ /**
+ * Props to give to the wrapped component.
+ */
+ wrapProps?: any;
+}
+
+function getTranslatedChildren(
+ translation: string,
+ children: React.ReactNode,
+): React.ReactNode[] {
+ const tr = translation.split(/%(\d+)\$s/);
+ const childArray = React.Children.toArray(children);
+ // Merge consecutive string children.
+ const placeholderChildren = [];
+ for (let i = 0; i < childArray.length; i++) {
+ const x = childArray[i];
+ if (x === undefined) {
+ continue;
+ } else if (typeof x === "string") {
+ continue;
+ } else {
+ placeholderChildren.push(x);
+ }
+ }
+ const result = [];
+ for (let i = 0; i < tr.length; i++) {
+ if (i % 2 == 0) {
+ // Text
+ result.push(tr[i]);
+ } else {
+ const childIdx = Number.parseInt(tr[i]) - 1;
+ result.push(placeholderChildren[childIdx]);
+ }
+ }
+ return result;
+}
+
+/**
+ * Translate text node children of this component.
+ * If a child component might produce a text node, it must be wrapped
+ * in a another non-text element.
+ *
+ * Example:
+ * ```
+ * <Translate>
+ * Hello. Your score is <span><PlayerScore player={player} /></span>
+ * </Translate>
+ * ```
+ */
+export class Translate extends React.Component<TranslateProps, {}> {
+ render(): JSX.Element {
+ const s = stringifyChildren(this.props.children);
+ const translation: string = i18nCore.jed.ngettext(s, s, 1);
+ const result = getTranslatedChildren(translation, this.props.children);
+ if (!this.props.wrap) {
+ return <div>{result}</div>;
+ }
+ return React.createElement(this.props.wrap, this.props.wrapProps, result);
+ }
+}
+
+/**
+ * Switch translation based on singular or plural based on the target prop.
+ * Should only contain TranslateSingular and TransplatePlural as children.
+ *
+ * Example:
+ * ```
+ * <TranslateSwitch target={n}>
+ * <TranslateSingular>I have {n} apple.</TranslateSingular>
+ * <TranslatePlural>I have {n} apples.</TranslatePlural>
+ * </TranslateSwitch>
+ * ```
+ */
+export class TranslateSwitch extends React.Component<
+ TranslateSwitchProps,
+ void
+> {
+ render(): JSX.Element {
+ let singular: React.ReactElement<TranslationPluralProps> | undefined;
+ let plural: React.ReactElement<TranslationPluralProps> | undefined;
+ const children = this.props.children;
+ if (children) {
+ React.Children.forEach(children, (child: any) => {
+ if (child.type === TranslatePlural) {
+ plural = child;
+ }
+ if (child.type === TranslateSingular) {
+ singular = child;
+ }
+ });
+ }
+ if (!singular || !plural) {
+ console.error("translation not found");
+ return React.createElement("span", {}, ["translation not found"]);
+ }
+ singular.props.target = this.props.target;
+ plural.props.target = this.props.target;
+ // We're looking up the translation based on the
+ // singular, even if we must use the plural form.
+ return singular;
+ }
+}
+
+interface TranslationPluralProps {
+ target: number;
+}
+
+/**
+ * See [[TranslateSwitch]].
+ */
+export class TranslatePlural extends React.Component<
+ TranslationPluralProps,
+ void
+> {
+ render(): JSX.Element {
+ const s = stringifyChildren(this.props.children);
+ const translation = i18nCore.jed.ngettext(s, s, 1);
+ const result = getTranslatedChildren(translation, this.props.children);
+ return <div>{result}</div>;
+ }
+}
+
+/**
+ * See [[TranslateSwitch]].
+ */
+export class TranslateSingular extends React.Component<
+ TranslationPluralProps,
+ void
+> {
+ render(): JSX.Element {
+ const s = stringifyChildren(this.props.children);
+ const translation = i18nCore.jed.ngettext(s, s, this.props.target);
+ const result = getTranslatedChildren(translation, this.props.children);
+ return <div>{result}</div>;
+ }
+}
diff --git a/packages/taler-wallet-webextension/src/pageEntryPoint.ts b/packages/taler-wallet-webextension/src/pageEntryPoint.ts
new file mode 100644
index 000000000..9fd1d36f1
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/pageEntryPoint.ts
@@ -0,0 +1,72 @@
+/*
+ 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/>
+ */
+
+/**
+ * Main entry point for extension pages.
+ *
+ * @author Florian Dold <dold@taler.net>
+ */
+
+import ReactDOM from "react-dom";
+import { createPopup } from "./pages/popup";
+import { createWithdrawPage } from "./pages/withdraw";
+import { createWelcomePage } from "./pages/welcome";
+import { createPayPage } from "./pages/pay";
+import { createRefundPage } from "./pages/refund";
+
+function main(): void {
+ try {
+ let mainElement;
+ const m = location.pathname.match(/([^/]+)$/);
+ if (!m) {
+ throw Error("can't parse page URL");
+ }
+ const page = m[1];
+ switch (page) {
+ case "popup.html":
+ mainElement = createPopup();
+ break;
+ case "withdraw.html":
+ mainElement = createWithdrawPage();
+ break;
+ case "welcome.html":
+ mainElement = createWelcomePage();
+ break;
+ case "pay.html":
+ mainElement = createPayPage();
+ break;
+ case "refund.html":
+ mainElement = createRefundPage();
+ break;
+ default:
+ throw Error(`page '${page}' not implemented`);
+ }
+ const container = document.getElementById("container");
+ if (!container) {
+ throw Error("container not found, can't mount page contents");
+ }
+ ReactDOM.render(mainElement, container);
+ } catch (e) {
+ console.error("got error", e);
+ document.body.innerText = `Fatal error: "${e.message}". Please report this bug at https://bugs.gnunet.org/.`;
+ }
+}
+
+if (document.readyState === "loading") {
+ document.addEventListener("DOMContentLoaded", main);
+} else {
+ main();
+}
diff --git a/packages/taler-wallet-webextension/src/pages/pay.tsx b/packages/taler-wallet-webextension/src/pages/pay.tsx
new file mode 100644
index 000000000..2abd423bd
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/pages/pay.tsx
@@ -0,0 +1,180 @@
+/*
+ This file is part of TALER
+ (C) 2015 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/>
+ */
+
+/**
+ * Page shown to the user to confirm entering
+ * a contract.
+ */
+
+/**
+ * Imports.
+ */
+import * as i18n from "../i18n";
+
+
+import { renderAmount, ProgressButton } from "../renderHtml";
+import * as wxApi from "../wxApi";
+
+import React, { useState, useEffect } from "react";
+
+import { Amounts, AmountJson, walletTypes, talerTypes } from "taler-wallet-core";
+
+function TalerPayDialog({ talerPayUri }: { talerPayUri: string }): JSX.Element {
+ const [payStatus, setPayStatus] = useState<walletTypes.PreparePayResult | undefined>();
+ const [payErrMsg, setPayErrMsg] = useState<string | undefined>("");
+ const [numTries, setNumTries] = useState(0);
+ const [loading, setLoading] = useState(false);
+ let amountEffective: AmountJson | undefined = undefined;
+
+ useEffect(() => {
+ const doFetch = async (): Promise<void> => {
+ const p = await wxApi.preparePay(talerPayUri);
+ setPayStatus(p);
+ };
+ doFetch();
+ }, [numTries, talerPayUri]);
+
+ if (!payStatus) {
+ return <span>Loading payment information ...</span>;
+ }
+
+ let insufficientBalance = false;
+ if (payStatus.status == "insufficient-balance") {
+ insufficientBalance = true;
+ }
+
+ if (payStatus.status === "payment-possible") {
+ amountEffective = Amounts.parseOrThrow(payStatus.amountEffective);
+ }
+
+ if (payStatus.status === walletTypes.PreparePayResultType.AlreadyConfirmed && numTries === 0) {
+ return (
+ <span>
+ You have already paid for this article. Click{" "}
+ <a href={payStatus.nextUrl}>here</a> to view it again.
+ </span>
+ );
+ }
+
+ let contractTerms: talerTypes.ContractTerms;
+
+ try {
+ contractTerms = talerTypes.codecForContractTerms().decode(payStatus.contractTerms);
+ } catch (e) {
+ // This should never happen, as the wallet is supposed to check the contract terms
+ // before storing them.
+ console.error(e);
+ console.log("raw contract terms were", payStatus.contractTerms);
+ return <span>Invalid contract terms.</span>;
+ }
+
+ if (!contractTerms) {
+ return (
+ <span>
+ Error: did not get contract terms from merchant or wallet backend.
+ </span>
+ );
+ }
+
+ let merchantName: React.ReactElement;
+ if (contractTerms.merchant && contractTerms.merchant.name) {
+ merchantName = <strong>{contractTerms.merchant.name}</strong>;
+ } else {
+ merchantName = <strong>(pub: {contractTerms.merchant_pub})</strong>;
+ }
+
+ const amount = (
+ <strong>{renderAmount(Amounts.parseOrThrow(contractTerms.amount))}</strong>
+ );
+
+ const doPayment = async (): Promise<void> => {
+ if (payStatus.status !== "payment-possible") {
+ throw Error(`invalid state: ${payStatus.status}`);
+ }
+ const proposalId = payStatus.proposalId;
+ setNumTries(numTries + 1);
+ try {
+ setLoading(true);
+ const res = await wxApi.confirmPay(proposalId, undefined);
+ document.location.href = res.nextUrl;
+ } catch (e) {
+ console.error(e);
+ setPayErrMsg(e.message);
+ }
+ };
+
+ return (
+ <div>
+ <p>
+ <i18n.Translate wrap="p">
+ The merchant <span>{merchantName}</span> offers you to purchase:
+ </i18n.Translate>
+ <div style={{ textAlign: "center" }}>
+ <strong>{contractTerms.summary}</strong>
+ </div>
+ {amountEffective ? (
+ <i18n.Translate wrap="p">
+ The total price is <span>{amount} </span>
+ (plus <span>{renderAmount(amountEffective)}</span> fees).
+ </i18n.Translate>
+ ) : (
+ <i18n.Translate wrap="p">
+ The total price is <span>{amount}</span>.
+ </i18n.Translate>
+ )}
+ </p>
+
+ {insufficientBalance ? (
+ <div>
+ <p style={{ color: "red", fontWeight: "bold" }}>
+ Unable to pay: Your balance is insufficient.
+ </p>
+ </div>
+ ) : null}
+
+ {payErrMsg ? (
+ <div>
+ <p>Payment failed: {payErrMsg}</p>
+ <button
+ className="pure-button button-success"
+ onClick={() => doPayment()}
+ >
+ {i18n.str`Retry`}
+ </button>
+ </div>
+ ) : (
+ <div>
+ <ProgressButton
+ loading={loading}
+ disabled={insufficientBalance}
+ onClick={() => doPayment()}
+ >
+ {i18n.str`Confirm payment`}
+ </ProgressButton>
+ </div>
+ )}
+ </div>
+ );
+}
+
+export function createPayPage(): JSX.Element {
+ const url = new URL(document.location.href);
+ const talerPayUri = url.searchParams.get("talerPayUri");
+ if (!talerPayUri) {
+ throw Error("invalid parameter");
+ }
+ return <TalerPayDialog talerPayUri={talerPayUri} />;
+}
diff --git a/packages/taler-wallet-webextension/src/pages/payback.tsx b/packages/taler-wallet-webextension/src/pages/payback.tsx
new file mode 100644
index 000000000..5d42f5f47
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/pages/payback.tsx
@@ -0,0 +1,30 @@
+/*
+ This file is part of TALER
+ (C) 2017 Inria
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * View and edit auditors.
+ *
+ * @author Florian Dold
+ */
+
+/**
+ * Imports.
+ */
+import * as React from "react";
+
+export function makePaybackPage(): JSX.Element {
+ return <div>not implemented</div>;
+}
diff --git a/packages/taler-wallet-webextension/src/pages/popup.tsx b/packages/taler-wallet-webextension/src/pages/popup.tsx
new file mode 100644
index 000000000..72c9f4bcb
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/pages/popup.tsx
@@ -0,0 +1,502 @@
+/*
+ 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/>
+ */
+
+/**
+ * Popup shown to the user when they click
+ * the Taler browser action button.
+ *
+ * @author Florian Dold
+ */
+
+/**
+ * Imports.
+ */
+import * as i18n from "../i18n";
+
+import {
+ AmountJson,
+ Amounts,
+ time,
+ taleruri,
+ walletTypes,
+} from "taler-wallet-core";
+
+
+import { abbrev, renderAmount, PageLink } from "../renderHtml";
+import * as wxApi from "../wxApi";
+
+import React, { Fragment, useState, useEffect } from "react";
+
+import moment from "moment";
+import { PermissionsCheckbox } from "./welcome";
+
+// FIXME: move to newer react functions
+/* eslint-disable react/no-deprecated */
+
+class Router extends React.Component<any, any> {
+ static setRoute(s: string): void {
+ window.location.hash = s;
+ }
+
+ static getRoute(): string {
+ // Omit the '#' at the beginning
+ return window.location.hash.substring(1);
+ }
+
+ static onRoute(f: any): () => void {
+ Router.routeHandlers.push(f);
+ return () => {
+ const i = Router.routeHandlers.indexOf(f);
+ this.routeHandlers = this.routeHandlers.splice(i, 1);
+ };
+ }
+
+ private static routeHandlers: any[] = [];
+
+ componentWillMount(): void {
+ console.log("router mounted");
+ window.onhashchange = () => {
+ this.setState({});
+ for (const f of Router.routeHandlers) {
+ f();
+ }
+ };
+ }
+
+ render(): JSX.Element {
+ const route = window.location.hash.substring(1);
+ console.log("rendering route", route);
+ let defaultChild: React.ReactChild | null = null;
+ let foundChild: React.ReactChild | null = null;
+ React.Children.forEach(this.props.children, (child) => {
+ const childProps: any = (child as any).props;
+ if (!childProps) {
+ return;
+ }
+ if (childProps.default) {
+ defaultChild = child as React.ReactChild;
+ }
+ if (childProps.route === route) {
+ foundChild = child as React.ReactChild;
+ }
+ });
+ const c: React.ReactChild | null = foundChild || defaultChild;
+ if (!c) {
+ throw Error("unknown route");
+ }
+ Router.setRoute((c as any).props.route);
+ return <div>{c}</div>;
+ }
+}
+
+interface TabProps {
+ target: string;
+ children?: React.ReactNode;
+}
+
+function Tab(props: TabProps): JSX.Element {
+ let cssClass = "";
+ if (props.target === Router.getRoute()) {
+ cssClass = "active";
+ }
+ const onClick = (e: React.MouseEvent<HTMLAnchorElement>): void => {
+ Router.setRoute(props.target);
+ e.preventDefault();
+ };
+ return (
+ <a onClick={onClick} href={props.target} className={cssClass}>
+ {props.children}
+ </a>
+ );
+}
+
+class WalletNavBar extends React.Component<any, any> {
+ private cancelSubscription: any;
+
+ componentWillMount(): void {
+ this.cancelSubscription = Router.onRoute(() => {
+ this.setState({});
+ });
+ }
+
+ componentWillUnmount(): void {
+ if (this.cancelSubscription) {
+ this.cancelSubscription();
+ }
+ }
+
+ render(): JSX.Element {
+ console.log("rendering nav bar");
+ return (
+ <div className="nav" id="header">
+ <Tab target="/balance">{i18n.str`Balance`}</Tab>
+ <Tab target="/history">{i18n.str`History`}</Tab>
+ <Tab target="/settings">{i18n.str`Settings`}</Tab>
+ <Tab target="/debug">{i18n.str`Debug`}</Tab>
+ </div>
+ );
+ }
+}
+
+/**
+ * Render an amount as a large number with a small currency symbol.
+ */
+function bigAmount(amount: AmountJson): JSX.Element {
+ const v = amount.value + amount.fraction / Amounts.fractionalBase;
+ return (
+ <span>
+ <span style={{ fontSize: "5em", display: "block" }}>{v}</span>{" "}
+ <span>{amount.currency}</span>
+ </span>
+ );
+}
+
+function EmptyBalanceView(): JSX.Element {
+ return (
+ <i18n.Translate wrap="p">
+ You have no balance to show. Need some{" "}
+ <PageLink pageName="welcome.html">help</PageLink> getting started?
+ </i18n.Translate>
+ );
+}
+
+class WalletBalanceView extends React.Component<any, any> {
+ private balance?: walletTypes.BalancesResponse;
+ private gotError = false;
+ private canceler: (() => void) | undefined = undefined;
+ private unmount = false;
+ private updateBalanceRunning = false;
+
+ componentWillMount(): void {
+ this.canceler = wxApi.onUpdateNotification(() => this.updateBalance());
+ this.updateBalance();
+ }
+
+ componentWillUnmount(): void {
+ console.log("component WalletBalanceView will unmount");
+ if (this.canceler) {
+ this.canceler();
+ }
+ this.unmount = true;
+ }
+
+ async updateBalance(): Promise<void> {
+ if (this.updateBalanceRunning) {
+ return;
+ }
+ this.updateBalanceRunning = true;
+ let balance: walletTypes.BalancesResponse;
+ try {
+ balance = await wxApi.getBalance();
+ } catch (e) {
+ if (this.unmount) {
+ return;
+ }
+ this.gotError = true;
+ console.error("could not retrieve balances", e);
+ this.setState({});
+ return;
+ } finally {
+ this.updateBalanceRunning = false;
+ }
+ if (this.unmount) {
+ return;
+ }
+ this.gotError = false;
+ console.log("got balance", balance);
+ this.balance = balance;
+ this.setState({});
+ }
+
+ formatPending(entry: walletTypes.Balance): JSX.Element {
+ let incoming: JSX.Element | undefined;
+ let payment: JSX.Element | undefined;
+
+ const available = Amounts.parseOrThrow(entry.available);
+ const pendingIncoming = Amounts.parseOrThrow(entry.pendingIncoming);
+ const pendingOutgoing = Amounts.parseOrThrow(entry.pendingOutgoing);
+
+ console.log(
+ "available: ",
+ entry.pendingIncoming ? renderAmount(entry.available) : null,
+ );
+ console.log(
+ "incoming: ",
+ entry.pendingIncoming ? renderAmount(entry.pendingIncoming) : null,
+ );
+
+ if (!Amounts.isZero(pendingIncoming)) {
+ incoming = (
+ <i18n.Translate wrap="span">
+ <span style={{ color: "darkgreen" }}>
+ {"+"}
+ {renderAmount(entry.pendingIncoming)}
+ </span>{" "}
+ incoming
+ </i18n.Translate>
+ );
+ }
+
+ const l = [incoming, payment].filter((x) => x !== undefined);
+ if (l.length === 0) {
+ return <span />;
+ }
+
+ if (l.length === 1) {
+ return <span>({l})</span>;
+ }
+ return (
+ <span>
+ ({l[0]}, {l[1]})
+ </span>
+ );
+ }
+
+ render(): JSX.Element {
+ const wallet = this.balance;
+ if (this.gotError) {
+ return (
+ <div className="balance">
+ <p>{i18n.str`Error: could not retrieve balance information.`}</p>
+ <p>
+ Click <PageLink pageName="welcome.html">here</PageLink> for help and
+ diagnostics.
+ </p>
+ </div>
+ );
+ }
+ if (!wallet) {
+ return <span></span>;
+ }
+ console.log(wallet);
+ const listing = wallet.balances.map((entry) => {
+ const av = Amounts.parseOrThrow(entry.available);
+ return (
+ <p key={av.currency}>
+ {bigAmount(av)} {this.formatPending(entry)}
+ </p>
+ );
+ });
+ return listing.length > 0 ? (
+ <div className="balance">{listing}</div>
+ ) : (
+ <EmptyBalanceView />
+ );
+ }
+}
+
+function Icon({ l }: { l: string }): JSX.Element {
+ return <div className={"icon"}>{l}</div>;
+}
+
+function formatAndCapitalize(text: string): string {
+ text = text.replace("-", " ");
+ text = text.replace(/^./, text[0].toUpperCase());
+ return text;
+}
+
+const HistoryComponent = (props: any): JSX.Element => {
+ return <span>TBD</span>;
+};
+
+class WalletSettings extends React.Component<any, any> {
+ render(): JSX.Element {
+ return (
+ <div>
+ <h2>Permissions</h2>
+ <PermissionsCheckbox />
+ </div>
+ );
+ }
+}
+
+function reload(): void {
+ try {
+ chrome.runtime.reload();
+ window.close();
+ } catch (e) {
+ // Functionality missing in firefox, ignore!
+ }
+}
+
+function confirmReset(): void {
+ if (
+ confirm(
+ "Do you want to IRREVOCABLY DESTROY everything inside your" +
+ " wallet and LOSE ALL YOUR COINS?",
+ )
+ ) {
+ wxApi.resetDb();
+ window.close();
+ }
+}
+
+function WalletDebug(props: any): JSX.Element {
+ return (
+ <div>
+ <p>Debug tools:</p>
+ <button onClick={openExtensionPage("/popup.html")}>wallet tab</button>
+ <button onClick={openExtensionPage("/benchmark.html")}>benchmark</button>
+ <button onClick={openExtensionPage("/show-db.html")}>show db</button>
+ <button onClick={openExtensionPage("/tree.html")}>show tree</button>
+ <br />
+ <button onClick={confirmReset}>reset</button>
+ <button onClick={reload}>reload chrome extension</button>
+ </div>
+ );
+}
+
+function openExtensionPage(page: string) {
+ return () => {
+ chrome.tabs.create({
+ url: chrome.extension.getURL(page),
+ });
+ };
+}
+
+function openTab(page: string) {
+ return (evt: React.SyntheticEvent<any>) => {
+ evt.preventDefault();
+ chrome.tabs.create({
+ url: page,
+ });
+ };
+}
+
+function makeExtensionUrlWithParams(
+ url: string,
+ params?: { [name: string]: string | undefined },
+): string {
+ const innerUrl = new URL(chrome.extension.getURL("/" + url));
+ if (params) {
+ for (const key in params) {
+ const p = params[key];
+ if (p) {
+ innerUrl.searchParams.set(key, p);
+ }
+ }
+ }
+ return innerUrl.href;
+}
+
+function actionForTalerUri(talerUri: string): string | undefined {
+ const uriType = taleruri.classifyTalerUri(talerUri);
+ switch (uriType) {
+ case taleruri.TalerUriType.TalerWithdraw:
+ return makeExtensionUrlWithParams("withdraw.html", {
+ talerWithdrawUri: talerUri,
+ });
+ case taleruri.TalerUriType.TalerPay:
+ return makeExtensionUrlWithParams("pay.html", {
+ talerPayUri: talerUri,
+ });
+ case taleruri.TalerUriType.TalerTip:
+ return makeExtensionUrlWithParams("tip.html", {
+ talerTipUri: talerUri,
+ });
+ case taleruri.TalerUriType.TalerRefund:
+ return makeExtensionUrlWithParams("refund.html", {
+ talerRefundUri: talerUri,
+ });
+ case taleruri.TalerUriType.TalerNotifyReserve:
+ // FIXME: implement
+ break;
+ default:
+ console.warn(
+ "Response with HTTP 402 has Taler header, but header value is not a taler:// URI.",
+ );
+ break;
+ }
+ return undefined;
+}
+
+async function findTalerUriInActiveTab(): Promise<string | undefined> {
+ return new Promise((resolve, reject) => {
+ chrome.tabs.executeScript(
+ {
+ code: `
+ (() => {
+ let x = document.querySelector("a[href^='taler://'");
+ return x ? x.href.toString() : null;
+ })();
+ `,
+ allFrames: false,
+ },
+ (result) => {
+ if (chrome.runtime.lastError) {
+ console.error(chrome.runtime.lastError);
+ resolve(undefined);
+ return;
+ }
+ console.log("got result", result);
+ resolve(result[0]);
+ },
+ );
+ });
+}
+
+function WalletPopup(): JSX.Element {
+ const [talerActionUrl, setTalerActionUrl] = useState<string | undefined>(
+ undefined,
+ );
+ const [dismissed, setDismissed] = useState(false);
+ useEffect(() => {
+ async function check(): Promise<void> {
+ const talerUri = await findTalerUriInActiveTab();
+ if (talerUri) {
+ const actionUrl = actionForTalerUri(talerUri);
+ setTalerActionUrl(actionUrl);
+ }
+ }
+ check();
+ });
+ if (talerActionUrl && !dismissed) {
+ return (
+ <div style={{ padding: "1em" }}>
+ <h1>Taler Action</h1>
+ <p>This page has a Taler action. </p>
+ <p>
+ <button
+ onClick={() => {
+ window.open(talerActionUrl, "_blank");
+ }}
+ >
+ Open
+ </button>
+ </p>
+ <p>
+ <button onClick={() => setDismissed(true)}>Dismiss</button>
+ </p>
+ </div>
+ );
+ }
+ return (
+ <div>
+ <WalletNavBar />
+ <div style={{ margin: "1em" }}>
+ <Router>
+ <WalletBalanceView route="/balance" default />
+ <WalletSettings route="/settings" />
+ <WalletDebug route="/debug" />
+ </Router>
+ </div>
+ </div>
+ );
+}
+
+export function createPopup(): JSX.Element {
+ return <WalletPopup />;
+}
diff --git a/packages/taler-wallet-webextension/src/pages/refund.tsx b/packages/taler-wallet-webextension/src/pages/refund.tsx
new file mode 100644
index 000000000..7326dfc88
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/pages/refund.tsx
@@ -0,0 +1,89 @@
+/*
+ This file is part of TALER
+ (C) 2015-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/>
+ */
+
+/**
+ * Page that shows refund status for purchases.
+ *
+ * @author Florian Dold
+ */
+
+import React, { useEffect, useState } from "react";
+
+import * as wxApi from "../wxApi";
+import { AmountView } from "../renderHtml";
+import { walletTypes } from "taler-wallet-core";
+
+function RefundStatusView(props: { talerRefundUri: string }): JSX.Element {
+ const [applied, setApplied] = useState(false);
+ const [purchaseDetails, setPurchaseDetails] = useState<
+ walletTypes.PurchaseDetails | undefined
+ >(undefined);
+ const [errMsg, setErrMsg] = useState<string | undefined>(undefined);
+
+ useEffect(() => {
+ const doFetch = async (): Promise<void> => {
+ try {
+ const result = await wxApi.applyRefund(props.talerRefundUri);
+ setApplied(true);
+ const r = await wxApi.getPurchaseDetails(result.proposalId);
+ setPurchaseDetails(r);
+ } catch (e) {
+ console.error(e);
+ setErrMsg(e.message);
+ console.log("err message", e.message);
+ }
+ };
+ doFetch();
+ }, [props.talerRefundUri]);
+
+ console.log("rendering");
+
+ if (errMsg) {
+ return <span>Error: {errMsg}</span>;
+ }
+
+ if (!applied || !purchaseDetails) {
+ return <span>Updating refund status</span>;
+ }
+
+ return (
+ <>
+ <h2>Refund Status</h2>
+ <p>
+ The product <em>{purchaseDetails.contractTerms.summary}</em> has
+ received a total refund of{" "}
+ <AmountView amount={purchaseDetails.totalRefundAmount} />.
+ </p>
+ <p>Note that additional fees from the exchange may apply.</p>
+ </>
+ );
+}
+
+export function createRefundPage(): JSX.Element {
+ const url = new URL(document.location.href);
+
+ const container = document.getElementById("container");
+ if (!container) {
+ throw Error("fatal: can't mount component, container missing");
+ }
+
+ const talerRefundUri = url.searchParams.get("talerRefundUri");
+ if (!talerRefundUri) {
+ throw Error("taler refund URI requred");
+ }
+
+ return <RefundStatusView talerRefundUri={talerRefundUri} />;
+}
diff --git a/packages/taler-wallet-webextension/src/pages/reset-required.tsx b/packages/taler-wallet-webextension/src/pages/reset-required.tsx
new file mode 100644
index 000000000..0ef5fe8b7
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/pages/reset-required.tsx
@@ -0,0 +1,93 @@
+/*
+ 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/>
+ */
+
+/**
+ * Page to inform the user when a database reset is required.
+ *
+ * @author Florian Dold
+ */
+
+import * as React from "react";
+
+import * as wxApi from "../wxApi";
+
+interface State {
+ /**
+ * Did the user check the confirmation check box?
+ */
+ checked: boolean;
+
+ /**
+ * Do we actually need to reset the db?
+ */
+ resetRequired: boolean;
+}
+
+class ResetNotification extends React.Component<any, State> {
+ constructor(props: any) {
+ super(props);
+ this.state = { checked: false, resetRequired: true };
+ setInterval(() => this.update(), 500);
+ }
+ async update(): Promise<void> {
+ const res = await wxApi.checkUpgrade();
+ this.setState({ resetRequired: res.dbResetRequired });
+ }
+ render(): JSX.Element {
+ if (this.state.resetRequired) {
+ return (
+ <div>
+ <h1>Manual Reset Reqired</h1>
+ <p>
+ The wallet&apos;s database in your browser is incompatible with the{" "}
+ currently installed wallet. Please reset manually.
+ </p>
+ <p>
+ Once the database format has stabilized, we will provide automatic
+ upgrades.
+ </p>
+ <input
+ id="check"
+ type="checkbox"
+ checked={this.state.checked}
+ onChange={(e) => this.setState({ checked: e.target.checked })}
+ />{" "}
+ <label htmlFor="check">
+ I understand that I will lose all my data
+ </label>
+ <br />
+ <button
+ className="pure-button"
+ disabled={!this.state.checked}
+ onClick={() => wxApi.resetDb()}
+ >
+ Reset
+ </button>
+ </div>
+ );
+ }
+ return (
+ <div>
+ <h1>Everything is fine!</h1>A reset is not required anymore, you can
+ close this page.
+ </div>
+ );
+ }
+}
+
+export function createResetRequiredPage(): JSX.Element {
+ return <ResetNotification />;
+}
diff --git a/packages/taler-wallet-webextension/src/pages/return-coins.tsx b/packages/taler-wallet-webextension/src/pages/return-coins.tsx
new file mode 100644
index 000000000..e8cf8c9dd
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/pages/return-coins.tsx
@@ -0,0 +1,30 @@
+/*
+ This file is part of TALER
+ (C) 2017 Inria
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Return coins to own bank account.
+ *
+ * @author Florian Dold
+ */
+
+/**
+ * Imports.
+ */
+import * as React from "react";
+
+export function createReturnCoinsPage(): JSX.Element {
+ return <span>Not implemented yet.</span>;
+}
diff --git a/packages/taler-wallet-webextension/src/pages/tip.tsx b/packages/taler-wallet-webextension/src/pages/tip.tsx
new file mode 100644
index 000000000..6cf4e1875
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/pages/tip.tsx
@@ -0,0 +1,103 @@
+/*
+ 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/>
+ */
+
+/**
+ * Page shown to the user to confirm creation
+ * of a reserve, usually requested by the bank.
+ *
+ * @author Florian Dold
+ */
+
+import * as React from "react";
+
+import { acceptTip, getTipStatus } from "../wxApi";
+
+import { renderAmount, ProgressButton } from "../renderHtml";
+
+import { useState, useEffect } from "react";
+import { walletTypes } from "taler-wallet-core";
+
+function TipDisplay(props: { talerTipUri: string }): JSX.Element {
+ const [tipStatus, setTipStatus] = useState<walletTypes.TipStatus | undefined>(undefined);
+ const [discarded, setDiscarded] = useState(false);
+ const [loading, setLoading] = useState(false);
+ const [finished, setFinished] = useState(false);
+
+ useEffect(() => {
+ const doFetch = async (): Promise<void> => {
+ const ts = await getTipStatus(props.talerTipUri);
+ setTipStatus(ts);
+ };
+ doFetch();
+ }, [props.talerTipUri]);
+
+ if (discarded) {
+ return <span>You&apos;ve discarded the tip.</span>;
+ }
+
+ if (finished) {
+ return <span>Tip has been accepted!</span>;
+ }
+
+ if (!tipStatus) {
+ return <span>Loading ...</span>;
+ }
+
+ const discard = (): void => {
+ setDiscarded(true);
+ };
+
+ const accept = async (): Promise<void> => {
+ setLoading(true);
+ await acceptTip(tipStatus.tipId);
+ setFinished(true);
+ };
+
+ return (
+ <div>
+ <h2>Tip Received!</h2>
+ <p>
+ You received a tip of <strong>{renderAmount(tipStatus.amount)}</strong>{" "}
+ from <span> </span>
+ <strong>{tipStatus.merchantOrigin}</strong>.
+ </p>
+ <p>
+ The tip is handled by the exchange{" "}
+ <strong>{tipStatus.exchangeUrl}</strong>. This exchange will charge fees
+ of <strong>{renderAmount(tipStatus.totalFees)}</strong> for this
+ operation.
+ </p>
+ <form className="pure-form">
+ <ProgressButton loading={loading} onClick={() => accept()}>
+ Accept Tip
+ </ProgressButton>{" "}
+ <button className="pure-button" type="button" onClick={() => discard()}>
+ Discard tip
+ </button>
+ </form>
+ </div>
+ );
+}
+
+export function createTipPage(): JSX.Element {
+ const url = new URL(document.location.href);
+ const talerTipUri = url.searchParams.get("talerTipUri");
+ if (typeof talerTipUri !== "string") {
+ throw Error("talerTipUri must be a string");
+ }
+
+ return <TipDisplay talerTipUri={talerTipUri} />;
+}
diff --git a/packages/taler-wallet-webextension/src/pages/welcome.tsx b/packages/taler-wallet-webextension/src/pages/welcome.tsx
new file mode 100644
index 000000000..ff5de572c
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/pages/welcome.tsx
@@ -0,0 +1,190 @@
+/*
+ This file is part of GNU Taler
+ (C) 2019 Taler Systems SA
+
+ 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/>
+ */
+
+/**
+ * Welcome page, shown on first installs.
+ *
+ * @author Florian Dold
+ */
+
+import React, { useState, useEffect } from "react";
+import { getDiagnostics } from "../wxApi";
+import { PageLink } from "../renderHtml";
+import * as wxApi from "../wxApi";
+import { getPermissionsApi } from "../compat";
+import { extendedPermissions } from "../permissions";
+import { walletTypes } from "taler-wallet-core";
+
+function Diagnostics(): JSX.Element | null {
+ const [timedOut, setTimedOut] = useState(false);
+ const [diagnostics, setDiagnostics] = useState<walletTypes.WalletDiagnostics | undefined>(
+ undefined,
+ );
+
+ useEffect(() => {
+ let gotDiagnostics = false;
+ setTimeout(() => {
+ if (!gotDiagnostics) {
+ console.error("timed out");
+ setTimedOut(true);
+ }
+ }, 1000);
+ const doFetch = async (): Promise<void> => {
+ const d = await getDiagnostics();
+ console.log("got diagnostics", d);
+ gotDiagnostics = true;
+ setDiagnostics(d);
+ };
+ console.log("fetching diagnostics");
+ doFetch();
+ }, []);
+
+ if (timedOut) {
+ return <p>Diagnostics timed out. Could not talk to the wallet backend.</p>;
+ }
+
+ if (diagnostics) {
+ if (diagnostics.errors.length === 0) {
+ return null;
+ } else {
+ return (
+ <div
+ style={{
+ borderLeft: "0.5em solid red",
+ paddingLeft: "1em",
+ paddingTop: "0.2em",
+ paddingBottom: "0.2em",
+ }}
+ >
+ <p>Problems detected:</p>
+ <ol>
+ {diagnostics.errors.map((errMsg) => (
+ <li key={errMsg}>{errMsg}</li>
+ ))}
+ </ol>
+ {diagnostics.firefoxIdbProblem ? (
+ <p>
+ Please check in your <code>about:config</code> settings that you
+ have IndexedDB enabled (check the preference name{" "}
+ <code>dom.indexedDB.enabled</code>).
+ </p>
+ ) : null}
+ {diagnostics.dbOutdated ? (
+ <p>
+ Your wallet database is outdated. Currently automatic migration is
+ not supported. Please go{" "}
+ <PageLink pageName="reset-required.html">here</PageLink> to reset
+ the wallet database.
+ </p>
+ ) : null}
+ </div>
+ );
+ }
+ }
+
+ return <p>Running diagnostics ...</p>;
+}
+
+export function PermissionsCheckbox(): JSX.Element {
+ const [extendedPermissionsEnabled, setExtendedPermissionsEnabled] = useState(
+ false,
+ );
+ async function handleExtendedPerm(requestedVal: boolean): Promise<void> {
+ let nextVal: boolean | undefined;
+ if (requestedVal) {
+ const granted = await new Promise<boolean>((resolve, reject) => {
+ // We set permissions here, since apparently FF wants this to be done
+ // as the result of an input event ...
+ getPermissionsApi().request(extendedPermissions, (granted: boolean) => {
+ if (chrome.runtime.lastError) {
+ console.error("error requesting permissions");
+ console.error(chrome.runtime.lastError);
+ reject(chrome.runtime.lastError);
+ return;
+ }
+ console.log("permissions granted:", granted);
+ resolve(granted);
+ });
+ });
+ const res = await wxApi.setExtendedPermissions(granted);
+ console.log(res);
+ nextVal = res.newValue;
+ } else {
+ const res = await wxApi.setExtendedPermissions(false);
+ console.log(res);
+ nextVal = res.newValue;
+ }
+ console.log("new permissions applied:", nextVal);
+ setExtendedPermissionsEnabled(nextVal ?? false);
+ }
+ useEffect(() => {
+ async function getExtendedPermValue(): Promise<void> {
+ const res = await wxApi.getExtendedPermissions();
+ setExtendedPermissionsEnabled(res.newValue);
+ }
+ getExtendedPermValue();
+ });
+ return (
+ <div>
+ <input
+ checked={extendedPermissionsEnabled}
+ onChange={(x) => handleExtendedPerm(x.target.checked)}
+ type="checkbox"
+ id="checkbox-perm"
+ style={{ width: "1.5em", height: "1.5em", verticalAlign: "middle" }}
+ />
+ <label
+ htmlFor="checkbox-perm"
+ style={{ marginLeft: "0.5em", fontWeight: "bold" }}
+ >
+ Automatically open wallet based on page content
+ </label>
+ <span
+ style={{
+ color: "#383838",
+ fontSize: "smaller",
+ display: "block",
+ marginLeft: "2em",
+ }}
+ >
+ (Enabling this option below will make using the wallet faster, but
+ requires more permissions from your browser.)
+ </span>
+ </div>
+ );
+}
+
+function Welcome(): JSX.Element {
+ return (
+ <>
+ <p>Thank you for installing the wallet.</p>
+ <Diagnostics />
+ <h2>Permissions</h2>
+ <PermissionsCheckbox />
+ <h2>Next Steps</h2>
+ <a href="https://demo.taler.net/" style={{ display: "block" }}>
+ Try the demo »
+ </a>
+ <a href="https://demo.taler.net/" style={{ display: "block" }}>
+ Learn how to top up your wallet balance »
+ </a>
+ </>
+ );
+}
+
+export function createWelcomePage(): JSX.Element {
+ return <Welcome />;
+}
diff --git a/packages/taler-wallet-webextension/src/pages/withdraw.tsx b/packages/taler-wallet-webextension/src/pages/withdraw.tsx
new file mode 100644
index 000000000..4a92704b3
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/pages/withdraw.tsx
@@ -0,0 +1,229 @@
+/*
+ This file is part of TALER
+ (C) 2015-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/>
+ */
+
+/**
+ * Page shown to the user to confirm creation
+ * of a reserve, usually requested by the bank.
+ *
+ * @author Florian Dold
+ */
+
+import * as i18n from "../i18n";
+
+import { WithdrawDetailView, renderAmount } from "../renderHtml";
+
+import React, { useState, useEffect } from "react";
+import {
+ acceptWithdrawal,
+ onUpdateNotification,
+} from "../wxApi";
+
+function WithdrawalDialog(props: { talerWithdrawUri: string }): JSX.Element {
+ const [details, setDetails] = useState<
+ any | undefined
+ >();
+ const [selectedExchange, setSelectedExchange] = useState<
+ string | undefined
+ >();
+ const talerWithdrawUri = props.talerWithdrawUri;
+ const [cancelled, setCancelled] = useState(false);
+ const [selecting, setSelecting] = useState(false);
+ const [customUrl, setCustomUrl] = useState<string>("");
+ const [errMsg, setErrMsg] = useState<string | undefined>("");
+ const [updateCounter, setUpdateCounter] = useState(1);
+
+ useEffect(() => {
+ return onUpdateNotification(() => {
+ setUpdateCounter(updateCounter + 1);
+ });
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ useEffect(() => {
+ const fetchData = async (): Promise<void> => {
+ // FIXME: re-implement with new API
+ // console.log("getting from", talerWithdrawUri);
+ // let d: WithdrawalDetailsResponse | undefined = undefined;
+ // try {
+ // d = await getWithdrawDetails(talerWithdrawUri, selectedExchange);
+ // } catch (e) {
+ // console.error(
+ // `error getting withdraw details for uri ${talerWithdrawUri}, exchange ${selectedExchange}`,
+ // e,
+ // );
+ // setErrMsg(e.message);
+ // return;
+ // }
+ // console.log("got withdrawDetails", d);
+ // if (!selectedExchange && d.bankWithdrawDetails.suggestedExchange) {
+ // console.log("setting selected exchange");
+ // setSelectedExchange(d.bankWithdrawDetails.suggestedExchange);
+ // }
+ // setDetails(d);
+ };
+ fetchData();
+ }, [selectedExchange, errMsg, selecting, talerWithdrawUri, updateCounter]);
+
+ if (errMsg) {
+ return (
+ <div>
+ <i18n.Translate wrap="p">
+ Could not get details for withdraw operation:
+ </i18n.Translate>
+ <p style={{ color: "red" }}>{errMsg}</p>
+ <p>
+ <span
+ role="button"
+ tabIndex={0}
+ style={{ textDecoration: "underline", cursor: "pointer" }}
+ onClick={() => {
+ setSelecting(true);
+ setErrMsg(undefined);
+ setSelectedExchange(undefined);
+ setDetails(undefined);
+ }}
+ >
+ {i18n.str`Chose different exchange provider`}
+ </span>
+ </p>
+ </div>
+ );
+ }
+
+ if (!details) {
+ return <span>Loading...</span>;
+ }
+
+ if (cancelled) {
+ return <span>Withdraw operation has been cancelled.</span>;
+ }
+
+ if (selecting) {
+ const bankSuggestion =
+ details && details.bankWithdrawDetails.suggestedExchange;
+ return (
+ <div>
+ {i18n.str`Please select an exchange. You can review the details before after your selection.`}
+ {bankSuggestion && (
+ <div>
+ <h2>Bank Suggestion</h2>
+ <button
+ className="pure-button button-success"
+ onClick={() => {
+ setDetails(undefined);
+ setSelectedExchange(bankSuggestion);
+ setSelecting(false);
+ }}
+ >
+ <i18n.Translate wrap="span">
+ Select <strong>{bankSuggestion}</strong>
+ </i18n.Translate>
+ </button>
+ </div>
+ )}
+ <h2>Custom Selection</h2>
+ <p>
+ <input
+ type="text"
+ onChange={(e) => setCustomUrl(e.target.value)}
+ value={customUrl}
+ />
+ </p>
+ <button
+ className="pure-button button-success"
+ onClick={() => {
+ setDetails(undefined);
+ setSelectedExchange(customUrl);
+ setSelecting(false);
+ }}
+ >
+ <i18n.Translate wrap="span">Select custom exchange</i18n.Translate>
+ </button>
+ </div>
+ );
+ }
+
+ const accept = async (): Promise<void> => {
+ if (!selectedExchange) {
+ throw Error("can't accept, no exchange selected");
+ }
+ console.log("accepting exchange", selectedExchange);
+ const res = await acceptWithdrawal(talerWithdrawUri, selectedExchange);
+ console.log("accept withdrawal response", res);
+ if (res.confirmTransferUrl) {
+ document.location.href = res.confirmTransferUrl;
+ }
+ };
+
+ return (
+ <div>
+ <h1>Digital Cash Withdrawal</h1>
+ <i18n.Translate wrap="p">
+ You are about to withdraw{" "}
+ <strong>{renderAmount(details.bankWithdrawDetails.amount)}</strong> from
+ your bank account into your wallet.
+ </i18n.Translate>
+ {selectedExchange ? (
+ <p>
+ The exchange <strong>{selectedExchange}</strong> will be used as the
+ Taler payment service provider.
+ </p>
+ ) : null}
+
+ <div>
+ <button
+ className="pure-button button-success"
+ disabled={!selectedExchange}
+ onClick={() => accept()}
+ >
+ {i18n.str`Accept fees and withdraw`}
+ </button>
+ <p>
+ <span
+ role="button"
+ tabIndex={0}
+ style={{ textDecoration: "underline", cursor: "pointer" }}
+ onClick={() => setSelecting(true)}
+ >
+ {i18n.str`Chose different exchange provider`}
+ </span>
+ <br />
+ <span
+ role="button"
+ tabIndex={0}
+ style={{ textDecoration: "underline", cursor: "pointer" }}
+ onClick={() => setCancelled(true)}
+ >
+ {i18n.str`Cancel withdraw operation`}
+ </span>
+ </p>
+
+ {details.exchangeWithdrawDetails ? (
+ <WithdrawDetailView rci={details.exchangeWithdrawDetails} />
+ ) : null}
+ </div>
+ </div>
+ );
+}
+
+export function createWithdrawPage(): JSX.Element {
+ const url = new URL(document.location.href);
+ const talerWithdrawUri = url.searchParams.get("talerWithdrawUri");
+ if (!talerWithdrawUri) {
+ throw Error("withdraw URI required");
+ }
+ return <WithdrawalDialog talerWithdrawUri={talerWithdrawUri} />;
+}
diff --git a/packages/taler-wallet-webextension/src/permissions.ts b/packages/taler-wallet-webextension/src/permissions.ts
new file mode 100644
index 000000000..bcd357fd6
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/permissions.ts
@@ -0,0 +1,20 @@
+/*
+ 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/>
+ */
+
+export const extendedPermissions = {
+ permissions: ["webRequest", "webRequestBlocking"],
+ origins: ["http://*/*", "https://*/*"],
+};
diff --git a/packages/taler-wallet-webextension/src/renderHtml.tsx b/packages/taler-wallet-webextension/src/renderHtml.tsx
new file mode 100644
index 000000000..89f6c12e8
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/renderHtml.tsx
@@ -0,0 +1,341 @@
+/*
+ This file is part of TALER
+ (C) 2016 INRIA
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Helpers functions to render Taler-related data structures to HTML.
+ *
+ * @author Florian Dold
+ */
+
+/**
+ * Imports.
+ */
+import { AmountJson, Amounts, time, walletTypes } from "taler-wallet-core";
+import * as i18n from "./i18n";
+import React from "react";
+
+/**
+ * Render amount as HTML, which non-breaking space between
+ * decimal value and currency.
+ */
+export function renderAmount(amount: AmountJson | string): JSX.Element {
+ let a;
+ if (typeof amount === "string") {
+ a = Amounts.parse(amount);
+ } else {
+ a = amount;
+ }
+ if (!a) {
+ return <span>(invalid amount)</span>;
+ }
+ const x = a.value + a.fraction / Amounts.fractionalBase;
+ return (
+ <span>
+ {x}&nbsp;{a.currency}
+ </span>
+ );
+}
+
+export const AmountView = ({
+ amount,
+}: {
+ amount: AmountJson | string;
+}): JSX.Element => renderAmount(amount);
+
+/**
+ * Abbreviate a string to a given length, and show the full
+ * string on hover as a tooltip.
+ */
+export function abbrev(s: string, n = 5): JSX.Element {
+ let sAbbrev = s;
+ if (s.length > n) {
+ sAbbrev = s.slice(0, n) + "..";
+ }
+ return (
+ <span className="abbrev" title={s}>
+ {sAbbrev}
+ </span>
+ );
+}
+
+interface CollapsibleState {
+ collapsed: boolean;
+}
+
+interface CollapsibleProps {
+ initiallyCollapsed: boolean;
+ title: string;
+}
+
+/**
+ * Component that shows/hides its children when clicking
+ * a heading.
+ */
+export class Collapsible extends React.Component<
+ CollapsibleProps,
+ CollapsibleState
+> {
+ constructor(props: CollapsibleProps) {
+ super(props);
+ this.state = { collapsed: props.initiallyCollapsed };
+ }
+ render(): JSX.Element {
+ const doOpen = (e: any): void => {
+ this.setState({ collapsed: false });
+ e.preventDefault();
+ };
+ const doClose = (e: any): void => {
+ this.setState({ collapsed: true });
+ e.preventDefault();
+ };
+ if (this.state.collapsed) {
+ return (
+ <h2>
+ <a className="opener opener-collapsed" href="#" onClick={doOpen}>
+ {" "}
+ {this.props.title}
+ </a>
+ </h2>
+ );
+ }
+ return (
+ <div>
+ <h2>
+ <a className="opener opener-open" href="#" onClick={doClose}>
+ {" "}
+ {this.props.title}
+ </a>
+ </h2>
+ {this.props.children}
+ </div>
+ );
+ }
+}
+
+function WireFee(props: {
+ s: string;
+ rci: walletTypes.ExchangeWithdrawDetails;
+}): JSX.Element {
+ return (
+ <>
+ <thead>
+ <tr>
+ <th colSpan={3}>Wire Method {props.s}</th>
+ </tr>
+ <tr>
+ <th>Applies Until</th>
+ <th>Wire Fee</th>
+ <th>Closing Fee</th>
+ </tr>
+ </thead>
+ <tbody>
+ {props.rci.wireFees.feesForType[props.s].map((f) => (
+ <tr key={f.sig}>
+ <td>{time.stringifyTimestamp(f.endStamp)}</td>
+ <td>{renderAmount(f.wireFee)}</td>
+ <td>{renderAmount(f.closingFee)}</td>
+ </tr>
+ ))}
+ </tbody>
+ </>
+ );
+}
+
+function AuditorDetailsView(props: {
+ rci: walletTypes.ExchangeWithdrawDetails | null;
+}): JSX.Element {
+ const rci = props.rci;
+ console.log("rci", rci);
+ if (!rci) {
+ return (
+ <p>
+ Details will be displayed when a valid exchange provider URL is entered.
+ </p>
+ );
+ }
+ if ((rci.exchangeInfo.details?.auditors ?? []).length === 0) {
+ return <p>The exchange is not audited by any auditors.</p>;
+ }
+ return (
+ <div>
+ {(rci.exchangeInfo.details?.auditors ?? []).map((a) => (
+ <div key={a.auditor_pub}>
+ <h3>Auditor {a.auditor_url}</h3>
+ <p>
+ Public key: <ExpanderText text={a.auditor_pub} />
+ </p>
+ <p>
+ Trusted:{" "}
+ {rci.trustedAuditorPubs.indexOf(a.auditor_pub) >= 0 ? "yes" : "no"}
+ </p>
+ <p>
+ Audits {a.denomination_keys.length} of {rci.numOfferedDenoms}{" "}
+ denominations
+ </p>
+ </div>
+ ))}
+ </div>
+ );
+}
+
+function FeeDetailsView(props: {
+ rci: walletTypes.ExchangeWithdrawDetails | null;
+}): JSX.Element {
+ const rci = props.rci;
+ if (!rci) {
+ return (
+ <p>
+ Details will be displayed when a valid exchange provider URL is entered.
+ </p>
+ );
+ }
+
+ const denoms = rci.selectedDenoms;
+ const withdrawFee = renderAmount(rci.withdrawFee);
+ const overhead = renderAmount(rci.overhead);
+
+ return (
+ <div>
+ <h3>Overview</h3>
+ <p>
+ Public key:{" "}
+ <ExpanderText
+ text={rci.exchangeInfo.details?.masterPublicKey ?? "??"}
+ />
+ </p>
+ <p>
+ {i18n.str`Withdrawal fees:`} {withdrawFee}
+ </p>
+ <p>
+ {i18n.str`Rounding loss:`} {overhead}
+ </p>
+ <p>{i18n.str`Earliest expiration (for deposit): ${time.stringifyTimestamp(
+ rci.earliestDepositExpiration,
+ )}`}</p>
+ <h3>Coin Fees</h3>
+ <div style={{ overflow: "auto" }}>
+ <table className="pure-table">
+ <thead>
+ <tr>
+ <th>{i18n.str`# Coins`}</th>
+ <th>{i18n.str`Value`}</th>
+ <th>{i18n.str`Withdraw Fee`}</th>
+ <th>{i18n.str`Refresh Fee`}</th>
+ <th>{i18n.str`Deposit Fee`}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {denoms.selectedDenoms.map((ds) => {
+ return (
+ <tr key={ds.denom.denomPub}>
+ <td>{ds.count + "x"}</td>
+ <td>{renderAmount(ds.denom.value)}</td>
+ <td>{renderAmount(ds.denom.feeWithdraw)}</td>
+ <td>{renderAmount(ds.denom.feeRefresh)}</td>
+ <td>{renderAmount(ds.denom.feeDeposit)}</td>
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ </div>
+ <h3>Wire Fees</h3>
+ <div style={{ overflow: "auto" }}>
+ <table className="pure-table">
+ {Object.keys(rci.wireFees.feesForType).map((s) => (
+ <WireFee key={s} s={s} rci={rci} />
+ ))}
+ </table>
+ </div>
+ </div>
+ );
+}
+
+/**
+ * Shows details about a withdraw request.
+ */
+export function WithdrawDetailView(props: {
+ rci: walletTypes.ExchangeWithdrawDetails | null;
+}): JSX.Element {
+ const rci = props.rci;
+ return (
+ <div>
+ <Collapsible initiallyCollapsed={true} title="Fee and Spending Details">
+ <FeeDetailsView rci={rci} />
+ </Collapsible>
+ <Collapsible initiallyCollapsed={true} title="Auditor Details">
+ <AuditorDetailsView rci={rci} />
+ </Collapsible>
+ </div>
+ );
+}
+
+interface ExpanderTextProps {
+ text: string;
+}
+
+/**
+ * Show a heading with a toggle to show/hide the expandable content.
+ */
+export function ExpanderText({ text }: ExpanderTextProps): JSX.Element {
+ return <span>{text}</span>;
+}
+
+export interface LoadingButtonProps {
+ loading: boolean;
+}
+
+export function ProgressButton(
+ props: React.PropsWithChildren<LoadingButtonProps> &
+ React.DetailedHTMLProps<
+ React.ButtonHTMLAttributes<HTMLButtonElement>,
+ HTMLButtonElement
+ >,
+): JSX.Element {
+ return (
+ <button
+ className="pure-button pure-button-primary"
+ type="button"
+ {...props}
+ >
+ {props.loading ? (
+ <span>
+ <object
+ className="svg-icon svg-baseline"
+ data="/img/spinner-bars.svg"
+ />
+ </span>
+ ) : null}{" "}
+ {props.children}
+ </button>
+ );
+}
+
+export function PageLink(
+ props: React.PropsWithChildren<{ pageName: string }>,
+): JSX.Element {
+ const url = chrome.extension.getURL(`/${props.pageName}`);
+ return (
+ <a
+ className="actionLink"
+ href={url}
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ {props.children}
+ </a>
+ );
+}
diff --git a/packages/taler-wallet-webextension/src/wxApi.ts b/packages/taler-wallet-webextension/src/wxApi.ts
new file mode 100644
index 000000000..ee86d90e5
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wxApi.ts
@@ -0,0 +1,239 @@
+/*
+ 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/>
+ */
+
+/**
+ * Interface to the wallet through WebExtension messaging.
+ */
+
+/**
+ * Imports.
+ */
+import { AmountJson, walletTypes } from "taler-wallet-core";
+
+
+export interface ExtendedPermissionsResponse {
+ newValue: boolean;
+}
+
+
+/**
+ * Response with information about available version upgrades.
+ */
+export interface UpgradeResponse {
+ /**
+ * Is a reset required because of a new DB version
+ * that can't be atomatically upgraded?
+ */
+ dbResetRequired: boolean;
+
+ /**
+ * Current database version.
+ */
+ currentDbVersion: string;
+
+ /**
+ * Old db version (if applicable).
+ */
+ oldDbVersion: string;
+}
+
+/**
+ * Error thrown when the function from the backend (via RPC) threw an error.
+ */
+export class WalletApiError extends Error {
+ constructor(message: string, public detail: any) {
+ super(message);
+ // restore prototype chain
+ Object.setPrototypeOf(this, new.target.prototype);
+ }
+}
+
+async function callBackend(
+ type: string,
+ detail: any,
+): Promise<any> {
+ return new Promise<any>((resolve, reject) => {
+ chrome.runtime.sendMessage({ type, detail }, (resp) => {
+ if (chrome.runtime.lastError) {
+ console.log("Error calling backend");
+ reject(
+ new Error(
+ `Error contacting backend: chrome.runtime.lastError.message`,
+ ),
+ );
+ }
+ if (typeof resp === "object" && resp && resp.error) {
+ console.warn("response error:", resp);
+ const e = new WalletApiError(resp.error.message, resp.error);
+ reject(e);
+ } else {
+ resolve(resp);
+ }
+ });
+ });
+}
+
+
+
+/**
+ * Start refreshing a coin.
+ */
+export function refresh(coinPub: string): Promise<void> {
+ return callBackend("refresh-coin", { coinPub });
+}
+
+/**
+ * Pay for a proposal.
+ */
+export function confirmPay(
+ proposalId: string,
+ sessionId: string | undefined,
+): Promise<walletTypes.ConfirmPayResult> {
+ return callBackend("confirm-pay", { proposalId, sessionId });
+}
+
+/**
+ * Check upgrade information
+ */
+export function checkUpgrade(): Promise<UpgradeResponse> {
+ return callBackend("check-upgrade", {});
+}
+
+/**
+ * Reset database
+ */
+export function resetDb(): Promise<void> {
+ return callBackend("reset-db", {});
+}
+
+/**
+ * Get balances for all currencies/exchanges.
+ */
+export function getBalance(): Promise<walletTypes.BalancesResponse> {
+ return callBackend("balances", {});
+}
+
+/**
+ * Return coins to a bank account.
+ */
+export function returnCoins(args: {
+ amount: AmountJson;
+ exchange: string;
+ senderWire: string;
+}): Promise<void> {
+ return callBackend("return-coins", args);
+}
+
+/**
+ * Look up a purchase in the wallet database from
+ * the contract terms hash.
+ */
+export function getPurchaseDetails(
+ proposalId: string,
+): Promise<walletTypes.PurchaseDetails> {
+ return callBackend("get-purchase-details", { proposalId });
+}
+
+/**
+ * Get the status of processing a tip.
+ */
+export function getTipStatus(talerTipUri: string): Promise<walletTypes.TipStatus> {
+ return callBackend("get-tip-status", { talerTipUri });
+}
+
+/**
+ * Mark a tip as accepted by the user.
+ */
+export function acceptTip(talerTipUri: string): Promise<void> {
+ return callBackend("accept-tip", { talerTipUri });
+}
+
+/**
+ * Download a refund and accept it.
+ */
+export function applyRefund(
+ refundUrl: string,
+): Promise<{ contractTermsHash: string; proposalId: string }> {
+ return callBackend("accept-refund", { refundUrl });
+}
+
+/**
+ * Abort a failed payment and try to get a refund.
+ */
+export function abortFailedPayment(contractTermsHash: string): Promise<void> {
+ return callBackend("abort-failed-payment", { contractTermsHash });
+}
+
+/**
+ * Abort a failed payment and try to get a refund.
+ */
+export function benchmarkCrypto(repetitions: number): Promise<walletTypes.BenchmarkResult> {
+ return callBackend("benchmark-crypto", { repetitions });
+}
+
+/**
+ * Get details about a pay operation.
+ */
+export function preparePay(talerPayUri: string): Promise<walletTypes.PreparePayResult> {
+ return callBackend("prepare-pay", { talerPayUri });
+}
+
+/**
+ * Get details about a withdraw operation.
+ */
+export function acceptWithdrawal(
+ talerWithdrawUri: string,
+ selectedExchange: string,
+): Promise<walletTypes.AcceptWithdrawalResponse> {
+ return callBackend("accept-withdrawal", {
+ talerWithdrawUri,
+ selectedExchange,
+ });
+}
+
+/**
+ * Get diagnostics information
+ */
+export function getDiagnostics(): Promise<walletTypes.WalletDiagnostics> {
+ return callBackend("get-diagnostics", {});
+}
+
+/**
+ * Get diagnostics information
+ */
+export function setExtendedPermissions(
+ value: boolean,
+): Promise<ExtendedPermissionsResponse> {
+ return callBackend("set-extended-permissions", { value });
+}
+
+/**
+ * Get diagnostics information
+ */
+export function getExtendedPermissions(): Promise<ExtendedPermissionsResponse> {
+ return callBackend("get-extended-permissions", {});
+}
+
+export function onUpdateNotification(f: () => void): () => void {
+ const port = chrome.runtime.connect({ name: "notifications" });
+ const listener = (): void => {
+ f();
+ };
+ port.onMessage.addListener(listener);
+ return () => {
+ port.onMessage.removeListener(listener);
+ };
+}
diff --git a/packages/taler-wallet-webextension/src/wxBackend.ts b/packages/taler-wallet-webextension/src/wxBackend.ts
new file mode 100644
index 000000000..3adc9a82d
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/wxBackend.ts
@@ -0,0 +1,566 @@
+/*
+ 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/>
+ */
+
+/**
+ * Messaging for the WebExtensions wallet. Should contain
+ * parts that are specific for WebExtensions, but as little business
+ * logic as possible.
+ */
+
+/**
+ * Imports.
+ */
+import { isFirefox, getPermissionsApi } from "./compat";
+import * as wxApi from "./wxApi";
+import MessageSender = chrome.runtime.MessageSender;
+import { extendedPermissions } from "./permissions";
+
+import { Wallet, promiseUtil, db, walletTypes, taleruri, queryLib } from "taler-wallet-core";
+import { BrowserHttpLib } from "./browserHttpLib";
+import { BrowserCryptoWorkerFactory } from "./browserCryptoWorkerFactory";
+
+const NeedsWallet = Symbol("NeedsWallet");
+
+/**
+ * Currently active wallet instance. Might be unloaded and
+ * re-instantiated when the database is reset.
+ */
+let currentWallet: Wallet | undefined;
+
+let currentDatabase: IDBDatabase | undefined;
+
+/**
+ * Last version if an outdated DB, if applicable.
+ */
+let outdatedDbVersion: number | undefined;
+
+const walletInit: promiseUtil.OpenedPromise<void> = promiseUtil.openPromise<void>();
+
+const notificationPorts: chrome.runtime.Port[] = [];
+
+async function handleMessage(
+ sender: MessageSender,
+ type: string,
+ detail: any,
+): Promise<any> {
+ function needsWallet(): Wallet {
+ if (!currentWallet) {
+ throw NeedsWallet;
+ }
+ return currentWallet;
+ }
+ switch (type) {
+ case "balances": {
+ return needsWallet().getBalances();
+ }
+ case "dump-db": {
+ const db = needsWallet().db;
+ return db.exportDatabase();
+ }
+ case "import-db": {
+ const db = needsWallet().db;
+ return db.importDatabase(detail.dump);
+ }
+ case "ping": {
+ return Promise.resolve();
+ }
+ case "reset-db": {
+ db.deleteTalerDatabase(indexedDB);
+ setBadgeText({ text: "" });
+ console.log("reset done");
+ if (!currentWallet) {
+ reinitWallet();
+ }
+ return Promise.resolve({});
+ }
+ case "confirm-pay": {
+ if (typeof detail.proposalId !== "string") {
+ throw Error("proposalId must be string");
+ }
+ return needsWallet().confirmPay(detail.proposalId, detail.sessionId);
+ }
+ case "exchange-info": {
+ if (!detail.baseUrl) {
+ return Promise.resolve({ error: "bad url" });
+ }
+ return needsWallet().updateExchangeFromUrl(detail.baseUrl);
+ }
+ case "get-exchanges": {
+ return needsWallet().getExchangeRecords();
+ }
+ case "get-currencies": {
+ return needsWallet().getCurrencies();
+ }
+ case "update-currency": {
+ return needsWallet().updateCurrency(detail.currencyRecord);
+ }
+ case "get-reserves": {
+ if (typeof detail.exchangeBaseUrl !== "string") {
+ return Promise.reject(Error("exchangeBaseUrl missing"));
+ }
+ return needsWallet().getReserves(detail.exchangeBaseUrl);
+ }
+ case "get-coins": {
+ if (typeof detail.exchangeBaseUrl !== "string") {
+ return Promise.reject(Error("exchangBaseUrl missing"));
+ }
+ return needsWallet().getCoinsForExchange(detail.exchangeBaseUrl);
+ }
+ case "get-denoms": {
+ if (typeof detail.exchangeBaseUrl !== "string") {
+ return Promise.reject(Error("exchangBaseUrl missing"));
+ }
+ return needsWallet().getDenoms(detail.exchangeBaseUrl);
+ }
+ case "refresh-coin": {
+ if (typeof detail.coinPub !== "string") {
+ return Promise.reject(Error("coinPub missing"));
+ }
+ return needsWallet().refresh(detail.coinPub);
+ }
+ case "get-sender-wire-infos": {
+ return needsWallet().getSenderWireInfos();
+ }
+ case "return-coins": {
+ const d = {
+ amount: detail.amount,
+ exchange: detail.exchange,
+ senderWire: detail.senderWire,
+ };
+ return needsWallet().returnCoins(d);
+ }
+ case "check-upgrade": {
+ let dbResetRequired = false;
+ if (!currentWallet) {
+ dbResetRequired = true;
+ }
+ const resp: wxApi.UpgradeResponse = {
+ currentDbVersion: db.WALLET_DB_MINOR_VERSION.toString(),
+ dbResetRequired,
+ oldDbVersion: (outdatedDbVersion || "unknown").toString(),
+ };
+ return resp;
+ }
+ case "get-purchase-details": {
+ const proposalId = detail.proposalId;
+ if (!proposalId) {
+ throw Error("proposalId missing");
+ }
+ if (typeof proposalId !== "string") {
+ throw Error("proposalId must be a string");
+ }
+ return needsWallet().getPurchaseDetails(proposalId);
+ }
+ case "accept-refund":
+ return needsWallet().applyRefund(detail.refundUrl);
+ case "get-tip-status": {
+ return needsWallet().getTipStatus(detail.talerTipUri);
+ }
+ case "accept-tip": {
+ return needsWallet().acceptTip(detail.talerTipUri);
+ }
+ case "abort-failed-payment": {
+ if (!detail.contractTermsHash) {
+ throw Error("contracTermsHash not given");
+ }
+ return needsWallet().abortFailedPayment(detail.contractTermsHash);
+ }
+ case "benchmark-crypto": {
+ if (!detail.repetitions) {
+ throw Error("repetitions not given");
+ }
+ return needsWallet().benchmarkCrypto(detail.repetitions);
+ }
+ case "accept-withdrawal": {
+ return needsWallet().acceptWithdrawal(
+ detail.talerWithdrawUri,
+ detail.selectedExchange,
+ );
+ }
+ case "get-diagnostics": {
+ const manifestData = chrome.runtime.getManifest();
+ const errors: string[] = [];
+ let firefoxIdbProblem = false;
+ let dbOutdated = false;
+ try {
+ await walletInit.promise;
+ } catch (e) {
+ errors.push("Error during wallet initialization: " + e);
+ if (
+ currentDatabase === undefined &&
+ outdatedDbVersion === undefined &&
+ isFirefox()
+ ) {
+ firefoxIdbProblem = true;
+ }
+ }
+ if (!currentWallet) {
+ errors.push("Could not create wallet backend.");
+ }
+ if (!currentDatabase) {
+ errors.push("Could not open database");
+ }
+ if (outdatedDbVersion !== undefined) {
+ errors.push(`Outdated DB version: ${outdatedDbVersion}`);
+ dbOutdated = true;
+ }
+ const diagnostics: walletTypes.WalletDiagnostics = {
+ walletManifestDisplayVersion:
+ manifestData.version_name || "(undefined)",
+ walletManifestVersion: manifestData.version,
+ errors,
+ firefoxIdbProblem,
+ dbOutdated,
+ };
+ return diagnostics;
+ }
+ case "prepare-pay":
+ return needsWallet().preparePayForUri(detail.talerPayUri);
+ case "set-extended-permissions": {
+ const newVal = detail.value;
+ console.log("new extended permissions value", newVal);
+ if (newVal) {
+ setupHeaderListener();
+ return { newValue: true };
+ } else {
+ await new Promise((resolve, reject) => {
+ getPermissionsApi().remove(extendedPermissions, (rem) => {
+ console.log("permissions removed:", rem);
+ resolve();
+ });
+ });
+ return { newVal: false };
+ }
+ }
+ case "get-extended-permissions": {
+ const res = await new Promise((resolve, reject) => {
+ getPermissionsApi().contains(extendedPermissions, (result: boolean) => {
+ resolve(result);
+ });
+ });
+ return { newValue: res };
+ }
+ default:
+ console.error(`Request type ${type} unknown`);
+ console.error(`Request detail was ${detail}`);
+ return {
+ error: {
+ message: `request type ${type} unknown`,
+ requestType: type,
+ },
+ };
+ }
+}
+
+async function dispatch(
+ req: any,
+ sender: any,
+ sendResponse: any,
+): Promise<void> {
+ try {
+ const p = handleMessage(sender, req.type, req.detail);
+ const r = await p;
+ try {
+ sendResponse(r);
+ } catch (e) {
+ // might fail if tab disconnected
+ }
+ } catch (e) {
+ console.log(`exception during wallet handler for '${req.type}'`);
+ console.log("request", req);
+ console.error(e);
+ let stack;
+ try {
+ stack = e.stack.toString();
+ } catch (e) {
+ // might fail
+ }
+ try {
+ sendResponse({
+ error: {
+ message: e.message,
+ stack,
+ },
+ });
+ } catch (e) {
+ console.log(e);
+ // might fail if tab disconnected
+ }
+ }
+}
+
+function getTab(tabId: number): Promise<chrome.tabs.Tab> {
+ return new Promise((resolve, reject) => {
+ chrome.tabs.get(tabId, (tab: chrome.tabs.Tab) => resolve(tab));
+ });
+}
+
+function setBadgeText(options: chrome.browserAction.BadgeTextDetails): void {
+ // not supported by all browsers ...
+ if (chrome && chrome.browserAction && chrome.browserAction.setBadgeText) {
+ chrome.browserAction.setBadgeText(options);
+ } else {
+ console.warn("can't set badge text, not supported", options);
+ }
+}
+
+function waitMs(timeoutMs: number): Promise<void> {
+ return new Promise((resolve, reject) => {
+ const bgPage = chrome.extension.getBackgroundPage();
+ if (!bgPage) {
+ reject("fatal: no background page");
+ return;
+ }
+ bgPage.setTimeout(() => resolve(), timeoutMs);
+ });
+}
+
+function makeSyncWalletRedirect(
+ url: string,
+ tabId: number,
+ oldUrl: string,
+ params?: { [name: string]: string | undefined },
+): Record<string, unknown> {
+ const innerUrl = new URL(chrome.extension.getURL("/" + url));
+ if (params) {
+ for (const key in params) {
+ const p = params[key];
+ if (p) {
+ innerUrl.searchParams.set(key, p);
+ }
+ }
+ }
+ if (isFirefox()) {
+ // Some platforms don't support the sync redirect (yet), so fall back to
+ // async redirect after a timeout.
+ const doit = async (): Promise<void> => {
+ await waitMs(150);
+ const tab = await getTab(tabId);
+ if (tab.url === oldUrl) {
+ chrome.tabs.update(tabId, { url: innerUrl.href });
+ }
+ };
+ doit();
+ }
+ console.log("redirecting to", innerUrl.href);
+ chrome.tabs.update(tabId, { url: innerUrl.href });
+ return { redirectUrl: innerUrl.href };
+}
+
+async function reinitWallet(): Promise<void> {
+ if (currentWallet) {
+ currentWallet.stop();
+ currentWallet = undefined;
+ }
+ currentDatabase = undefined;
+ setBadgeText({ text: "" });
+ try {
+ currentDatabase = await db.openTalerDatabase(indexedDB, reinitWallet);
+ } catch (e) {
+ console.error("could not open database", e);
+ walletInit.reject(e);
+ return;
+ }
+ const http = new BrowserHttpLib();
+ console.log("setting wallet");
+ const wallet = new Wallet(
+ new queryLib.Database(currentDatabase),
+ http,
+ new BrowserCryptoWorkerFactory(),
+ );
+ wallet.addNotificationListener((x) => {
+ for (const x of notificationPorts) {
+ try {
+ x.postMessage({ type: "notification" });
+ } catch (e) {
+ console.error(e);
+ }
+ }
+ });
+ wallet.runRetryLoop().catch((e) => {
+ console.log("error during wallet retry loop", e);
+ });
+ // Useful for debugging in the background page.
+ (window as any).talerWallet = wallet;
+ currentWallet = wallet;
+ walletInit.resolve();
+}
+
+try {
+ // This needs to be outside of main, as Firefox won't fire the event if
+ // the listener isn't created synchronously on loading the backend.
+ chrome.runtime.onInstalled.addListener((details) => {
+ console.log("onInstalled with reason", details.reason);
+ if (details.reason === "install") {
+ const url = chrome.extension.getURL("/welcome.html");
+ chrome.tabs.create({ active: true, url: url });
+ }
+ });
+} catch (e) {
+ console.error(e);
+}
+
+function headerListener(
+ details: chrome.webRequest.WebResponseHeadersDetails,
+): chrome.webRequest.BlockingResponse | undefined {
+ console.log("header listener");
+ if (chrome.runtime.lastError) {
+ console.error(chrome.runtime.lastError);
+ return;
+ }
+ const wallet = currentWallet;
+ if (!wallet) {
+ console.warn("wallet not available while handling header");
+ return;
+ }
+ console.log("in header listener");
+ if (details.statusCode === 402 || details.statusCode === 202) {
+ console.log(`got 402/202 from ${details.url}`);
+ for (const header of details.responseHeaders || []) {
+ if (header.name.toLowerCase() === "taler") {
+ const talerUri = header.value || "";
+ const uriType = taleruri.classifyTalerUri(talerUri);
+ switch (uriType) {
+ case taleruri.TalerUriType.TalerWithdraw:
+ return makeSyncWalletRedirect(
+ "withdraw.html",
+ details.tabId,
+ details.url,
+ {
+ talerWithdrawUri: talerUri,
+ },
+ );
+ case taleruri.TalerUriType.TalerPay:
+ return makeSyncWalletRedirect(
+ "pay.html",
+ details.tabId,
+ details.url,
+ {
+ talerPayUri: talerUri,
+ },
+ );
+ case taleruri.TalerUriType.TalerTip:
+ return makeSyncWalletRedirect(
+ "tip.html",
+ details.tabId,
+ details.url,
+ {
+ talerTipUri: talerUri,
+ },
+ );
+ case taleruri.TalerUriType.TalerRefund:
+ return makeSyncWalletRedirect(
+ "refund.html",
+ details.tabId,
+ details.url,
+ {
+ talerRefundUri: talerUri,
+ },
+ );
+ case taleruri.TalerUriType.TalerNotifyReserve:
+ Promise.resolve().then(() => {
+ const w = currentWallet;
+ if (!w) {
+ return;
+ }
+ w.handleNotifyReserve();
+ });
+ break;
+ default:
+ console.warn(
+ "Response with HTTP 402 has Taler header, but header value is not a taler:// URI.",
+ );
+ break;
+ }
+ }
+ }
+ }
+ return;
+}
+
+function setupHeaderListener(): void {
+ console.log("setting up header listener");
+ // Handlers for catching HTTP requests
+ getPermissionsApi().contains(extendedPermissions, (result: boolean) => {
+ if (
+ chrome.webRequest.onHeadersReceived &&
+ chrome.webRequest.onHeadersReceived.hasListener(headerListener)
+ ) {
+ chrome.webRequest.onHeadersReceived.removeListener(headerListener);
+ }
+ if (result) {
+ console.log("actually adding listener");
+ chrome.webRequest.onHeadersReceived.addListener(
+ headerListener,
+ { urls: ["<all_urls>"] },
+ ["responseHeaders", "blocking"],
+ );
+ }
+ chrome.webRequest.handlerBehaviorChanged(() => {
+ if (chrome.runtime.lastError) {
+ console.error(chrome.runtime.lastError);
+ }
+ });
+ });
+}
+
+/**
+ * Main function to run for the WebExtension backend.
+ *
+ * Sets up all event handlers and other machinery.
+ */
+export async function wxMain(): Promise<void> {
+ // Explicitly unload the extension page as soon as an update is available,
+ // so the update gets installed as soon as possible.
+ chrome.runtime.onUpdateAvailable.addListener((details) => {
+ console.log("update available:", details);
+ chrome.runtime.reload();
+ });
+ reinitWallet();
+
+ // Handlers for messages coming directly from the content
+ // script on the page
+ chrome.runtime.onMessage.addListener((req, sender, sendResponse) => {
+ dispatch(req, sender, sendResponse);
+ return true;
+ });
+
+ chrome.runtime.onConnect.addListener((port) => {
+ notificationPorts.push(port);
+ port.onDisconnect.addListener((discoPort) => {
+ const idx = notificationPorts.indexOf(discoPort);
+ if (idx >= 0) {
+ notificationPorts.splice(idx, 1);
+ }
+ });
+ });
+
+ try {
+ setupHeaderListener();
+ } catch (e) {
+ console.log(e);
+ }
+
+ // On platforms that support it, also listen to external
+ // modification of permissions.
+ getPermissionsApi().addPermissionsListener((perm) => {
+ if (chrome.runtime.lastError) {
+ console.error(chrome.runtime.lastError);
+ return;
+ }
+ setupHeaderListener();
+ });
+}
diff --git a/packages/taler-wallet-webextension/tsconfig.json b/packages/taler-wallet-webextension/tsconfig.json
new file mode 100644
index 000000000..c3c4144bf
--- /dev/null
+++ b/packages/taler-wallet-webextension/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "lib": ["es6", "DOM"],
+ "jsx": "react",
+ "reactNamespace": "React",
+ "module": "commonjs",
+ "target": "es5",
+ "noImplicitAny": true,
+ "outDir": "lib",
+ "declaration": true,
+ "noEmitOnError": true,
+ "strict": true,
+ "incremental": true,
+ "sourceMap": true,
+ "esModuleInterop": true
+ },
+ "include": ["src/**/*"]
+}
diff --git a/packages/taler-wallet-webextension/webextension/manifest.json b/packages/taler-wallet-webextension/webextension/manifest.json
new file mode 100644
index 000000000..b09e3ecbd
--- /dev/null
+++ b/packages/taler-wallet-webextension/webextension/manifest.json
@@ -0,0 +1,49 @@
+{
+ "manifest_version": 2,
+
+ "name": "GNU Taler Wallet (git)",
+ "description": "Privacy preserving and transparent payments",
+ "author": "GNU Taler Developers",
+ "version": "0.6.77.4",
+ "version_name": "0.7.1-dev.3",
+
+ "minimum_chrome_version": "51",
+ "minimum_opera_version": "36",
+
+ "applications": {
+ "gecko": {
+ "id": "wallet@taler.net",
+ "strict_min_version": "68.0"
+ }
+ },
+
+ "icons": {
+ "32": "img/icon.png",
+ "128": "img/logo.png"
+ },
+
+ "permissions": [
+ "storage",
+ "activeTab"
+ ],
+
+ "optional_permissions": [
+ "webRequest",
+ "webRequestBlocking",
+ "http://*/*",
+ "https://*/*"
+ ],
+
+ "browser_action": {
+ "default_icon": {
+ "32": "img/icon.png"
+ },
+ "default_title": "Taler",
+ "default_popup": "popup.html"
+ },
+
+ "background": {
+ "page": "background.html",
+ "persistent": true
+ }
+}
diff --git a/packages/taler-wallet-webextension/webextension/pack.sh b/packages/taler-wallet-webextension/webextension/pack.sh
new file mode 100755
index 000000000..ef005014f
--- /dev/null
+++ b/packages/taler-wallet-webextension/webextension/pack.sh
@@ -0,0 +1,23 @@
+#!/usr/bin/env bash
+
+set -eu
+
+if [[ ! -e package.json ]]; then
+ echo "Please run this from the root of the repo.">&2
+ exit 1
+fi
+
+vers_manifest=$(jq -r '.version' webextension/manifest.json)
+
+rm -rf dist/wx
+mkdir -p dist/wx
+cp webextension/manifest.json dist/wx/
+cp -r webextension/static/* dist/wx/
+cp -r dist/webextension/* dist/wx/
+
+cd dist/wx
+
+zipfile="../taler-wallet-${vers_manifest}.zip"
+
+rm -f -- "$zipfile"
+zip -r "$zipfile" ./*
diff --git a/packages/taler-wallet-webextension/webextension/static/add-auditor.html b/packages/taler-wallet-webextension/webextension/static/add-auditor.html
new file mode 100644
index 000000000..47a97c075
--- /dev/null
+++ b/packages/taler-wallet-webextension/webextension/static/add-auditor.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="UTF-8" />
+
+ <title>Taler Wallet: Add Auditor</title>
+
+ <link rel="stylesheet" type="text/css" href="/style/wallet.css" />
+
+ <link rel="icon" href="/img/icon.png" />
+
+ <script src="/pageEntryPoint.js"></script>
+
+ <style>
+ .tree-item {
+ margin: 2em;
+ border-radius: 5px;
+ border: 1px solid gray;
+ padding: 1em;
+ }
+ .button-linky {
+ background: none;
+ color: black;
+ text-decoration: underline;
+ border: none;
+ }
+ </style>
+ </head>
+
+ <body>
+ <div id="container"></div>
+ </body>
+</html>
diff --git a/packages/taler-wallet-webextension/webextension/static/auditors.html b/packages/taler-wallet-webextension/webextension/static/auditors.html
new file mode 100644
index 000000000..15261290d
--- /dev/null
+++ b/packages/taler-wallet-webextension/webextension/static/auditors.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="UTF-8" />
+ <title>Taler Wallet: Auditors</title>
+
+ <link rel="stylesheet" type="text/css" href="/style/wallet.css" />
+
+ <link rel="icon" href="/img/icon.png" />
+
+ <script src="/dist/webextension/pageEntryPoint.js"></script>
+
+ <style>
+ body {
+ font-size: 100%;
+ }
+ .tree-item {
+ margin: 2em;
+ border-radius: 5px;
+ border: 1px solid gray;
+ padding: 1em;
+ }
+ .button-linky {
+ background: none;
+ color: black;
+ text-decoration: underline;
+ border: none;
+ }
+ </style>
+ </head>
+
+ <body>
+ <div id="container"></div>
+ </body>
+</html>
diff --git a/packages/taler-wallet-webextension/webextension/static/background.html b/packages/taler-wallet-webextension/webextension/static/background.html
new file mode 100644
index 000000000..b89c05588
--- /dev/null
+++ b/packages/taler-wallet-webextension/webextension/static/background.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <script src="/background.js"></script>
+ <title>(wallet bg page)</title>
+ </head>
+ <body>
+ <img id="taler-logo" src="/img/icon.png" />
+ </body>
+</html>
diff --git a/packages/taler-wallet-webextension/webextension/static/benchmark.html b/packages/taler-wallet-webextension/webextension/static/benchmark.html
new file mode 100644
index 000000000..a29fe0725
--- /dev/null
+++ b/packages/taler-wallet-webextension/webextension/static/benchmark.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="UTF-8" />
+ <title>Taler Wallet: Benchmarks</title>
+ <link rel="stylesheet" type="text/css" href="/style/wallet.css" />
+ <link rel="icon" href="/img/icon.png" />
+ <script src="/pageEntryPoint.js"></script>
+ </head>
+ <body>
+ <section id="main">
+ <h1>Benchmarks</h1>
+ <div id="container"></div>
+ </section>
+ </body>
+</html>
diff --git a/packages/taler-wallet-webextension/webextension/static/img/icon.png b/packages/taler-wallet-webextension/webextension/static/img/icon.png
new file mode 100644
index 000000000..b4733bebc
--- /dev/null
+++ b/packages/taler-wallet-webextension/webextension/static/img/icon.png
Binary files differ
diff --git a/packages/taler-wallet-webextension/webextension/static/img/logo-2015-medium.png b/packages/taler-wallet-webextension/webextension/static/img/logo-2015-medium.png
new file mode 100644
index 000000000..acf84baaf
--- /dev/null
+++ b/packages/taler-wallet-webextension/webextension/static/img/logo-2015-medium.png
Binary files differ
diff --git a/packages/taler-wallet-webextension/webextension/static/img/logo.png b/packages/taler-wallet-webextension/webextension/static/img/logo.png
new file mode 120000
index 000000000..1ddb87d2c
--- /dev/null
+++ b/packages/taler-wallet-webextension/webextension/static/img/logo.png
@@ -0,0 +1 @@
+logo-2015-medium.png \ No newline at end of file
diff --git a/packages/taler-wallet-webextension/webextension/static/img/spinner-bars.svg b/packages/taler-wallet-webextension/webextension/static/img/spinner-bars.svg
new file mode 100644
index 000000000..f6f7dfcb3
--- /dev/null
+++ b/packages/taler-wallet-webextension/webextension/static/img/spinner-bars.svg
@@ -0,0 +1,53 @@
+<!-- By Sam Herbert (@sherb), for everyone. More @ http://goo.gl/7AJzbL -->
+<svg width="135" height="140" viewBox="0 0 135 140" xmlns="http://www.w3.org/2000/svg" fill="#fff">
+ <rect y="10" width="15" height="120" rx="6">
+ <animate attributeName="height"
+ begin="0.5s" dur="1s"
+ values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
+ repeatCount="indefinite" />
+ <animate attributeName="y"
+ begin="0.5s" dur="1s"
+ values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
+ repeatCount="indefinite" />
+ </rect>
+ <rect x="30" y="10" width="15" height="120" rx="6">
+ <animate attributeName="height"
+ begin="0.25s" dur="1s"
+ values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
+ repeatCount="indefinite" />
+ <animate attributeName="y"
+ begin="0.25s" dur="1s"
+ values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
+ repeatCount="indefinite" />
+ </rect>
+ <rect x="60" width="15" height="140" rx="6">
+ <animate attributeName="height"
+ begin="0s" dur="1s"
+ values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
+ repeatCount="indefinite" />
+ <animate attributeName="y"
+ begin="0s" dur="1s"
+ values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
+ repeatCount="indefinite" />
+ </rect>
+ <rect x="90" y="10" width="15" height="120" rx="6">
+ <animate attributeName="height"
+ begin="0.25s" dur="1s"
+ values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
+ repeatCount="indefinite" />
+ <animate attributeName="y"
+ begin="0.25s" dur="1s"
+ values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
+ repeatCount="indefinite" />
+ </rect>
+ <rect x="120" y="10" width="15" height="120" rx="6">
+ <animate attributeName="height"
+ begin="0.5s" dur="1s"
+ values="120;110;100;90;80;70;60;50;40;140;120" calcMode="linear"
+ repeatCount="indefinite" />
+ <animate attributeName="y"
+ begin="0.5s" dur="1s"
+ values="10;15;20;25;30;35;40;45;50;0;10" calcMode="linear"
+ repeatCount="indefinite" />
+ </rect>
+</svg>
diff --git a/packages/taler-wallet-webextension/webextension/static/pay.html b/packages/taler-wallet-webextension/webextension/static/pay.html
new file mode 100644
index 000000000..452c56df0
--- /dev/null
+++ b/packages/taler-wallet-webextension/webextension/static/pay.html
@@ -0,0 +1,73 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="UTF-8" />
+ <title>Taler Wallet: Confirm Contract</title>
+
+ <link rel="stylesheet" type="text/css" href="/style/pure.css" />
+ <link rel="stylesheet" type="text/css" href="/style/wallet.css" />
+ <link rel="icon" href="/img/icon.png" />
+ <script src="/pageEntryPoint.js"></script>
+
+ <style>
+ button.accept {
+ background-color: #5757d2;
+ border: 1px solid black;
+ border-radius: 5px;
+ margin: 1em 0;
+ padding: 0.5em;
+ font-weight: bold;
+ color: white;
+ }
+ button.linky {
+ background: none !important;
+ border: none;
+ padding: 0 !important;
+
+ font-family: arial, sans-serif;
+ color: #069;
+ text-decoration: underline;
+ cursor: pointer;
+ }
+
+ input.url {
+ width: 25em;
+ }
+
+ button.accept:disabled {
+ background-color: #dedbe8;
+ border: 1px solid white;
+ border-radius: 5px;
+ margin: 1em 0;
+ padding: 0.5em;
+ font-weight: bold;
+ color: #2c2c2c;
+ }
+
+ .errorbox {
+ border: 1px solid;
+ display: inline-block;
+ margin: 1em;
+ padding: 1em;
+ font-weight: bold;
+ background: #ff8a8a;
+ }
+
+ .okaybox {
+ border: 1px solid;
+ display: inline-block;
+ margin: 1em;
+ padding: 1em;
+ font-weight: bold;
+ background: #00fa9a;
+ }
+ </style>
+ </head>
+
+ <body>
+ <section id="main">
+ <h1>GNU Taler Wallet</h1>
+ <article id="container" class="fade"></article>
+ </section>
+ </body>
+</html>
diff --git a/packages/taler-wallet-webextension/webextension/static/payback.html b/packages/taler-wallet-webextension/webextension/static/payback.html
new file mode 100644
index 000000000..7ca9dc974
--- /dev/null
+++ b/packages/taler-wallet-webextension/webextension/static/payback.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="UTF-8" />
+ <title>Taler Wallet: Payback</title>
+
+ <link rel="stylesheet" type="text/css" href="/style/pure.css" />
+ <link rel="stylesheet" type="text/css" href="/style/wallet.css" />
+ <link rel="icon" href="/img/icon.png" />
+ <script src="/pageEntryPoint.js"></script>
+
+ <style>
+ body {
+ font-size: 100%;
+ }
+ .tree-item {
+ margin: 2em;
+ border-radius: 5px;
+ border: 1px solid gray;
+ padding: 1em;
+ }
+ .button-linky {
+ background: none;
+ color: black;
+ text-decoration: underline;
+ border: none;
+ }
+ </style>
+ </head>
+
+ <body>
+ <div id="container"></div>
+ </body>
+</html>
diff --git a/packages/taler-wallet-webextension/webextension/static/popup.html b/packages/taler-wallet-webextension/webextension/static/popup.html
new file mode 100644
index 000000000..83f2f2861
--- /dev/null
+++ b/packages/taler-wallet-webextension/webextension/static/popup.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <link rel="stylesheet" type="text/css" href="/style/pure.css" />
+ <link rel="stylesheet" type="text/css" href="/style/wallet.css" />
+ <link rel="stylesheet" type="text/css" href="/style/popup.css" />
+ <link rel="icon" href="/img/icon.png" />
+ <script src="/pageEntryPoint.js"></script>
+ </head>
+
+ <body>
+ <div id="container" style="margin: 0; padding: 0;"></div>
+ </body>
+</html>
diff --git a/packages/taler-wallet-webextension/webextension/static/refund.html b/packages/taler-wallet-webextension/webextension/static/refund.html
new file mode 100644
index 000000000..3c1d78a24
--- /dev/null
+++ b/packages/taler-wallet-webextension/webextension/static/refund.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="UTF-8" />
+ <title>Taler Wallet: Refund Status</title>
+
+ <link rel="icon" href="/img/icon.png" />
+ <link rel="stylesheet" type="text/css" href="/style/pure.css" />
+ <link rel="stylesheet" type="text/css" href="/style/wallet.css" />
+ <script src="/pageEntryPoint.js"></script>
+ </head>
+
+ <body>
+ <section id="main">
+ <h1>GNU Taler Wallet</h1>
+ <article id="container" class="fade"></article>
+ </section>
+ </body>
+</html>
diff --git a/packages/taler-wallet-webextension/webextension/static/reset-required.html b/packages/taler-wallet-webextension/webextension/static/reset-required.html
new file mode 100644
index 000000000..84943fbf1
--- /dev/null
+++ b/packages/taler-wallet-webextension/webextension/static/reset-required.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="UTF-8" />
+ <title>Taler Wallet: Select Taler Provider</title>
+
+ <link rel="icon" href="/img/icon.png" />
+ <link rel="stylesheet" type="text/css" href="/style/pure.css" />
+ <link rel="stylesheet" type="text/css" href="/style/wallet.css" />
+ <script src="/pageEntryPoint.js"></script>
+
+ <style>
+ body {
+ font-size: 100%;
+ overflow-y: scroll;
+ }
+ </style>
+ </head>
+
+ <body>
+ <section id="main">
+ <div id="container"></div>
+ </section>
+ </body>
+</html>
diff --git a/packages/taler-wallet-webextension/webextension/static/return-coins.html b/packages/taler-wallet-webextension/webextension/static/return-coins.html
new file mode 100644
index 000000000..90703b447
--- /dev/null
+++ b/packages/taler-wallet-webextension/webextension/static/return-coins.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="UTF-8" />
+ <title>Taler Wallet: Return Coins to Bank Account</title>
+
+ <link rel="icon" href="/img/icon.png" />
+ <link rel="stylesheet" type="text/css" href="/style/pure.css" />
+ <link rel="stylesheet" type="text/css" href="/style/wallet.css" />
+ <script src="/pageEntryPoint.js"></script>
+ </head>
+
+ <body>
+ <div id="container"></div>
+ </body>
+</html>
diff --git a/packages/taler-wallet-webextension/webextension/static/style/popup.css b/packages/taler-wallet-webextension/webextension/static/style/popup.css
new file mode 100644
index 000000000..cca002399
--- /dev/null
+++ b/packages/taler-wallet-webextension/webextension/static/style/popup.css
@@ -0,0 +1,185 @@
+/**
+ * @author Gabor X. Toth
+ * @author Marcello Stanisci
+ * @author Florian Dold
+ */
+
+body {
+ min-height: 20em;
+ width: 30em;
+ margin: 0;
+ padding: 0;
+ max-height: 800px;
+ overflow: hidden;
+ background-color: #f8faf7;
+ font-family: Arial, Helvetica, sans-serif;
+}
+
+.nav {
+ background-color: #033;
+ padding: 0.5em 0;
+}
+
+.nav a {
+ color: #f8faf7;
+ padding: 0.7em 1.4em;
+ text-decoration: none;
+}
+
+.nav a.active {
+ background-color: #f8faf7;
+ color: #000;
+ font-weight: bold;
+}
+
+.container {
+ overflow-y: scroll;
+ max-height: 400px;
+}
+
+.abbrev {
+ text-decoration-style: dotted;
+}
+
+#content {
+ padding: 1em;
+}
+
+#wallet-table .amount {
+ text-align: right;
+}
+
+.hidden {
+ display: none;
+}
+
+#transactions-table th,
+#transactions-table td {
+ padding: 0.2em 0.5em;
+}
+
+#reserve-create table {
+ width: 100%;
+}
+
+#reserve-create table td.label {
+ width: 5em;
+}
+
+#reserve-create table .input input[type="text"] {
+ width: 100%;
+}
+
+.historyItem {
+ min-width: 380px;
+ display: flex;
+ flex-direction: row;
+ border-bottom: 1px solid #d9dbd8;
+ padding: 0.5em;
+ align-items: center;
+}
+
+.historyItem .amount {
+ font-size: 110%;
+ font-weight: bold;
+ text-align: right;
+}
+
+.historyDate,
+.historyTitle,
+.historyText,
+.historySmall {
+ margin: 0.3em;
+}
+
+.historyDate {
+ font-size: 90%;
+ color: slategray;
+ text-align: right;
+}
+
+.historyLeft {
+ display: flex;
+ flex-direction: column;
+ text-align: right;
+}
+
+.historyContent {
+ flex: 1;
+}
+
+.historyTitle {
+ font-weight: 400;
+}
+
+.historyText {
+ font-size: 90%;
+}
+
+.historySmall {
+ font-size: 70%;
+ text-transform: uppercase;
+}
+
+.historyAmount {
+ flex-grow: 1;
+}
+
+.historyAmount .primary {
+ font-size: 100%;
+}
+
+.historyAmount .secondary {
+ font-size: 80%;
+}
+
+.historyAmount .positive {
+ color: #088;
+}
+
+.historyAmount .positive:before {
+ content: "+";
+}
+
+.historyAmount .negative {
+ color: #800;
+}
+
+.historyAmount .negative:before {
+ color: #800;
+ content: "-";
+}
+.icon {
+ margin: 0 10px;
+ text-align: center;
+ width: 35px;
+ font-size: 20px;
+ border-radius: 50%;
+ background: #ccc;
+ color: #fff;
+ padding-top: 4px;
+ height: 30px;
+}
+
+.option {
+ text-transform: uppercase;
+ text-align: right;
+ padding: 0.4em;
+ font-size: 0.9em;
+}
+
+input[type="checkbox"],
+input[type="radio"] {
+ vertical-align: middle;
+ position: relative;
+ bottom: 1px;
+}
+
+input[type="radio"] {
+ bottom: 2px;
+}
+
+.balance {
+ text-align: center;
+ padding-top: 2em;
+}
diff --git a/packages/taler-wallet-webextension/webextension/static/style/pure.css b/packages/taler-wallet-webextension/webextension/static/style/pure.css
new file mode 100644
index 000000000..88a4bb7d7
--- /dev/null
+++ b/packages/taler-wallet-webextension/webextension/static/style/pure.css
@@ -0,0 +1,1513 @@
+/*!
+Pure v0.6.2
+Copyright 2013 Yahoo!
+Licensed under the BSD License.
+https://github.com/yahoo/pure/blob/master/LICENSE.md
+*/
+/*!
+normalize.css v^3.0 | MIT License | git.io/normalize
+Copyright (c) Nicolas Gallagher and Jonathan Neal
+*/
+/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */
+
+/**
+ * 1. Set default font family to sans-serif.
+ * 2. Prevent iOS and IE text size adjust after device orientation change,
+ * without disabling user zoom.
+ */
+
+html {
+ font-family: sans-serif; /* 1 */
+ -ms-text-size-adjust: 100%; /* 2 */
+ -webkit-text-size-adjust: 100%; /* 2 */
+}
+
+/**
+ * Remove default margin.
+ */
+
+body {
+ margin: 0;
+}
+
+/* HTML5 display definitions
+ ========================================================================== */
+
+/**
+ * Correct `block` display not defined for any HTML5 element in IE 8/9.
+ * Correct `block` display not defined for `details` or `summary` in IE 10/11
+ * and Firefox.
+ * Correct `block` display not defined for `main` in IE 11.
+ */
+
+article,
+aside,
+details,
+figcaption,
+figure,
+footer,
+header,
+hgroup,
+main,
+menu,
+nav,
+section,
+summary {
+ display: block;
+}
+
+/**
+ * 1. Correct `inline-block` display not defined in IE 8/9.
+ * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
+ */
+
+audio,
+canvas,
+progress,
+video {
+ display: inline-block; /* 1 */
+ vertical-align: baseline; /* 2 */
+}
+
+/**
+ * Prevent modern browsers from displaying `audio` without controls.
+ * Remove excess height in iOS 5 devices.
+ */
+
+audio:not([controls]) {
+ display: none;
+ height: 0;
+}
+
+/**
+ * Address `[hidden]` styling not present in IE 8/9/10.
+ * Hide the `template` element in IE 8/9/10/11, Safari, and Firefox < 22.
+ */
+
+[hidden],
+template {
+ display: none;
+}
+
+/* Links
+ ========================================================================== */
+
+/**
+ * Remove the gray background color from active links in IE 10.
+ */
+
+a {
+ background-color: transparent;
+}
+
+/**
+ * Improve readability of focused elements when they are also in an
+ * active/hover state.
+ */
+
+a:active,
+a:hover {
+ outline: 0;
+}
+
+/* Text-level semantics
+ ========================================================================== */
+
+/**
+ * Address styling not present in IE 8/9/10/11, Safari, and Chrome.
+ */
+
+abbr[title] {
+ border-bottom: 1px dotted;
+}
+
+/**
+ * Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
+ */
+
+b,
+strong {
+ font-weight: bold;
+}
+
+/**
+ * Address styling not present in Safari and Chrome.
+ */
+
+dfn {
+ font-style: italic;
+}
+
+/**
+ * Address variable `h1` font-size and margin within `section` and `article`
+ * contexts in Firefox 4+, Safari, and Chrome.
+ */
+
+h1 {
+ font-size: 2em;
+ margin: 0.67em 0;
+}
+
+/**
+ * Address styling not present in IE 8/9.
+ */
+
+mark {
+ background: #ff0;
+ color: #000;
+}
+
+/**
+ * Address inconsistent and variable font size in all browsers.
+ */
+
+small {
+ font-size: 80%;
+}
+
+/**
+ * Prevent `sub` and `sup` affecting `line-height` in all browsers.
+ */
+
+sub,
+sup {
+ font-size: 75%;
+ line-height: 0;
+ position: relative;
+ vertical-align: baseline;
+}
+
+sup {
+ top: -0.5em;
+}
+
+sub {
+ bottom: -0.25em;
+}
+
+/* Embedded content
+ ========================================================================== */
+
+/**
+ * Remove border when inside `a` element in IE 8/9/10.
+ */
+
+img {
+ border: 0;
+}
+
+/**
+ * Correct overflow not hidden in IE 9/10/11.
+ */
+
+svg:not(:root) {
+ overflow: hidden;
+}
+
+/* Grouping content
+ ========================================================================== */
+
+/**
+ * Address margin not present in IE 8/9 and Safari.
+ */
+
+figure {
+ margin: 1em 40px;
+}
+
+/**
+ * Address differences between Firefox and other browsers.
+ */
+
+hr {
+ box-sizing: content-box;
+ height: 0;
+}
+
+/**
+ * Contain overflow in all browsers.
+ */
+
+pre {
+ overflow: auto;
+}
+
+/**
+ * Address odd `em`-unit font size rendering in all browsers.
+ */
+
+code,
+kbd,
+pre,
+samp {
+ font-family: monospace, monospace;
+ font-size: 1em;
+}
+
+/* Forms
+ ========================================================================== */
+
+/**
+ * Known limitation: by default, Chrome and Safari on OS X allow very limited
+ * styling of `select`, unless a `border` property is set.
+ */
+
+/**
+ * 1. Correct color not being inherited.
+ * Known issue: affects color of disabled elements.
+ * 2. Correct font properties not being inherited.
+ * 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
+ */
+
+button,
+input,
+optgroup,
+select,
+textarea {
+ color: inherit; /* 1 */
+ font: inherit; /* 2 */
+ margin: 0; /* 3 */
+}
+
+/**
+ * Address `overflow` set to `hidden` in IE 8/9/10/11.
+ */
+
+button {
+ overflow: visible;
+}
+
+/**
+ * Address inconsistent `text-transform` inheritance for `button` and `select`.
+ * All other form control elements do not inherit `text-transform` values.
+ * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
+ * Correct `select` style inheritance in Firefox.
+ */
+
+button,
+select {
+ text-transform: none;
+}
+
+/**
+ * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
+ * and `video` controls.
+ * 2. Correct inability to style clickable `input` types in iOS.
+ * 3. Improve usability and consistency of cursor style between image-type
+ * `input` and others.
+ */
+
+button,
+html input[type="button"], /* 1 */
+input[type="reset"],
+input[type="submit"] {
+ -webkit-appearance: button; /* 2 */
+ cursor: pointer; /* 3 */
+}
+
+/**
+ * Re-set default cursor for disabled elements.
+ */
+
+button[disabled],
+html input[disabled] {
+ cursor: default;
+}
+
+/**
+ * Remove inner padding and border in Firefox 4+.
+ */
+
+button::-moz-focus-inner,
+input::-moz-focus-inner {
+ border: 0;
+ padding: 0;
+}
+
+/**
+ * Address Firefox 4+ setting `line-height` on `input` using `!important` in
+ * the UA stylesheet.
+ */
+
+input {
+ line-height: normal;
+}
+
+/**
+ * It's recommended that you don't attempt to style these elements.
+ * Firefox's implementation doesn't respect box-sizing, padding, or width.
+ *
+ * 1. Address box sizing set to `content-box` in IE 8/9/10.
+ * 2. Remove excess padding in IE 8/9/10.
+ */
+
+input[type="checkbox"],
+input[type="radio"] {
+ box-sizing: border-box; /* 1 */
+ padding: 0; /* 2 */
+}
+
+/**
+ * Fix the cursor style for Chrome's increment/decrement buttons. For certain
+ * `font-size` values of the `input`, it causes the cursor style of the
+ * decrement button to change from `default` to `text`.
+ */
+
+input[type="number"]::-webkit-inner-spin-button,
+input[type="number"]::-webkit-outer-spin-button {
+ height: auto;
+}
+
+/**
+ * 1. Address `appearance` set to `searchfield` in Safari and Chrome.
+ * 2. Address `box-sizing` set to `border-box` in Safari and Chrome.
+ */
+
+input[type="search"] {
+ -webkit-appearance: textfield; /* 1 */
+ box-sizing: content-box; /* 2 */
+}
+
+/**
+ * Remove inner padding and search cancel button in Safari and Chrome on OS X.
+ * Safari (but not Chrome) clips the cancel button when the search input has
+ * padding (and `textfield` appearance).
+ */
+
+input[type="search"]::-webkit-search-cancel-button,
+input[type="search"]::-webkit-search-decoration {
+ -webkit-appearance: none;
+}
+
+/**
+ * Define consistent border, margin, and padding.
+ */
+
+fieldset {
+ border: 1px solid #c0c0c0;
+ margin: 0 2px;
+ padding: 0.35em 0.625em 0.75em;
+}
+
+/**
+ * 1. Correct `color` not being inherited in IE 8/9/10/11.
+ * 2. Remove padding so people aren't caught out if they zero out fieldsets.
+ */
+
+legend {
+ border: 0; /* 1 */
+ padding: 0; /* 2 */
+}
+
+/**
+ * Remove default vertical scrollbar in IE 8/9/10/11.
+ */
+
+textarea {
+ overflow: auto;
+}
+
+/**
+ * Don't inherit the `font-weight` (applied by a rule above).
+ * NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
+ */
+
+optgroup {
+ font-weight: bold;
+}
+
+/* Tables
+ ========================================================================== */
+
+/**
+ * Remove most spacing between table cells.
+ */
+
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
+
+td,
+th {
+ padding: 0;
+}
+
+/*csslint important:false*/
+
+/* ==========================================================================
+ Pure Base Extras
+ ========================================================================== */
+
+/**
+ * Extra rules that Pure adds on top of Normalize.css
+ */
+
+/**
+ * Always hide an element when it has the `hidden` HTML attribute.
+ */
+
+.hidden,
+[hidden] {
+ display: none !important;
+}
+
+/**
+ * Add this class to an image to make it fit within it's fluid parent wrapper while maintaining
+ * aspect ratio.
+ */
+.pure-img {
+ max-width: 100%;
+ height: auto;
+ display: block;
+}
+
+/*csslint regex-selectors:false, known-properties:false, duplicate-properties:false*/
+
+.pure-g {
+ letter-spacing: -0.31em; /* Webkit: collapse white-space between units */
+ *letter-spacing: normal; /* reset IE < 8 */
+ *word-spacing: -0.43em; /* IE < 8: collapse white-space between units */
+ text-rendering: optimizespeed; /* Webkit: fixes text-rendering: optimizeLegibility */
+
+ /*
+ Sets the font stack to fonts known to work properly with the above letter
+ and word spacings. See: https://github.com/yahoo/pure/issues/41/
+
+ The following font stack makes Pure Grids work on all known environments.
+
+ * FreeSans: Ships with many Linux distros, including Ubuntu
+
+ * Arimo: Ships with Chrome OS. Arimo has to be defined before Helvetica and
+ Arial to get picked up by the browser, even though neither is available
+ in Chrome OS.
+
+ * Droid Sans: Ships with all versions of Android.
+
+ * Helvetica, Arial, sans-serif: Common font stack on OS X and Windows.
+ */
+ font-family: FreeSans, Arimo, "Droid Sans", Helvetica, Arial, sans-serif;
+
+ /* Use flexbox when possible to avoid `letter-spacing` side-effects. */
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: -ms-flexbox;
+ display: flex;
+ -webkit-flex-flow: row wrap;
+ -ms-flex-flow: row wrap;
+ flex-flow: row wrap;
+
+ /* Prevents distributing space between rows */
+ -webkit-align-content: flex-start;
+ -ms-flex-line-pack: start;
+ align-content: flex-start;
+}
+
+/* IE10 display: -ms-flexbox (and display: flex in IE 11) does not work inside a table; fall back to block and rely on font hack */
+@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
+ table .pure-g {
+ display: block;
+ }
+}
+
+/* Opera as of 12 on Windows needs word-spacing.
+ The ".opera-only" selector is used to prevent actual prefocus styling
+ and is not required in markup.
+*/
+.opera-only :-o-prefocus,
+.pure-g {
+ word-spacing: -0.43em;
+}
+
+.pure-u {
+ display: inline-block;
+ *display: inline; /* IE < 8: fake inline-block */
+ zoom: 1;
+ letter-spacing: normal;
+ word-spacing: normal;
+ vertical-align: top;
+ text-rendering: auto;
+}
+
+/*
+Resets the font family back to the OS/browser's default sans-serif font,
+this the same font stack that Normalize.css sets for the `body`.
+*/
+.pure-g [class*="pure-u"] {
+ font-family: sans-serif;
+}
+
+.pure-u-1,
+.pure-u-1-1,
+.pure-u-1-2,
+.pure-u-1-3,
+.pure-u-2-3,
+.pure-u-1-4,
+.pure-u-3-4,
+.pure-u-1-5,
+.pure-u-2-5,
+.pure-u-3-5,
+.pure-u-4-5,
+.pure-u-5-5,
+.pure-u-1-6,
+.pure-u-5-6,
+.pure-u-1-8,
+.pure-u-3-8,
+.pure-u-5-8,
+.pure-u-7-8,
+.pure-u-1-12,
+.pure-u-5-12,
+.pure-u-7-12,
+.pure-u-11-12,
+.pure-u-1-24,
+.pure-u-2-24,
+.pure-u-3-24,
+.pure-u-4-24,
+.pure-u-5-24,
+.pure-u-6-24,
+.pure-u-7-24,
+.pure-u-8-24,
+.pure-u-9-24,
+.pure-u-10-24,
+.pure-u-11-24,
+.pure-u-12-24,
+.pure-u-13-24,
+.pure-u-14-24,
+.pure-u-15-24,
+.pure-u-16-24,
+.pure-u-17-24,
+.pure-u-18-24,
+.pure-u-19-24,
+.pure-u-20-24,
+.pure-u-21-24,
+.pure-u-22-24,
+.pure-u-23-24,
+.pure-u-24-24 {
+ display: inline-block;
+ *display: inline;
+ zoom: 1;
+ letter-spacing: normal;
+ word-spacing: normal;
+ vertical-align: top;
+ text-rendering: auto;
+}
+
+.pure-u-1-24 {
+ width: 4.1667%;
+ *width: 4.1357%;
+}
+
+.pure-u-1-12,
+.pure-u-2-24 {
+ width: 8.3333%;
+ *width: 8.3023%;
+}
+
+.pure-u-1-8,
+.pure-u-3-24 {
+ width: 12.5%;
+ *width: 12.469%;
+}
+
+.pure-u-1-6,
+.pure-u-4-24 {
+ width: 16.6667%;
+ *width: 16.6357%;
+}
+
+.pure-u-1-5 {
+ width: 20%;
+ *width: 19.969%;
+}
+
+.pure-u-5-24 {
+ width: 20.8333%;
+ *width: 20.8023%;
+}
+
+.pure-u-1-4,
+.pure-u-6-24 {
+ width: 25%;
+ *width: 24.969%;
+}
+
+.pure-u-7-24 {
+ width: 29.1667%;
+ *width: 29.1357%;
+}
+
+.pure-u-1-3,
+.pure-u-8-24 {
+ width: 33.3333%;
+ *width: 33.3023%;
+}
+
+.pure-u-3-8,
+.pure-u-9-24 {
+ width: 37.5%;
+ *width: 37.469%;
+}
+
+.pure-u-2-5 {
+ width: 40%;
+ *width: 39.969%;
+}
+
+.pure-u-5-12,
+.pure-u-10-24 {
+ width: 41.6667%;
+ *width: 41.6357%;
+}
+
+.pure-u-11-24 {
+ width: 45.8333%;
+ *width: 45.8023%;
+}
+
+.pure-u-1-2,
+.pure-u-12-24 {
+ width: 50%;
+ *width: 49.969%;
+}
+
+.pure-u-13-24 {
+ width: 54.1667%;
+ *width: 54.1357%;
+}
+
+.pure-u-7-12,
+.pure-u-14-24 {
+ width: 58.3333%;
+ *width: 58.3023%;
+}
+
+.pure-u-3-5 {
+ width: 60%;
+ *width: 59.969%;
+}
+
+.pure-u-5-8,
+.pure-u-15-24 {
+ width: 62.5%;
+ *width: 62.469%;
+}
+
+.pure-u-2-3,
+.pure-u-16-24 {
+ width: 66.6667%;
+ *width: 66.6357%;
+}
+
+.pure-u-17-24 {
+ width: 70.8333%;
+ *width: 70.8023%;
+}
+
+.pure-u-3-4,
+.pure-u-18-24 {
+ width: 75%;
+ *width: 74.969%;
+}
+
+.pure-u-19-24 {
+ width: 79.1667%;
+ *width: 79.1357%;
+}
+
+.pure-u-4-5 {
+ width: 80%;
+ *width: 79.969%;
+}
+
+.pure-u-5-6,
+.pure-u-20-24 {
+ width: 83.3333%;
+ *width: 83.3023%;
+}
+
+.pure-u-7-8,
+.pure-u-21-24 {
+ width: 87.5%;
+ *width: 87.469%;
+}
+
+.pure-u-11-12,
+.pure-u-22-24 {
+ width: 91.6667%;
+ *width: 91.6357%;
+}
+
+.pure-u-23-24 {
+ width: 95.8333%;
+ *width: 95.8023%;
+}
+
+.pure-u-1,
+.pure-u-1-1,
+.pure-u-5-5,
+.pure-u-24-24 {
+ width: 100%;
+}
+.pure-button {
+ /* Structure */
+ display: inline-block;
+ zoom: 1;
+ line-height: normal;
+ white-space: nowrap;
+ vertical-align: middle;
+ text-align: center;
+ cursor: pointer;
+ -webkit-user-drag: none;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ box-sizing: border-box;
+}
+
+/* Firefox: Get rid of the inner focus border */
+.pure-button::-moz-focus-inner {
+ padding: 0;
+ border: 0;
+}
+
+/* Inherit .pure-g styles */
+.pure-button-group {
+ letter-spacing: -0.31em; /* Webkit: collapse white-space between units */
+ *letter-spacing: normal; /* reset IE < 8 */
+ *word-spacing: -0.43em; /* IE < 8: collapse white-space between units */
+ text-rendering: optimizespeed; /* Webkit: fixes text-rendering: optimizeLegibility */
+}
+
+.opera-only :-o-prefocus,
+.pure-button-group {
+ word-spacing: -0.43em;
+}
+
+.pure-button-group .pure-button {
+ letter-spacing: normal;
+ word-spacing: normal;
+ vertical-align: top;
+ text-rendering: auto;
+}
+
+/*csslint outline-none:false*/
+
+.pure-button {
+ font-family: inherit;
+ font-size: 100%;
+ padding: 0.5em 1em;
+ color: #444; /* rgba not supported (IE 8) */
+ color: rgba(0, 0, 0, 0.8); /* rgba supported */
+ border: 1px solid #999; /*IE 6/7/8*/
+ border: none rgba(0, 0, 0, 0); /*IE9 + everything else*/
+ background-color: #e6e6e6;
+ text-decoration: none;
+ border-radius: 2px;
+}
+
+.pure-button-hover,
+.pure-button:hover,
+.pure-button:focus {
+ /* csslint ignore:start */
+ filter: alpha(opacity=90);
+ /* csslint ignore:end */
+ background-image: -webkit-linear-gradient(
+ transparent,
+ rgba(0, 0, 0, 0.05) 40%,
+ rgba(0, 0, 0, 0.1)
+ );
+ background-image: linear-gradient(
+ transparent,
+ rgba(0, 0, 0, 0.05) 40%,
+ rgba(0, 0, 0, 0.1)
+ );
+}
+.pure-button:focus {
+ outline: 0;
+}
+.pure-button-active,
+.pure-button:active {
+ box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.15) inset,
+ 0 0 6px rgba(0, 0, 0, 0.2) inset;
+ border-color: #000\9;
+}
+
+.pure-button[disabled],
+.pure-button-disabled,
+.pure-button-disabled:hover,
+.pure-button-disabled:focus,
+.pure-button-disabled:active {
+ border: none;
+ background-image: none;
+ /* csslint ignore:start */
+ filter: alpha(opacity=40);
+ /* csslint ignore:end */
+ opacity: 0.4;
+ cursor: not-allowed;
+ box-shadow: none;
+ pointer-events: none;
+}
+
+.pure-button-hidden {
+ display: none;
+}
+
+.pure-button-primary,
+.pure-button-selected,
+a.pure-button-primary,
+a.pure-button-selected {
+ background-color: rgb(0, 120, 231);
+ color: #fff;
+}
+
+/* Button Groups */
+.pure-button-group .pure-button {
+ margin: 0;
+ border-radius: 0;
+ border-right: 1px solid #111; /* fallback color for rgba() for IE7/8 */
+ border-right: 1px solid rgba(0, 0, 0, 0.2);
+}
+
+.pure-button-group .pure-button:first-child {
+ border-top-left-radius: 2px;
+ border-bottom-left-radius: 2px;
+}
+.pure-button-group .pure-button:last-child {
+ border-top-right-radius: 2px;
+ border-bottom-right-radius: 2px;
+ border-right: none;
+}
+
+/*csslint box-model:false*/
+/*
+Box-model set to false because we're setting a height on select elements, which
+also have border and padding. This is done because some browsers don't render
+the padding. We explicitly set the box-model for select elements to border-box,
+so we can ignore the csslint warning.
+*/
+
+.pure-form input[type="text"],
+.pure-form input[type="password"],
+.pure-form input[type="email"],
+.pure-form input[type="url"],
+.pure-form input[type="date"],
+.pure-form input[type="month"],
+.pure-form input[type="time"],
+.pure-form input[type="datetime"],
+.pure-form input[type="datetime-local"],
+.pure-form input[type="week"],
+.pure-form input[type="number"],
+.pure-form input[type="search"],
+.pure-form input[type="tel"],
+.pure-form input[type="color"],
+.pure-form select,
+.pure-form textarea {
+ padding: 0.5em 0.6em;
+ display: inline-block;
+ border: 1px solid #ccc;
+ box-shadow: inset 0 1px 3px #ddd;
+ border-radius: 4px;
+ vertical-align: middle;
+ box-sizing: border-box;
+}
+
+/*
+Need to separate out the :not() selector from the rest of the CSS 2.1 selectors
+since IE8 won't execute CSS that contains a CSS3 selector.
+*/
+.pure-form input:not([type]) {
+ padding: 0.5em 0.6em;
+ display: inline-block;
+ border: 1px solid #ccc;
+ box-shadow: inset 0 1px 3px #ddd;
+ border-radius: 4px;
+ box-sizing: border-box;
+}
+
+/* Chrome (as of v.32/34 on OS X) needs additional room for color to display. */
+/* May be able to remove this tweak as color inputs become more standardized across browsers. */
+.pure-form input[type="color"] {
+ padding: 0.2em 0.5em;
+}
+
+.pure-form input[type="text"]:focus,
+.pure-form input[type="password"]:focus,
+.pure-form input[type="email"]:focus,
+.pure-form input[type="url"]:focus,
+.pure-form input[type="date"]:focus,
+.pure-form input[type="month"]:focus,
+.pure-form input[type="time"]:focus,
+.pure-form input[type="datetime"]:focus,
+.pure-form input[type="datetime-local"]:focus,
+.pure-form input[type="week"]:focus,
+.pure-form input[type="number"]:focus,
+.pure-form input[type="search"]:focus,
+.pure-form input[type="tel"]:focus,
+.pure-form input[type="color"]:focus,
+.pure-form select:focus,
+.pure-form textarea:focus {
+ outline: 0;
+ border-color: #129fea;
+}
+
+/*
+Need to separate out the :not() selector from the rest of the CSS 2.1 selectors
+since IE8 won't execute CSS that contains a CSS3 selector.
+*/
+.pure-form input:not([type]):focus {
+ outline: 0;
+ border-color: #129fea;
+}
+
+.pure-form input[type="file"]:focus,
+.pure-form input[type="radio"]:focus,
+.pure-form input[type="checkbox"]:focus {
+ outline: thin solid #129fea;
+ outline: 1px auto #129fea;
+}
+.pure-form .pure-checkbox,
+.pure-form .pure-radio {
+ margin: 0.5em 0;
+ display: block;
+}
+
+.pure-form input[type="text"][disabled],
+.pure-form input[type="password"][disabled],
+.pure-form input[type="email"][disabled],
+.pure-form input[type="url"][disabled],
+.pure-form input[type="date"][disabled],
+.pure-form input[type="month"][disabled],
+.pure-form input[type="time"][disabled],
+.pure-form input[type="datetime"][disabled],
+.pure-form input[type="datetime-local"][disabled],
+.pure-form input[type="week"][disabled],
+.pure-form input[type="number"][disabled],
+.pure-form input[type="search"][disabled],
+.pure-form input[type="tel"][disabled],
+.pure-form input[type="color"][disabled],
+.pure-form select[disabled],
+.pure-form textarea[disabled] {
+ cursor: not-allowed;
+ background-color: #eaeded;
+ color: #cad2d3;
+}
+
+/*
+Need to separate out the :not() selector from the rest of the CSS 2.1 selectors
+since IE8 won't execute CSS that contains a CSS3 selector.
+*/
+.pure-form input:not([type])[disabled] {
+ cursor: not-allowed;
+ background-color: #eaeded;
+ color: #cad2d3;
+}
+.pure-form input[readonly],
+.pure-form select[readonly],
+.pure-form textarea[readonly] {
+ background-color: #eee; /* menu hover bg color */
+ color: #777; /* menu text color */
+ border-color: #ccc;
+}
+
+.pure-form input:focus:invalid,
+.pure-form textarea:focus:invalid,
+.pure-form select:focus:invalid {
+ color: #b94a48;
+ border-color: #e9322d;
+}
+.pure-form input[type="file"]:focus:invalid:focus,
+.pure-form input[type="radio"]:focus:invalid:focus,
+.pure-form input[type="checkbox"]:focus:invalid:focus {
+ outline-color: #e9322d;
+}
+.pure-form select {
+ /* Normalizes the height; padding is not sufficient. */
+ height: 2.25em;
+ border: 1px solid #ccc;
+ background-color: white;
+}
+.pure-form select[multiple] {
+ height: auto;
+}
+.pure-form label {
+ margin: 0.5em 0 0.2em;
+}
+.pure-form fieldset {
+ margin: 0;
+ padding: 0.35em 0 0.75em;
+ border: 0;
+}
+.pure-form legend {
+ display: block;
+ width: 100%;
+ padding: 0.3em 0;
+ margin-bottom: 0.3em;
+ color: #333;
+ border-bottom: 1px solid #e5e5e5;
+}
+
+.pure-form-stacked input[type="text"],
+.pure-form-stacked input[type="password"],
+.pure-form-stacked input[type="email"],
+.pure-form-stacked input[type="url"],
+.pure-form-stacked input[type="date"],
+.pure-form-stacked input[type="month"],
+.pure-form-stacked input[type="time"],
+.pure-form-stacked input[type="datetime"],
+.pure-form-stacked input[type="datetime-local"],
+.pure-form-stacked input[type="week"],
+.pure-form-stacked input[type="number"],
+.pure-form-stacked input[type="search"],
+.pure-form-stacked input[type="tel"],
+.pure-form-stacked input[type="color"],
+.pure-form-stacked input[type="file"],
+.pure-form-stacked select,
+.pure-form-stacked label,
+.pure-form-stacked textarea {
+ display: block;
+ margin: 0.25em 0;
+}
+
+/*
+Need to separate out the :not() selector from the rest of the CSS 2.1 selectors
+since IE8 won't execute CSS that contains a CSS3 selector.
+*/
+.pure-form-stacked input:not([type]) {
+ display: block;
+ margin: 0.25em 0;
+}
+.pure-form-aligned input,
+.pure-form-aligned textarea,
+.pure-form-aligned select,
+/* NOTE: pure-help-inline is deprecated. Use .pure-form-message-inline instead. */
+.pure-form-aligned .pure-help-inline,
+.pure-form-message-inline {
+ display: inline-block;
+ *display: inline;
+ *zoom: 1;
+ vertical-align: middle;
+}
+.pure-form-aligned textarea {
+ vertical-align: top;
+}
+
+/* Aligned Forms */
+.pure-form-aligned .pure-control-group {
+ margin-bottom: 0.5em;
+}
+.pure-form-aligned .pure-control-group label {
+ text-align: right;
+ display: inline-block;
+ vertical-align: middle;
+ width: 10em;
+ margin: 0 1em 0 0;
+}
+.pure-form-aligned .pure-controls {
+ margin: 1.5em 0 0 11em;
+}
+
+/* Rounded Inputs */
+.pure-form input.pure-input-rounded,
+.pure-form .pure-input-rounded {
+ border-radius: 2em;
+ padding: 0.5em 1em;
+}
+
+/* Grouped Inputs */
+.pure-form .pure-group fieldset {
+ margin-bottom: 10px;
+}
+.pure-form .pure-group input,
+.pure-form .pure-group textarea {
+ display: block;
+ padding: 10px;
+ margin: 0 0 -1px;
+ border-radius: 0;
+ position: relative;
+ top: -1px;
+}
+.pure-form .pure-group input:focus,
+.pure-form .pure-group textarea:focus {
+ z-index: 3;
+}
+.pure-form .pure-group input:first-child,
+.pure-form .pure-group textarea:first-child {
+ top: 1px;
+ border-radius: 4px 4px 0 0;
+ margin: 0;
+}
+.pure-form .pure-group input:first-child:last-child,
+.pure-form .pure-group textarea:first-child:last-child {
+ top: 1px;
+ border-radius: 4px;
+ margin: 0;
+}
+.pure-form .pure-group input:last-child,
+.pure-form .pure-group textarea:last-child {
+ top: -2px;
+ border-radius: 0 0 4px 4px;
+ margin: 0;
+}
+.pure-form .pure-group button {
+ margin: 0.35em 0;
+}
+
+.pure-form .pure-input-1 {
+ width: 100%;
+}
+.pure-form .pure-input-3-4 {
+ width: 75%;
+}
+.pure-form .pure-input-2-3 {
+ width: 66%;
+}
+.pure-form .pure-input-1-2 {
+ width: 50%;
+}
+.pure-form .pure-input-1-3 {
+ width: 33%;
+}
+.pure-form .pure-input-1-4 {
+ width: 25%;
+}
+
+/* Inline help for forms */
+/* NOTE: pure-help-inline is deprecated. Use .pure-form-message-inline instead. */
+.pure-form .pure-help-inline,
+.pure-form-message-inline {
+ display: inline-block;
+ padding-left: 0.3em;
+ color: #666;
+ vertical-align: middle;
+ font-size: 0.875em;
+}
+
+/* Block help for forms */
+.pure-form-message {
+ display: block;
+ color: #666;
+ font-size: 0.875em;
+}
+
+@media only screen and (max-width: 480px) {
+ .pure-form button[type="submit"] {
+ margin: 0.7em 0 0;
+ }
+
+ .pure-form input:not([type]),
+ .pure-form input[type="text"],
+ .pure-form input[type="password"],
+ .pure-form input[type="email"],
+ .pure-form input[type="url"],
+ .pure-form input[type="date"],
+ .pure-form input[type="month"],
+ .pure-form input[type="time"],
+ .pure-form input[type="datetime"],
+ .pure-form input[type="datetime-local"],
+ .pure-form input[type="week"],
+ .pure-form input[type="number"],
+ .pure-form input[type="search"],
+ .pure-form input[type="tel"],
+ .pure-form input[type="color"],
+ .pure-form label {
+ margin-bottom: 0.3em;
+ display: block;
+ }
+
+ .pure-group input:not([type]),
+ .pure-group input[type="text"],
+ .pure-group input[type="password"],
+ .pure-group input[type="email"],
+ .pure-group input[type="url"],
+ .pure-group input[type="date"],
+ .pure-group input[type="month"],
+ .pure-group input[type="time"],
+ .pure-group input[type="datetime"],
+ .pure-group input[type="datetime-local"],
+ .pure-group input[type="week"],
+ .pure-group input[type="number"],
+ .pure-group input[type="search"],
+ .pure-group input[type="tel"],
+ .pure-group input[type="color"] {
+ margin-bottom: 0;
+ }
+
+ .pure-form-aligned .pure-control-group label {
+ margin-bottom: 0.3em;
+ text-align: left;
+ display: block;
+ width: 100%;
+ }
+
+ .pure-form-aligned .pure-controls {
+ margin: 1.5em 0 0 0;
+ }
+
+ /* NOTE: pure-help-inline is deprecated. Use .pure-form-message-inline instead. */
+ .pure-form .pure-help-inline,
+ .pure-form-message-inline,
+ .pure-form-message {
+ display: block;
+ font-size: 0.75em;
+ /* Increased bottom padding to make it group with its related input element. */
+ padding: 0.2em 0 0.8em;
+ }
+}
+
+/*csslint adjoining-classes: false, box-model:false*/
+.pure-menu {
+ box-sizing: border-box;
+}
+
+.pure-menu-fixed {
+ position: fixed;
+ left: 0;
+ top: 0;
+ z-index: 3;
+}
+
+.pure-menu-list,
+.pure-menu-item {
+ position: relative;
+}
+
+.pure-menu-list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+.pure-menu-item {
+ padding: 0;
+ margin: 0;
+ height: 100%;
+}
+
+.pure-menu-link,
+.pure-menu-heading {
+ display: block;
+ text-decoration: none;
+ white-space: nowrap;
+}
+
+/* HORIZONTAL MENU */
+.pure-menu-horizontal {
+ width: 100%;
+ white-space: nowrap;
+}
+
+.pure-menu-horizontal .pure-menu-list {
+ display: inline-block;
+}
+
+/* Initial menus should be inline-block so that they are horizontal */
+.pure-menu-horizontal .pure-menu-item,
+.pure-menu-horizontal .pure-menu-heading,
+.pure-menu-horizontal .pure-menu-separator {
+ display: inline-block;
+ *display: inline;
+ zoom: 1;
+ vertical-align: middle;
+}
+
+/* Submenus should still be display: block; */
+.pure-menu-item .pure-menu-item {
+ display: block;
+}
+
+.pure-menu-children {
+ display: none;
+ position: absolute;
+ left: 100%;
+ top: 0;
+ margin: 0;
+ padding: 0;
+ z-index: 3;
+}
+
+.pure-menu-horizontal .pure-menu-children {
+ left: 0;
+ top: auto;
+ width: inherit;
+}
+
+.pure-menu-allow-hover:hover > .pure-menu-children,
+.pure-menu-active > .pure-menu-children {
+ display: block;
+ position: absolute;
+}
+
+/* Vertical Menus - show the dropdown arrow */
+.pure-menu-has-children > .pure-menu-link:after {
+ padding-left: 0.5em;
+ content: "\25B8";
+ font-size: small;
+}
+
+/* Horizontal Menus - show the dropdown arrow */
+.pure-menu-horizontal .pure-menu-has-children > .pure-menu-link:after {
+ content: "\25BE";
+}
+
+/* scrollable menus */
+.pure-menu-scrollable {
+ overflow-y: scroll;
+ overflow-x: hidden;
+}
+
+.pure-menu-scrollable .pure-menu-list {
+ display: block;
+}
+
+.pure-menu-horizontal.pure-menu-scrollable .pure-menu-list {
+ display: inline-block;
+}
+
+.pure-menu-horizontal.pure-menu-scrollable {
+ white-space: nowrap;
+ overflow-y: hidden;
+ overflow-x: auto;
+ -ms-overflow-style: none;
+ -webkit-overflow-scrolling: touch;
+ /* a little extra padding for this style to allow for scrollbars */
+ padding: 0.5em 0;
+}
+
+.pure-menu-horizontal.pure-menu-scrollable::-webkit-scrollbar {
+ display: none;
+}
+
+/* misc default styling */
+
+.pure-menu-separator,
+.pure-menu-horizontal .pure-menu-children .pure-menu-separator {
+ background-color: #ccc;
+ height: 1px;
+ margin: 0.3em 0;
+}
+
+.pure-menu-horizontal .pure-menu-separator {
+ width: 1px;
+ height: 1.3em;
+ margin: 0 0.3em;
+}
+
+/* Need to reset the separator since submenu is vertical */
+.pure-menu-horizontal .pure-menu-children .pure-menu-separator {
+ display: block;
+ width: auto;
+}
+
+.pure-menu-heading {
+ text-transform: uppercase;
+ color: #565d64;
+}
+
+.pure-menu-link {
+ color: #777;
+}
+
+.pure-menu-children {
+ background-color: #fff;
+}
+
+.pure-menu-link,
+.pure-menu-disabled,
+.pure-menu-heading {
+ padding: 0.5em 1em;
+}
+
+.pure-menu-disabled {
+ opacity: 0.5;
+}
+
+.pure-menu-disabled .pure-menu-link:hover {
+ background-color: transparent;
+}
+
+.pure-menu-active > .pure-menu-link,
+.pure-menu-link:hover,
+.pure-menu-link:focus {
+ background-color: #eee;
+}
+
+.pure-menu-selected .pure-menu-link,
+.pure-menu-selected .pure-menu-link:visited {
+ color: #000;
+}
+
+.pure-table {
+ /* Remove spacing between table cells (from Normalize.css) */
+ border-collapse: collapse;
+ border-spacing: 0;
+ empty-cells: show;
+ border: 1px solid #cbcbcb;
+}
+
+.pure-table caption {
+ color: #000;
+ font: italic 85%/1 arial, sans-serif;
+ padding: 1em 0;
+ text-align: center;
+}
+
+.pure-table td,
+.pure-table th {
+ border-left: 1px solid #cbcbcb; /* inner column border */
+ border-width: 0 0 0 1px;
+ font-size: inherit;
+ margin: 0;
+ overflow: visible; /*to make ths where the title is really long work*/
+ padding: 0.5em 1em; /* cell padding */
+}
+
+/* Consider removing this next declaration block, as it causes problems when
+there's a rowspan on the first cell. Case added to the tests. issue#432 */
+.pure-table td:first-child,
+.pure-table th:first-child {
+ border-left-width: 0;
+}
+
+.pure-table thead {
+ background-color: #e0e0e0;
+ color: #000;
+ text-align: left;
+ vertical-align: bottom;
+}
+
+/*
+striping:
+ even - #fff (white)
+ odd - #f2f2f2 (light gray)
+*/
+.pure-table td {
+ background-color: transparent;
+}
+.pure-table-odd td {
+ background-color: #f2f2f2;
+}
+
+/* nth-child selector for modern browsers */
+.pure-table-striped tr:nth-child(2n-1) td {
+ background-color: #f2f2f2;
+}
+
+/* BORDERED TABLES */
+.pure-table-bordered td {
+ border-bottom: 1px solid #cbcbcb;
+}
+.pure-table-bordered tbody > tr:last-child > td {
+ border-bottom-width: 0;
+}
+
+/* HORIZONTAL BORDERED TABLES */
+
+.pure-table-horizontal td,
+.pure-table-horizontal th {
+ border-width: 0 0 1px 0;
+ border-bottom: 1px solid #cbcbcb;
+}
+.pure-table-horizontal tbody > tr:last-child > td {
+ border-bottom-width: 0;
+}
diff --git a/packages/taler-wallet-webextension/webextension/static/style/wallet.css b/packages/taler-wallet-webextension/webextension/static/style/wallet.css
new file mode 100644
index 000000000..7c06f2386
--- /dev/null
+++ b/packages/taler-wallet-webextension/webextension/static/style/wallet.css
@@ -0,0 +1,290 @@
+body {
+ font-size: 100%;
+ overflow-y: scroll;
+ margin-top: 2em;
+}
+
+#main {
+ border: solid 5px black;
+ border-radius: 10px;
+ margin-left: auto;
+ margin-right: auto;
+ padding-top: 2em;
+ max-width: 50%;
+ padding: 2em;
+}
+
+header {
+ width: 100%;
+ height: 100px;
+ margin: 0;
+ padding: 0;
+}
+
+header #logo {
+ float: left;
+ width: 100px;
+ height: 100px;
+ padding: 0;
+ margin: 0;
+ text-align: center;
+ background-image: url(/img/logo.png);
+ background-size: 100px;
+}
+
+aside {
+ width: 100px;
+ float: left;
+}
+
+section#main {
+ margin: auto;
+ padding: 20px;
+ height: 100%;
+ max-width: 50%;
+}
+
+section#main h1:first-child {
+ margin-top: 0;
+}
+
+h1 {
+ font-size: 160%;
+ font-family: "monospace";
+}
+
+h2 {
+ font-size: 140%;
+ font-family: "monospace";
+}
+
+h3 {
+ font-size: 120%;
+ font-family: "monospace";
+}
+
+h4,
+h5,
+h6 {
+ font-family: "monospace";
+ font-size: 100%;
+}
+
+.form-row {
+ padding-top: 5px;
+ padding-bottom: 5px;
+}
+
+label {
+ padding-right: 1em;
+}
+
+input.url {
+ width: 25em;
+}
+
+.formish {
+}
+
+.json-key {
+ color: brown;
+}
+.json-value {
+ color: navy;
+}
+.json-string {
+ color: olive;
+}
+
+button {
+ font-size: 120%;
+ padding: 0.5em;
+}
+
+button.confirm-pay {
+ float: right;
+}
+
+/* We use fading to hide slower DOM updates */
+.fade {
+ -webkit-animation: fade 0.7s;
+ animation: fade 0.7s;
+ opacity: 1;
+}
+
+@-webkit-keyframes fade {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+}
+@keyframes fade {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+}
+
+button.linky {
+ background: none !important;
+ border: none;
+ padding: 0 !important;
+
+ font-family: arial, sans-serif;
+ color: #069;
+ text-decoration: underline;
+ cursor: pointer;
+}
+
+.blacklink a:link,
+.blacklink a:visited,
+.blacklink a:hover,
+.blacklink a:active {
+ color: #000;
+}
+
+table,
+th,
+td {
+ border: 1px solid black;
+}
+
+button.accept {
+ background-color: #5757d2;
+ border: 1px solid black;
+ border-radius: 5px;
+ margin: 1em 0;
+ padding: 0.5em;
+ font-weight: bold;
+ color: white;
+}
+button.linky {
+ background: none !important;
+ border: none;
+ padding: 0 !important;
+
+ font-family: arial, sans-serif;
+ color: #069;
+ text-decoration: underline;
+ cursor: pointer;
+}
+
+button.accept:disabled {
+ background-color: #dedbe8;
+ border: 1px solid white;
+ border-radius: 5px;
+ margin: 1em 0;
+ padding: 0.5em;
+ font-weight: bold;
+ color: #2c2c2c;
+}
+
+input.url {
+ width: 25em;
+}
+
+table {
+ border-collapse: collapse;
+}
+
+td {
+ border-left: 1px solid black;
+ border-right: 1px solid black;
+ text-align: center;
+ padding: 0.3em;
+}
+
+span.spacer {
+ padding-left: 0.5em;
+ padding-right: 0.5em;
+}
+
+.button-success,
+.button-destructive,
+.button-warning,
+.button-secondary {
+ color: white;
+ border-radius: 4px;
+ text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
+}
+
+.button-success {
+ background: rgb(28, 184, 65);
+}
+
+.button-destructive {
+ background: rgb(202, 60, 60);
+}
+
+.button-warning {
+ background: rgb(223, 117, 20);
+}
+
+.button-secondary {
+ background: rgb(66, 184, 221);
+}
+
+a.actionLink {
+ color: black;
+}
+
+.errorbox {
+ border: 1px solid;
+ display: inline-block;
+ margin: 1em;
+ padding: 1em;
+ font-weight: bold;
+ background: #ff8a8a;
+}
+
+.okaybox {
+ border: 1px solid;
+ display: inline-block;
+ margin: 1em;
+ padding: 1em;
+ font-weight: bold;
+ background: #00fa9a;
+}
+
+a.opener {
+ color: black;
+}
+.opener-open::before {
+ content: "\25bc";
+}
+.opener-collapsed::before {
+ content: "\25b6 ";
+}
+
+.svg-icon {
+ display: inline-flex;
+ align-self: center;
+ position: relative;
+ height: 1em;
+ width: 1em;
+}
+.svg-icon svg {
+ height: 1em;
+ width: 1em;
+}
+object.svg-icon.svg-baseline {
+ transform: translate(0, 0.125em);
+}
+
+.switch {
+ position: relative;
+ display: inline-block;
+ width: 60px;
+ height: 34px;
+}
+
+/* Hide default HTML checkbox */
+.switch input {
+ opacity: 0;
+ width: 0;
+ height: 0;
+} \ No newline at end of file
diff --git a/packages/taler-wallet-webextension/webextension/static/tip.html b/packages/taler-wallet-webextension/webextension/static/tip.html
new file mode 100644
index 000000000..00ed4d248
--- /dev/null
+++ b/packages/taler-wallet-webextension/webextension/static/tip.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="UTF-8" />
+ <title>Taler Wallet: Received Tip</title>
+
+ <link rel="icon" href="/img/icon.png" />
+ <link rel="stylesheet" type="text/css" href="/style/pure.css" />
+ <link rel="stylesheet" type="text/css" href="/style/wallet.css" />
+ <script src="/pageEntryPoint.js"></script>
+ </head>
+
+ <body>
+ <section id="main">
+ <h1>GNU Taler Wallet</h1>
+ <div id="container"></div>
+ </section>
+ </body>
+</html>
diff --git a/packages/taler-wallet-webextension/webextension/static/welcome.html b/packages/taler-wallet-webextension/webextension/static/welcome.html
new file mode 100644
index 000000000..07ecac707
--- /dev/null
+++ b/packages/taler-wallet-webextension/webextension/static/welcome.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="UTF-8" />
+ <title>Taler Wallet Installed</title>
+
+ <link rel="icon" href="/img/icon.png" />
+ <link rel="stylesheet" type="text/css" href="/style/pure.css" />
+ <link rel="stylesheet" type="text/css" href="/style/wallet.css" />
+ <script src="/pageEntryPoint.js"></script>
+ </head>
+
+ <body>
+ <section id="main">
+ <div style="border-bottom: 3px dashed #aa3939; margin-bottom: 2em;">
+ <h1 style="font-family: monospace; font-size: 250%;">
+ <span style="color: #aa3939;">❰</span>Taler Wallet<span style="color: #aa3939;">❱</span>
+ </h1>
+ </div>
+ <h1>Browser Extension Installed!</h1>
+ <div id="container">Loading...</div>
+ </section>
+ </body>
+</html>
diff --git a/packages/taler-wallet-webextension/webextension/static/withdraw.html b/packages/taler-wallet-webextension/webextension/static/withdraw.html
new file mode 100644
index 000000000..5137204bd
--- /dev/null
+++ b/packages/taler-wallet-webextension/webextension/static/withdraw.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="UTF-8" />
+ <title>Taler Wallet: Withdraw</title>
+ <link rel="icon" href="/img/icon.png" />
+ <link rel="stylesheet" type="text/css" href="/style/pure.css" />
+ <link rel="stylesheet" type="text/css" href="/style/wallet.css" />
+ <script src="/pageEntryPoint.js"></script>
+ </head>
+
+ <body>
+ <section id="main">
+ <div style="border-bottom: 3px dashed #aa3939; margin-bottom: 2em;">
+ <h1 style="font-family: monospace; font-size: 250%;">
+ <span style="color: #aa3939;">❰</span>Taler Wallet<span style="color: #aa3939;">❱</span>
+ </h1>
+ </div>
+ <div class="fade" id="container"></div>
+ </section>
+ </body>
+</html>