gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[GNUnet-SVN] [taler-wallet-webex] 03/05: headless wallet WIP


From: gnunet
Subject: [GNUnet-SVN] [taler-wallet-webex] 03/05: headless wallet WIP
Date: Thu, 01 Aug 2019 23:21:23 +0200

This is an automated email from the git hooks/post-receive script.

dold pushed a commit to branch master
in repository wallet-webex.

commit cc4e8ddc85d36f29a7385a7f4eb08c77f46b3af6
Author: Florian Dold <address@hidden>
AuthorDate: Wed Jul 31 01:33:56 2019 +0200

    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(-)

diff --git a/.vscode/settings.json b/.vscode/settings.json
index 565900b9..6482c5da 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 fb99d0a7..22bcfe13 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 42a2712b..ae5e9452 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 a8cc3822..3c9fe5bc 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 24342a43..6d43e2e6 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: 
"51R7ARKCD5HJTTV5F4G0M818E9SP280A40G2GVH04CR30GHS84R3JHHP6GSM2D9Q6514CGT568R32C9J6CWM4DSH64TM4DSM851K0CA48CVKAC1P6H144C2160T46DHK8CVM4HJ274S38C1M6S338D9N6GWM8DT684T3JCT36S13EC9G88R3EGHQ8S0KJGSQ60SKGD216N33AGJ2651K2E9S60TMCD1N75244HHQ6X33EDJ570R3GGJ2651MACA38D130DA560VK4HHJ68WK2CA26GW3ECSH6D13EC9S88VK2GT66WVK8D9G750K0D9R8RRK4DHQ71332GHK8D23GE26710M2H9K6WVK8HJ38MVKEGA66N23AC9H88VKACT58MV3CCSJ6H1K4DT38GRK0C9M8N33CE1R60V4AHA38H1KECSH6S33JH9N8GRKGH1K68S36GH354520818CMG26C1H60R30C9
 [...]
+  denomPub:
+    
"51R7ARKCD5HJTTV5F4G0M818E9SP280A40G2GVH04CR30GHS84R3JHHP6GSM2D9Q6514CGT568R32C9J6CWM4DSH64TM4DSM851K0CA48CVKAC1P6H144C2160T46DHK8CVM4HJ274S38C1M6S338D9N6GWM8DT684T3JCT36S13EC9G88R3EGHQ8S0KJGSQ60SKGD216N33AGJ2651K2E9S60TMCD1N75244HHQ6X33EDJ570R3GGJ2651MACA38D130DA560VK4HHJ68WK2CA26GW3ECSH6D13EC9S88VK2GT66WVK8D9G750K0D9R8RRK4DHQ71332GHK8D23GE26710M2H9K6WVK8HJ38MVKEGA66N23AC9H88VKACT58MV3CCSJ6H1K4DT38GRK0C9M8N33CE1R60V4AHA38H1KECSH6S33JH9N8GRKGH1K68S36GH354520818CMG26C1H60R30C935452081
 [...]
   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 43a3bc22..d3a93ff8 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 9c5263a6..5acda905 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 58d83e6f..305e50ff 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 2ddc15a3..70a85cda 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 59437da4..25dc6b85 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 00000000..0916ef14
--- /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<IDBDatabase> {
+  return new Promise<IDBDatabase>((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<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:
+          if (e.oldVersion !== WALLET_DB_VERSION) {
+            onUpgradeUnsupported(e.oldVersion, WALLET_DB_VERSION);
+            throw Error("incompatible DB");
+          }
+          break;
+      }
+    };
+  });
+}
+
+export function exportDb(db: IDBDatabase): Promise<any> {
+  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<void> {
+  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();
+    });
+  });
+}
+
+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 00000000..c57c3ab0
--- /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<import("../http").HttpResponse> {
+    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<import("../http").HttpResponse> {
+    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<import("../http").HttpResponse> {
+    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<BankUser> {
+  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 a102b397..6bdd04e2 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 db49b074..e8bb2e51 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 ea7d3447..d3bb5d48 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 fd7887a8..4fc108a1 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<CoinRecord> {
     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<string> {
+    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<SenderWireInfos> {
@@ -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 db44f039..7cbde964 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 76c42889..79c46857 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";

-- 
To stop receiving notification emails like this one, please contact
address@hidden.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]