aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/checkable.ts13
-rw-r--r--src/crypto/cryptoApi-test.ts24
-rw-r--r--src/crypto/cryptoApi.ts29
-rw-r--r--src/crypto/cryptoWorker.ts21
-rw-r--r--src/crypto/emscInterface-test.ts8
-rw-r--r--src/crypto/emscInterface.ts3
-rw-r--r--src/crypto/emscLoader.js36
-rw-r--r--src/db.ts122
-rw-r--r--src/headless/taler-wallet-cli.ts233
-rw-r--r--src/http.ts8
-rw-r--r--src/talerTypes.ts2
-rw-r--r--src/timer.ts2
-rw-r--r--src/wallet.ts41
13 files changed, 487 insertions, 55 deletions
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<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 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<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 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<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;