From cc4e8ddc85d36f29a7385a7f4eb08c77f46b3af6 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Wed, 31 Jul 2019 01:33:56 +0200 Subject: [PATCH] headless wallet WIP --- .vscode/settings.json | 3 +- gulpfile.js | 1 - package.json | 5 +- src/checkable.ts | 13 +- src/crypto/cryptoApi-test.ts | 24 ++-- src/crypto/cryptoApi.ts | 29 +++- src/crypto/cryptoWorker.ts | 21 ++- src/crypto/emscInterface-test.ts | 8 +- src/crypto/emscInterface.ts | 3 + src/crypto/emscLoader.js | 36 ++++- src/db.ts | 122 ++++++++++++++++ src/headless/taler-wallet-cli.ts | 233 +++++++++++++++++++++++++++++++ src/http.ts | 8 +- src/talerTypes.ts | 2 +- src/timer.ts | 2 +- src/wallet.ts | 41 +++--- tsconfig.json | 1 - yarn.lock | 45 +++--- 18 files changed, 521 insertions(+), 76 deletions(-) create mode 100644 src/db.ts create mode 100644 src/headless/taler-wallet-cli.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 565900b97..6482c5da1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -35,5 +35,6 @@ "**/*.js.map": true }, "tslint.enable": true, - "editor.wrappingIndent": "same" + "editor.wrappingIndent": "same", + "editor.tabSize": 2 } \ No newline at end of file diff --git a/gulpfile.js b/gulpfile.js index fb99d0a73..22bcfe134 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -109,7 +109,6 @@ const tsBaseArgs = { noImplicitAny: true, allowJs: true, checkJs: true, - noUnusedLocals: true, incremental: true, }; diff --git a/package.json b/package.json index 42a2712bd..ae5e9452d 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,6 @@ "@types/react-dom": "^16.0.0", "ava": "^1.4.1", "awesome-typescript-loader": "^5.2.1", - "axios": "^0.18.0", "glob": "^7.1.1", "gulp": "^4.0.0", "gulp-gzip": "^1.2.0", @@ -59,6 +58,8 @@ "webpack-merge": "^4.1.0" }, "dependencies": { - "commander": "^2.20.0" + "axios": "^0.19.0", + "commander": "^2.20.0", + "source-map-support": "^0.5.12" } } diff --git a/src/checkable.ts b/src/checkable.ts index a8cc38221..3c9fe5bc1 100644 --- a/src/checkable.ts +++ b/src/checkable.ts @@ -60,10 +60,10 @@ export namespace Checkable { stringChecker?: (s: string) => boolean; valueProp?: any; optional?: boolean; - extraAllowed?: boolean; } interface CheckableInfo { + extraAllowed: boolean; props: Prop[]; } @@ -91,7 +91,7 @@ export namespace Checkable { function getCheckableInfo(target: any): CheckableInfo { let chk = target[checkableInfoSym] as CheckableInfo|undefined; if (!chk) { - chk = { props: [] }; + chk = { props: [], extraAllowed: false }; target[checkableInfoSym] = chk; } return chk; @@ -188,7 +188,8 @@ export namespace Checkable { throw new SchemaError( `expected object for ${path.join(".")}, got ${typeof v} instead`); } - const props = type.prototype[checkableInfoSym].props; + const chk = type.prototype[checkableInfoSym]; + const props = chk.props; const remainingPropNames = new Set(Object.getOwnPropertyNames(v)); const obj = new type(); for (const innerProp of props) { @@ -207,7 +208,7 @@ export namespace Checkable { path.concat([innerProp.propertyKey])); } - if (!prop.extraAllowed && remainingPropNames.size !== 0) { + if (!chk.extraAllowed && remainingPropNames.size !== 0) { const err = `superfluous properties ${JSON.stringify(Array.from(remainingPropNames.values()))} of ${typeName}`; throw new SchemaError(err); } @@ -222,16 +223,16 @@ export namespace Checkable { */ export function Class(opts: {extra?: boolean, validate?: boolean} = {}) { return (target: any) => { + const chk = getCheckableInfo(target.prototype); + chk.extraAllowed = !!opts.extra; target.checked = (v: any) => { const cv = checkValue(v, { checker: checkValue, - extraAllowed: !!opts.extra, propertyKey: "(root)", type: target, }, ["(root)"]); if (opts.validate) { if (typeof target.validate !== "function") { - console.error("target", target); throw Error("invalid Checkable annotion: validate method required"); } // May throw exception diff --git a/src/crypto/cryptoApi-test.ts b/src/crypto/cryptoApi-test.ts index 24342a436..6d43e2e6e 100644 --- a/src/crypto/cryptoApi-test.ts +++ b/src/crypto/cryptoApi-test.ts @@ -26,10 +26,12 @@ import { import { CryptoApi } from "./cryptoApi"; -const masterPub1: string = "CQQZ9DY3MZ1ARMN5K1VKDETS04Y2QCKMMCFHZSWJWWVN82BTTH00"; +const masterPub1: string = + "CQQZ9DY3MZ1ARMN5K1VKDETS04Y2QCKMMCFHZSWJWWVN82BTTH00"; const denomValid1: DenominationRecord = { - denomPub: "51R7ARKCD5HJTTV5F4G0M818E9SP280A40G2GVH04CR30GHS84R3JHHP6GSM2D9Q6514CGT568R32C9J6CWM4DSH64TM4DSM851K0CA48CVKAC1P6H144C2160T46DHK8CVM4HJ274S38C1M6S338D9N6GWM8DT684T3JCT36S13EC9G88R3EGHQ8S0KJGSQ60SKGD216N33AGJ2651K2E9S60TMCD1N75244HHQ6X33EDJ570R3GGJ2651MACA38D130DA560VK4HHJ68WK2CA26GW3ECSH6D13EC9S88VK2GT66WVK8D9G750K0D9R8RRK4DHQ71332GHK8D23GE26710M2H9K6WVK8HJ38MVKEGA66N23AC9H88VKACT58MV3CCSJ6H1K4DT38GRK0C9M8N33CE1R60V4AHA38H1KECSH6S33JH9N8GRKGH1K68S36GH354520818CMG26C1H60R30C935452081918G2J2G0", + denomPub: + "51R7ARKCD5HJTTV5F4G0M818E9SP280A40G2GVH04CR30GHS84R3JHHP6GSM2D9Q6514CGT568R32C9J6CWM4DSH64TM4DSM851K0CA48CVKAC1P6H144C2160T46DHK8CVM4HJ274S38C1M6S338D9N6GWM8DT684T3JCT36S13EC9G88R3EGHQ8S0KJGSQ60SKGD216N33AGJ2651K2E9S60TMCD1N75244HHQ6X33EDJ570R3GGJ2651MACA38D130DA560VK4HHJ68WK2CA26GW3ECSH6D13EC9S88VK2GT66WVK8D9G750K0D9R8RRK4DHQ71332GHK8D23GE26710M2H9K6WVK8HJ38MVKEGA66N23AC9H88VKACT58MV3CCSJ6H1K4DT38GRK0C9M8N33CE1R60V4AHA38H1KECSH6S33JH9N8GRKGH1K68S36GH354520818CMG26C1H60R30C935452081918G2J2G0", denomPubHash: "dummy", exchangeBaseUrl: "https://exchange.example.com/", feeDeposit: { @@ -53,7 +55,8 @@ const denomValid1: DenominationRecord = { value: 0, }, isOffered: true, - masterSig: "CJFJCQ48Q45PSGJ5KY94N6M2TPARESM2E15BSPBD95YVVPEARAEQ6V6G4Z2XBMS0QM0F3Y9EYVP276FCS90EQ1578ZC8JHFBZ3NGP3G", + masterSig: + "CJFJCQ48Q45PSGJ5KY94N6M2TPARESM2E15BSPBD95YVVPEARAEQ6V6G4Z2XBMS0QM0F3Y9EYVP276FCS90EQ1578ZC8JHFBZ3NGP3G", stampExpireDeposit: "/Date(1851580381)/", stampExpireLegal: "/Date(1567756381)/", stampExpireWithdraw: "/Date(2482300381)/", @@ -69,24 +72,25 @@ const denomValid1: DenominationRecord = { const denomInvalid1 = JSON.parse(JSON.stringify(denomValid1)); denomInvalid1.value.value += 1; -test("string hashing", async (t) => { +test("string hashing", async t => { const crypto = new CryptoApi(); const s = await crypto.hashString("hello taler"); - const sh = "8RDMADB3YNF3QZBS3V467YZVJAMC2QAQX0TZGVZ6Q5PFRRAJFT70HHN0QF661QR9QWKYMMC7YEMPD679D2RADXCYK8Y669A2A5MKQFR"; + const sh = + "8RDMADB3YNF3QZBS3V467YZVJAMC2QAQX0TZGVZ6Q5PFRRAJFT70HHN0QF661QR9QWKYMMC7YEMPD679D2RADXCYK8Y669A2A5MKQFR"; t.true(s === sh); t.pass(); }); -test("precoin creation", async (t) => { +test("precoin creation", async t => { const crypto = new CryptoApi(); - const {priv, pub} = await crypto.createEddsaKeypair(); + const { priv, pub } = await crypto.createEddsaKeypair(); const r: ReserveRecord = { created: 0, current_amount: null, exchange_base_url: "https://example.com/exchange", hasPayback: false, - precoin_amount: {currency: "PUDOS", value: 0, fraction: 0}, - requested_amount: {currency: "PUDOS", value: 0, fraction: 0}, + precoin_amount: { currency: "PUDOS", value: 0, fraction: 0 }, + requested_amount: { currency: "PUDOS", value: 0, fraction: 0 }, reserve_priv: priv, reserve_pub: pub, timestamp_confirmed: 0, @@ -98,7 +102,7 @@ test("precoin creation", async (t) => { t.pass(); }); -test("denom validation", async (t) => { +test("denom validation", async t => { const crypto = new CryptoApi(); let v: boolean; v = await crypto.isValidDenom(denomValid1, masterPub1); diff --git a/src/crypto/cryptoApi.ts b/src/crypto/cryptoApi.ts index 43a3bc228..d3a93ff8d 100644 --- a/src/crypto/cryptoApi.ts +++ b/src/crypto/cryptoApi.ts @@ -98,6 +98,28 @@ export class CryptoApi { */ private numBusy: number = 0; + public enableTracing = false; + + /** + * Terminate all worker threads. + */ + terminateWorkers() { + for (let worker of this.workers) { + if (worker.w) { + 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; + } + } + } + /** * Start a worker (if not started) and set as busy. */ @@ -136,7 +158,7 @@ export class CryptoApi { ws.w = null; } }; - ws.terminationTimerHandle = timer.after(20 * 1000, destroy); + ws.terminationTimerHandle = timer.after(5 * 1000, destroy); } handleWorkerError(ws: WorkerState, e: ErrorEvent) { @@ -163,7 +185,7 @@ export class CryptoApi { this.findWork(ws); } - findWork(ws: WorkerState) { + private findWork(ws: WorkerState) { // try to find more work for this worker for (let i = 0; i < NUM_PRIO; i++) { const q = this.workQueues[NUM_PRIO - i - 1]; @@ -193,7 +215,8 @@ export class CryptoApi { console.error(`RPC with id ${id} has no registry entry`); return; } - console.log( + + this.enableTracing && console.log( `rpc ${currentWorkItem.operation} took ${timer.performanceNow() - currentWorkItem.startTime}ms`, ); diff --git a/src/crypto/cryptoWorker.ts b/src/crypto/cryptoWorker.ts index 9c5263a6f..5acda9053 100644 --- a/src/crypto/cryptoWorker.ts +++ b/src/crypto/cryptoWorker.ts @@ -56,6 +56,9 @@ import { import * as native from "./emscInterface"; namespace RpcFunctions { + + export let enableTracing: boolean = false; + /** * Create a pre-coin of the given denomination to be withdrawn from then given * reserve. @@ -735,19 +738,25 @@ worker.onmessage = (msg: MessageEvent) => { return; } - console.log("onmessage with", msg.data.operation); - console.log("foo"); + if (RpcFunctions.enableTracing) { + console.log("onmessage with", msg.data.operation); + } emscLoader.getLib().then(p => { const lib = p.lib; if (!native.isInitialized()) { - console.log("initializing emscripten for then first time with lib"); + if (RpcFunctions.enableTracing) { + console.log("initializing emscripten for then first time with lib"); + } native.initialize(lib); } - - console.log("about to execute", msg.data.operation); + if (RpcFunctions.enableTracing) { + console.log("about to execute", msg.data.operation); + } const res = f(...msg.data.args); - console.log("finished executing", msg.data.operation); + if (RpcFunctions.enableTracing) { + console.log("finished executing", msg.data.operation); + } worker.postMessage({ result: res, id: msg.data.id }); }); }; diff --git a/src/crypto/emscInterface-test.ts b/src/crypto/emscInterface-test.ts index 58d83e6fe..305e50ff4 100644 --- a/src/crypto/emscInterface-test.ts +++ b/src/crypto/emscInterface-test.ts @@ -17,8 +17,14 @@ // tslint:disable:max-line-length import test from "ava"; +import * as emscLoader from "./emscLoader"; import * as native from "./emscInterface"; +test.before(async () => { + const { lib } = await emscLoader.getLib(); + native.initialize(lib); +}); + test("string hashing", (t) => { const x = native.ByteArray.fromStringWithNull("hello taler"); const h = "8RDMADB3YNF3QZBS3V467YZVJAMC2QAQX0TZGVZ6Q5PFRRAJFT70HHN0QF661QR9QWKYMMC7YEMPD679D2RADXCYK8Y669A2A5MKQFR"; @@ -99,7 +105,7 @@ test("withdraw-request", (t) => { }); -test("withdraw-request", (t) => { +test("currency-conversion", (t) => { const a1 = new native.Amount({currency: "KUDOS", value: 1, fraction: 50000000}); const a2 = new native.Amount({currency: "KUDOS", value: 1, fraction: 50000000}); a1.add(a2); diff --git a/src/crypto/emscInterface.ts b/src/crypto/emscInterface.ts index 2ddc15a37..70a85cda1 100644 --- a/src/crypto/emscInterface.ts +++ b/src/crypto/emscInterface.ts @@ -43,6 +43,9 @@ export function initialize(lib: EmscLib) { if (!lib) { throw Error("library must be object"); } + if (!lib.ccall) { + throw Error("sanity check failed: EmscLib does not have 'ccall'"); + } if (maybeEmscEnv) { throw Error("emsc lib already initialized"); } diff --git a/src/crypto/emscLoader.js b/src/crypto/emscLoader.js index 59437da42..25dc6b85c 100644 --- a/src/crypto/emscLoader.js +++ b/src/crypto/emscLoader.js @@ -25,20 +25,29 @@ */ let cachedLib = undefined; +let cachedLibPromise = undefined; + +export let enableTracing = false; /** * Load the taler emscripten lib. * * If in a WebWorker, importScripts is used. Inside a browser, the module must * be globally available. Inside node, require is used. + * + * Returns a Promise<{ lib: EmscLib }> */ export function getLib() { - console.log("in getLib"); + enableTracing && console.log("in getLib"); if (cachedLib) { - console.log("lib is cached"); + enableTracing && console.log("lib is cached"); return Promise.resolve({ lib: cachedLib }); } + if (cachedLibPromise) { + return cachedLibPromise; + } if (typeof require !== "undefined") { + enableTracing && console.log("trying to load emscripten lib with 'require'"); // Make sure that TypeScript doesn't try // to check the taler-emscripten-lib. const indirectRequire = require; @@ -49,17 +58,30 @@ export function getLib() { const savedImportScripts = g.importScripts; delete g.importScripts; // Assume that the code is run from the build/ directory. - const lib = indirectRequire("../../../emscripten/taler-emscripten-lib.js"); + const libFn = indirectRequire("../../../emscripten/taler-emscripten-lib.js"); + const lib = libFn(); g.importScripts = savedImportScripts; if (lib) { - cachedLib = lib; - return Promise.resolve({ lib: cachedLib }); + if (!lib.ccall) { + throw Error("sanity check failed: taler-emscripten lib does not have 'ccall'"); + } + cachedLibPromise = new Promise((resolve, reject) => { + lib.onRuntimeInitialized = () => { + cachedLib = lib; + cachedLibPromise = undefined; + resolve({ lib: cachedLib }); + }; + }); + return cachedLibPromise; + } else { + // When we're running as a webpack bundle, the above require might + // have failed and returned 'undefined', so we try other ways to import. + console.log("failed to load emscripten lib with 'require', trying alternatives"); } - // When we're running as a webpack bundle, the above require might - // have failed and returned 'undefined', so we try other ways to import. } if (typeof importScripts !== "undefined") { + console.log("trying to load emscripten lib with 'importScripts'"); self.TalerEmscriptenLib = {}; importScripts('/emscripten/taler-emscripten-lib.js') if (!self.TalerEmscriptenLib) { diff --git a/src/db.ts b/src/db.ts new file mode 100644 index 000000000..0916ef145 --- /dev/null +++ b/src/db.ts @@ -0,0 +1,122 @@ +import { Stores, WALLET_DB_VERSION } from "./dbTypes"; +import { Store, Index } from "./query"; + +const DB_NAME = "taler"; + +/** + * Return a promise that resolves + * to the taler wallet db. + */ +export function openTalerDb( + idbFactory: IDBFactory, + onVersionChange: () => void, + onUpgradeUnsupported: (oldVersion: number, newVersion: number) => void, +): Promise { + return new Promise((resolve, reject) => { + const req = idbFactory.open(DB_NAME, WALLET_DB_VERSION); + req.onerror = e => { + console.log("taler database error", e); + reject(e); + }; + req.onsuccess = e => { + req.result.onversionchange = (evt: IDBVersionChangeEvent) => { + console.log( + `handling live db version change from ${evt.oldVersion} to ${ + evt.newVersion + }`, + ); + req.result.close(); + onVersionChange(); + }; + resolve(req.result); + }; + req.onupgradeneeded = e => { + const db = req.result; + console.log( + `DB: upgrade needed: oldVersion=${e.oldVersion}, newVersion=${ + e.newVersion + }`, + ); + switch (e.oldVersion) { + case 0: // DB does not exist yet + for (const n in Stores) { + if ((Stores as any)[n] instanceof Store) { + const si: Store = (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 = (si as any)[indexName]; + s.createIndex(ii.indexName, ii.keyPath, ii.options); + } + } + } + } + break; + default: + if (e.oldVersion !== WALLET_DB_VERSION) { + onUpgradeUnsupported(e.oldVersion, WALLET_DB_VERSION); + throw Error("incompatible DB"); + } + break; + } + }; + }); +} + +export function exportDb(db: IDBDatabase): Promise { + const dump = { + name: db.name, + stores: {} as { [s: string]: any }, + version: db.version, + }; + + return new Promise((resolve, reject) => { + const tx = db.transaction(Array.from(db.objectStoreNames)); + tx.addEventListener("complete", () => { + resolve(dump); + }); + // tslint:disable-next-line:prefer-for-of + for (let i = 0; i < db.objectStoreNames.length; i++) { + const name = db.objectStoreNames[i]; + const storeDump = {} as { [s: string]: any }; + dump.stores[name] = storeDump; + tx.objectStore(name) + .openCursor() + .addEventListener("success", (e: Event) => { + const cursor = (e.target as any).result; + if (cursor) { + storeDump[cursor.key] = cursor.value; + cursor.continue(); + } + }); + } + }); +} + +export function importDb(db: IDBDatabase, dump: any): Promise { + console.log("importing db", dump); + return new Promise((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(); + }); + }); +} + +export function deleteDb(idbFactory: IDBFactory) { + idbFactory.deleteDatabase(DB_NAME); +} diff --git a/src/headless/taler-wallet-cli.ts b/src/headless/taler-wallet-cli.ts new file mode 100644 index 000000000..c57c3ab00 --- /dev/null +++ b/src/headless/taler-wallet-cli.ts @@ -0,0 +1,233 @@ +import { MemoryBackend, BridgeIDBFactory, shimIndexedDB } from "idb-bridge"; +import { Wallet } from "../wallet"; +import { Notifier, Badge } from "../walletTypes"; +import { openTalerDb, exportDb } from "../db"; +import { HttpRequestLibrary } from "../http"; +import * as amounts from "../amounts"; +import Axios from "axios"; + +import URI = require("urijs"); + +import querystring = require("querystring"); + +class ConsoleNotifier implements Notifier { + notify(): void { + // nothing to do. + } +} + +class ConsoleBadge implements Badge { + startBusy(): void { + console.log("NOTIFICATION: busy"); + } + stopBusy(): void { + console.log("NOTIFICATION: busy end"); + } + showNotification(): void { + console.log("NOTIFICATION: show"); + } + clearNotification(): void { + console.log("NOTIFICATION: cleared"); + } +} + +export class NodeHttpLib implements HttpRequestLibrary { + async get(url: string): Promise { + console.log("making GET request to", url); + const resp = await Axios({ + method: "get", + url: url, + responseType: "json", + }); + console.log("got response", resp.data); + console.log("resp type", typeof resp.data); + return { + responseJson: resp.data, + status: resp.status, + }; + } + + async postJson( + url: string, + body: any, + ): Promise { + console.log("making POST request to", url); + const resp = await Axios({ + method: "post", + url: url, + responseType: "json", + data: body, + }); + console.log("got response", resp.data); + console.log("resp type", typeof resp.data); + return { + responseJson: resp.data, + status: resp.status, + }; + } + + async postForm( + url: string, + form: any, + ): Promise { + console.log("making POST request to", url); + const resp = await Axios({ + method: "post", + url: url, + data: querystring.stringify(form), + responseType: "json", + }); + console.log("got response", resp.data); + console.log("resp type", typeof resp.data); + return { + responseJson: resp.data, + status: resp.status, + }; + } +} + +interface BankUser { + username: string; + password: string; +} + +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; +} + +async function registerBankUser( + bankBaseUrl: string, + httpLib: HttpRequestLibrary, +): Promise { + const reqUrl = new URI("register").absoluteTo(bankBaseUrl).href(); + const randId = makeId(8); + const bankUser: BankUser = { + username: `testuser-${randId}`, + password: `testpw-${randId}`, + }; + const result = await httpLib.postForm(reqUrl, bankUser); + if (result.status != 200) { + throw Error("could not register bank user"); + } + return bankUser; +} + +async function createBankReserve( + bankBaseUrl: string, + bankUser: BankUser, + amount: string, + reservePub: string, + exchangePaytoUri: string, + httpLib: HttpRequestLibrary, +) { + const reqUrl = new URI("taler/withdraw").absoluteTo(bankBaseUrl).href(); + + const body = { + auth: { type: "basic" }, + username: bankUser, + amount, + reserve_pub: reservePub, + exchange_wire_detail: exchangePaytoUri, + }; + + const resp = await Axios({ + method: "post", + url: reqUrl, + data: body, + responseType: "json", + headers: { + "X-Taler-Bank-Username": bankUser.username, + "X-Taler-Bank-Password": bankUser.password, + }, + }); + + if (resp.status != 200) { + throw Error("failed to create bank reserve"); + } +} + +async function main() { + const myNotifier = new ConsoleNotifier(); + + const myBadge = new ConsoleBadge(); + + const myBackend = new MemoryBackend(); + + myBackend.enableTracing = false; + + BridgeIDBFactory.enableTracing = false; + + const myBridgeIdbFactory = new BridgeIDBFactory(myBackend); + const myIdbFactory: IDBFactory = (myBridgeIdbFactory as any) as IDBFactory; + + const myHttpLib = new NodeHttpLib(); + + const myVersionChange = () => { + console.error("version change requested, should not happen"); + throw Error(); + }; + + const myUnsupportedUpgrade = () => { + console.error("unsupported database migration"); + throw Error(); + }; + + shimIndexedDB(myBridgeIdbFactory); + + const exchangeBaseUrl = "https://exchange.test.taler.net/"; + const bankBaseUrl = "https://bank.test.taler.net/"; + + const myDb = await openTalerDb( + myIdbFactory, + myVersionChange, + myUnsupportedUpgrade, + ); + + const myWallet = new Wallet(myDb, myHttpLib, myBadge, myNotifier); + + const reserveResponse = await myWallet.createReserve({ + amount: amounts.parseOrThrow("TESTKUDOS:10.0"), + exchange: exchangeBaseUrl, + }); + + const bankUser = await registerBankUser(bankBaseUrl, myHttpLib); + + console.log("bank user", bankUser); + + const exchangePaytoUri = await myWallet.getExchangePaytoUri( + "https://exchange.test.taler.net/", + ["x-taler-bank"], + ); + + await createBankReserve( + bankBaseUrl, + bankUser, + "TESTKUDOS:10.0", + reserveResponse.reservePub, + exchangePaytoUri, + myHttpLib, + ); + + await myWallet.confirmReserve({ reservePub: reserveResponse.reservePub }); + + //await myWallet.waitForReserveDrained(reserveResponse.reservePub); + + //myWallet.clearNotification(); + + //myWallet.stop(); + + const dbContents = await exportDb(myDb); + + console.log("db:", JSON.stringify(dbContents, null, 2)); +} + +main().catch(err => { + console.error("Failed with exception:"); + console.error(err); +}); diff --git a/src/http.ts b/src/http.ts index a102b3973..6bdd04e24 100644 --- a/src/http.ts +++ b/src/http.ts @@ -24,7 +24,7 @@ */ export interface HttpResponse { status: number; - responseText: string; + responseJson: object & any; } @@ -58,8 +58,12 @@ export class BrowserHttpLib implements HttpRequestLibrary { } myRequest.addEventListener("readystatechange", (e) => { if (myRequest.readyState === XMLHttpRequest.DONE) { + const responseJson = JSON.parse(myRequest.responseText); + if (responseJson === null || typeof responseJson !== "object") { + reject(Error("Invalid JSON from HTTP response")); + } const resp = { - responseText: myRequest.responseText, + responseJson: responseJson, status: myRequest.status, }; resolve(resp); diff --git a/src/talerTypes.ts b/src/talerTypes.ts index db49b0747..e8bb2e510 100644 --- a/src/talerTypes.ts +++ b/src/talerTypes.ts @@ -852,7 +852,7 @@ export class WireFeesJson { } -@Checkable.Class() +@Checkable.Class({extra: true}) export class AccountInfo { @Checkable.String() url: string; diff --git a/src/timer.ts b/src/timer.ts index ea7d34470..d3bb5d485 100644 --- a/src/timer.ts +++ b/src/timer.ts @@ -33,7 +33,7 @@ class IntervalHandle { } clear() { - clearTimeout(this.h); + clearInterval(this.h); } } diff --git a/src/wallet.ts b/src/wallet.ts index fd7887a85..4fc108a11 100644 --- a/src/wallet.ts +++ b/src/wallet.ts @@ -1334,6 +1334,7 @@ export class Wallet { this.processReserve(reserve); } + private async withdrawExecute(pc: PreCoinRecord): Promise { const wd: any = {}; @@ -1350,7 +1351,7 @@ export class Wallet { status: resp.status, }); } - const r = JSON.parse(resp.responseText); + const r = resp.responseJson; const denomSig = await this.cryptoApi.rsaUnblind( r.ev_sig, pc.blindingKey, @@ -1462,7 +1463,7 @@ export class Wallet { if (resp.status !== 200) { throw Error(); } - const reserveInfo = ReserveStatus.checked(JSON.parse(resp.responseText)); + const reserveInfo = ReserveStatus.checked(resp.responseJson); if (!reserveInfo) { throw Error(); } @@ -1486,7 +1487,7 @@ export class Wallet { throw Error("/wire request failed"); } - const wiJson = JSON.parse(resp.responseText); + const wiJson = resp.responseJson; if (!wiJson) { throw Error("/wire response malformed"); } @@ -1745,6 +1746,17 @@ export class Wallet { return ret; } + async getExchangePaytoUri(exchangeBaseUrl: string, supportedTargetTypes: string[]): Promise { + const wireInfo = await this.getWireInfo(exchangeBaseUrl); + for (let account of wireInfo.accounts) { + const paytoUri = new URI(account.url); + if (supportedTargetTypes.includes(paytoUri.authority())) { + return account.url; + } + } + throw Error("no matching exchange account found"); + } + /** * Update or add exchange DB entry by fetching the /keys information. * Optionally link the reserve entry to the new or existing @@ -1757,9 +1769,7 @@ export class Wallet { if (keysResp.status !== 200) { throw Error("/keys request failed"); } - const exchangeKeysJson = KeysJson.checked( - JSON.parse(keysResp.responseText), - ); + const exchangeKeysJson = KeysJson.checked(keysResp.responseJson); const exchangeWire = await this.getWireInfo(baseUrl); return this.updateExchangeFromJson(baseUrl, exchangeKeysJson, exchangeWire); } @@ -2291,18 +2301,14 @@ export class Wallet { console.log("melt request:", meltReq); const resp = await this.http.postJson(reqUrl.href(), meltReq); - console.log("melt response:", resp.responseText); + console.log("melt response:", resp.responseJson); if (resp.status !== 200) { - console.error(resp.responseText); + console.error(resp.responseJson); throw Error("refresh failed"); } - const respJson = JSON.parse(resp.responseText); - - if (!respJson) { - throw Error("exchange responded with garbage"); - } + const respJson = resp.responseJson; const norevealIndex = respJson.noreveal_index; @@ -2376,7 +2382,7 @@ export class Wallet { return; } - const respJson = JSON.parse(resp.responseText); + const respJson = resp.responseJson; if (!respJson.ev_sigs || !Array.isArray(respJson.ev_sigs)) { console.log("/refresh/reveal did not contain ev_sigs"); @@ -2647,9 +2653,7 @@ export class Wallet { if (resp.status !== 200) { throw Error(); } - const paybackConfirmation = PaybackConfirmation.checked( - JSON.parse(resp.responseText), - ); + const paybackConfirmation = PaybackConfirmation.checked(resp.responseJson); if (paybackConfirmation.reserve_pub !== coin.reservePub) { throw Error(`Coin's reserve doesn't match reserve on payback`); } @@ -2710,6 +2714,7 @@ export class Wallet { */ stop() { this.timerGroup.stopCurrentAndFutureTimers(); + this.cryptoApi.terminateWorkers(); } async getSenderWireInfos(): Promise { @@ -2857,7 +2862,7 @@ export class Wallet { console.error("deposit failed due to status code", resp); continue; } - const respJson = JSON.parse(resp.responseText); + const respJson = resp.responseJson; if (respJson.status !== "DEPOSIT_OK") { console.error("deposit failed", resp); continue; diff --git a/tsconfig.json b/tsconfig.json index db44f039d..7cbde9642 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,7 +19,6 @@ "noImplicitAny": true, "allowJs": true, "checkJs": true, - "noUnusedLocals": true, "incremental": true }, "files": [ diff --git a/yarn.lock b/yarn.lock index 76c42889c..79c468572 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1057,13 +1057,13 @@ awesome-typescript-loader@^5.2.1: source-map-support "^0.5.3" webpack-log "^1.2.0" -axios@^0.18.0: - version "0.18.0" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.18.0.tgz#32d53e4851efdc0a11993b6cd000789d70c05102" - integrity sha1-MtU+SFHv3AoRmTts0AB4nXDAUQI= +axios@^0.19.0: + version "0.19.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.0.tgz#8e09bff3d9122e133f7b8101c8fbdd00ed3d2ab8" + integrity sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ== dependencies: - follow-redirects "^1.3.0" - is-buffer "^1.1.5" + follow-redirects "1.5.10" + is-buffer "^2.0.2" babel-code-frame@^6.22.0: version "6.26.0" @@ -1975,12 +1975,12 @@ debug@2.6.9, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3: dependencies: ms "2.0.0" -debug@^3.2.6: - version "3.2.6" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" - integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== +debug@=3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== dependencies: - ms "^2.1.1" + ms "2.0.0" debug@^4.1.0, debug@^4.1.1: version "4.1.1" @@ -2650,12 +2650,12 @@ flush-write-stream@^1.0.0, flush-write-stream@^1.0.2: inherits "^2.0.3" readable-stream "^2.3.6" -follow-redirects@^1.3.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.7.0.tgz#489ebc198dc0e7f64167bd23b03c4c19b5784c76" - integrity sha512-m/pZQy4Gj287eNy94nivy5wchN3Kp+Q5WgUPNy5lJSZ3sgkVKSYV/ZChMAQVIgx1SqfZ2zBZtPA2YlXIWxxJOQ== +follow-redirects@1.5.10: + version "1.5.10" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a" + integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ== dependencies: - debug "^3.2.6" + debug "=3.1.0" for-in@^1.0.1, for-in@^1.0.2: version "1.0.2" @@ -3343,6 +3343,11 @@ is-buffer@^1.1.5: resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== +is-buffer@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.3.tgz#4ecf3fcf749cbd1e472689e109ac66261a25e725" + integrity sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw== + is-ci@^1.0.10: version "1.2.1" resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.2.1.tgz#e3779c8ee17fccf428488f6e281187f2e632841c" @@ -5760,6 +5765,14 @@ source-map-support@^0.5.11, source-map-support@^0.5.3, source-map-support@~0.5.1 buffer-from "^1.0.0" source-map "^0.6.0" +source-map-support@^0.5.12: + version "0.5.12" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.12.tgz#b4f3b10d51857a5af0138d3ce8003b201613d599" + integrity sha512-4h2Pbvyy15EE02G+JOZpUCmqWJuqrs+sEkzewTm++BPi7Hvn/HwcqLAcNxYAyI0x13CpPPn+kMjl+hplXMHITQ== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + source-map-url@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3"