aboutsummaryrefslogtreecommitdiff
path: root/extension/lib/wallet
diff options
context:
space:
mode:
Diffstat (limited to 'extension/lib/wallet')
-rw-r--r--extension/lib/wallet/checkable.ts136
-rw-r--r--extension/lib/wallet/db.ts97
-rw-r--r--extension/lib/wallet/emscriptif.ts938
-rw-r--r--extension/lib/wallet/http.ts85
-rw-r--r--extension/lib/wallet/query.ts283
-rw-r--r--extension/lib/wallet/timerThread.ts10
-rw-r--r--extension/lib/wallet/types.ts109
-rw-r--r--extension/lib/wallet/wallet.ts697
-rw-r--r--extension/lib/wallet/wxmessaging.js144
-rw-r--r--extension/lib/wallet/wxmessaging.ts138
10 files changed, 2637 insertions, 0 deletions
diff --git a/extension/lib/wallet/checkable.ts b/extension/lib/wallet/checkable.ts
new file mode 100644
index 000000000..7587f529c
--- /dev/null
+++ b/extension/lib/wallet/checkable.ts
@@ -0,0 +1,136 @@
+/*
+ This file is part of TALER
+ (C) 2016 GNUnet e.V.
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ TALER; see the file COPYING. If not, If not, see <http://www.gnu.org/licenses/>
+ */
+
+
+"use strict";
+
+/**
+ * Decorators for type-checking JSON into
+ * an object.
+ * @module Checkable
+ * @author Florian Dold
+ */
+
+export namespace Checkable {
+ let chkSym = Symbol("checkable");
+
+ function checkNumber(target, prop): any {
+ if ((typeof target) !== "number") {
+ throw Error("number expected for " + prop.propertyKey);
+ }
+ return target;
+ }
+
+ function checkString(target, prop): any {
+ if (typeof target !== "string") {
+ throw Error("string expected for " + prop.propertyKey);
+ }
+ return target;
+ }
+
+ function checkAnyObject(target, prop): any {
+ if (typeof target !== "object") {
+ throw Error("object expected for " + prop.propertyKey);
+ }
+ return target;
+ }
+
+ function checkValue(target, prop): any {
+ let type = prop.type;
+ if (!type) {
+ throw Error("assertion failed");
+ }
+ let v = target;
+ if (!v || typeof v !== "object") {
+ throw Error("expected object for " + prop.propertyKey);
+ }
+ let props = type.prototype[chkSym].props;
+ let remainingPropNames = new Set(Object.getOwnPropertyNames(v));
+ let obj = new type();
+ for (let prop of props) {
+ if (!remainingPropNames.has(prop.propertyKey)) {
+ throw Error("Property missing: " + prop.propertyKey);
+ }
+ if (!remainingPropNames.delete(prop.propertyKey)) {
+ throw Error("assertion failed");
+ }
+ let propVal = v[prop.propertyKey];
+ obj[prop.propertyKey] = prop.checker(propVal, prop);
+ }
+
+ if (remainingPropNames.size != 0) {
+ throw Error("superfluous properties " + JSON.stringify(Array.from(
+ remainingPropNames.values())));
+ }
+ return obj;
+ }
+
+ export function Class(target) {
+ target.checked = (v) => {
+ return checkValue(v, {
+ propertyKey: "(root)",
+ type: target,
+ checker: checkValue
+ });
+ };
+ return target;
+ }
+
+ export function Value(type) {
+ function deco(target: Object, propertyKey: string | symbol): void {
+ let chk = mkChk(target);
+ chk.props.push({
+ propertyKey: propertyKey,
+ checker: checkValue,
+ type: type
+ });
+ }
+
+ return deco;
+ }
+
+ export function List(type) {
+ function deco(target: Object, propertyKey: string | symbol): void {
+ throw Error("not implemented");
+ }
+
+ return deco;
+ }
+
+ export function Number(target: Object, propertyKey: string | symbol): void {
+ let chk = mkChk(target);
+ chk.props.push({propertyKey: propertyKey, checker: checkNumber});
+ }
+
+ export function AnyObject(target: Object, propertyKey: string | symbol): void {
+ let chk = mkChk(target);
+ chk.props.push({propertyKey: propertyKey, checker: checkAnyObject});
+ }
+
+ export function String(target: Object, propertyKey: string | symbol): void {
+ let chk = mkChk(target);
+ chk.props.push({propertyKey: propertyKey, checker: checkString});
+ }
+
+ function mkChk(target) {
+ let chk = target[chkSym];
+ if (!chk) {
+ chk = {props: []};
+ target[chkSym] = chk;
+ }
+ return chk;
+ }
+}
diff --git a/extension/lib/wallet/db.ts b/extension/lib/wallet/db.ts
new file mode 100644
index 000000000..a208f0923
--- /dev/null
+++ b/extension/lib/wallet/db.ts
@@ -0,0 +1,97 @@
+/*
+ This file is part of TALER
+ (C) 2016 GNUnet e.V.
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ TALER; see the file COPYING. If not, If not, see <http://www.gnu.org/licenses/>
+ */
+
+"use strict";
+
+/**
+ * Declarations and helpers for
+ * things that are stored in the wallet's
+ * database.
+ * @module Db
+ * @author Florian Dold
+ */
+
+const DB_NAME = "taler";
+const DB_VERSION = 1;
+
+/**
+ * Return a promise that resolves
+ * to the taler wallet db.
+ */
+export function openTalerDb(): Promise<IDBDatabase> {
+ return new Promise((resolve, reject) => {
+ let req = indexedDB.open(DB_NAME, DB_VERSION);
+ req.onerror = (e) => {
+ reject(e);
+ };
+ req.onsuccess = (e) => {
+ resolve(req.result);
+ };
+ req.onupgradeneeded = (e) => {
+ let db = req.result;
+ console.log("DB: upgrade needed: oldVersion = " + e.oldVersion);
+ switch (e.oldVersion) {
+ case 0: // DB does not exist yet
+ let mints = db.createObjectStore("mints", {keyPath: "baseUrl"});
+ mints.createIndex("pubKey", "keys.master_public_key");
+ db.createObjectStore("reserves", {keyPath: "reserve_pub"});
+ db.createObjectStore("denoms", {keyPath: "denomPub"});
+ let coins = db.createObjectStore("coins", {keyPath: "coinPub"});
+ coins.createIndex("mintBaseUrl", "mintBaseUrl");
+ db.createObjectStore("transactions", {keyPath: "contractHash"});
+ db.createObjectStore("precoins",
+ {keyPath: "coinPub", autoIncrement: true});
+ db.createObjectStore("history", {keyPath: "id", autoIncrement: true});
+ break;
+ }
+ };
+ });
+}
+
+
+export function exportDb(db): Promise<any> {
+ let dump = {
+ name: db.name,
+ version: db.version,
+ stores: {}
+ };
+
+ return new Promise((resolve, reject) => {
+
+ let tx = db.transaction(db.objectStoreNames);
+ tx.addEventListener("complete", (e) => {
+ resolve(dump);
+ });
+ for (let i = 0; i < db.objectStoreNames.length; i++) {
+ let name = db.objectStoreNames[i];
+ let storeDump = {};
+ dump.stores[name] = storeDump;
+ let store = tx.objectStore(name)
+ .openCursor()
+ .addEventListener("success", (e) => {
+ let cursor = e.target.result;
+ if (cursor) {
+ storeDump[cursor.key] = cursor.value;
+ cursor.continue();
+ }
+ });
+ }
+ });
+}
+
+export function deleteDb() {
+ indexedDB.deleteDatabase(DB_NAME);
+} \ No newline at end of file
diff --git a/extension/lib/wallet/emscriptif.ts b/extension/lib/wallet/emscriptif.ts
new file mode 100644
index 000000000..d8fd72289
--- /dev/null
+++ b/extension/lib/wallet/emscriptif.ts
@@ -0,0 +1,938 @@
+/*
+ This file is part of TALER
+ (C) 2015 GNUnet e.V.
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ TALER; see the file COPYING. If not, If not, see <http://www.gnu.org/licenses/>
+ */
+
+import {AmountJson_interface} from "./types";
+import * as EmscWrapper from "../emscripten/emsc";
+
+/**
+ * High-level interface to emscripten-compiled modules used
+ * by the wallet.
+ * @module EmscriptIf
+ * @author Florian Dold
+ */
+
+"use strict";
+
+// Size of a native pointer.
+const PTR_SIZE = 4;
+
+const GNUNET_OK = 1;
+const GNUNET_YES = 1;
+const GNUNET_NO = 0;
+const GNUNET_SYSERR = -1;
+
+let Module = EmscWrapper.Module;
+
+let getEmsc: EmscWrapper.EmscFunGen = (...args) => Module.cwrap.apply(null, args);
+
+var emsc = {
+ free: (ptr) => Module._free(ptr),
+ get_value: getEmsc('TALER_WR_get_value',
+ 'number',
+ ['number']),
+ get_fraction: getEmsc('TALER_WR_get_fraction',
+ 'number',
+ ['number']),
+ get_currency: getEmsc('TALER_WR_get_currency',
+ 'string',
+ ['number']),
+ amount_add: getEmsc('TALER_amount_add',
+ 'number',
+ ['number', 'number', 'number']),
+ amount_subtract: getEmsc('TALER_amount_subtract',
+ 'number',
+ ['number', 'number', 'number']),
+ amount_normalize: getEmsc('TALER_amount_normalize',
+ 'void',
+ ['number']),
+ amount_get_zero: getEmsc('TALER_amount_get_zero',
+ 'number',
+ ['string', 'number']),
+ amount_cmp: getEmsc('TALER_amount_cmp',
+ 'number',
+ ['number', 'number']),
+ amount_hton: getEmsc('TALER_amount_hton',
+ 'void',
+ ['number', 'number']),
+ amount_ntoh: getEmsc('TALER_amount_ntoh',
+ 'void',
+ ['number', 'number']),
+ hash: getEmsc('GNUNET_CRYPTO_hash',
+ 'void',
+ ['number', 'number', 'number']),
+ memmove: getEmsc('memmove',
+ 'number',
+ ['number', 'number', 'number']),
+ rsa_public_key_free: getEmsc('GNUNET_CRYPTO_rsa_public_key_free',
+ 'void',
+ ['number']),
+ rsa_signature_free: getEmsc('GNUNET_CRYPTO_rsa_signature_free',
+ 'void',
+ ['number']),
+ string_to_data: getEmsc('GNUNET_STRINGS_string_to_data',
+ 'number',
+ ['number', 'number', 'number', 'number']),
+ eddsa_sign: getEmsc('GNUNET_CRYPTO_eddsa_sign',
+ 'number',
+ ['number', 'number', 'number']),
+ hash_create_random: getEmsc('GNUNET_CRYPTO_hash_create_random',
+ 'void',
+ ['number', 'number']),
+ rsa_blinding_key_destroy: getEmsc('GNUNET_CRYPTO_rsa_blinding_key_free',
+ 'void',
+ ['number']),
+};
+
+var emscAlloc = {
+ get_amount: getEmsc('TALER_WRALL_get_amount',
+ 'number',
+ ['number', 'number', 'number', 'string']),
+ eddsa_key_create: getEmsc('GNUNET_CRYPTO_eddsa_key_create',
+ 'number', []),
+ eddsa_public_key_from_private: getEmsc(
+ 'TALER_WRALL_eddsa_public_key_from_private',
+ 'number',
+ ['number']),
+ data_to_string_alloc: getEmsc('GNUNET_STRINGS_data_to_string_alloc',
+ 'number',
+ ['number', 'number']),
+ purpose_create: getEmsc('TALER_WRALL_purpose_create',
+ 'number',
+ ['number', 'number', 'number']),
+ rsa_blind: getEmsc('GNUNET_CRYPTO_rsa_blind',
+ 'number',
+ ['number', 'number', 'number', 'number']),
+ rsa_blinding_key_create: getEmsc('GNUNET_CRYPTO_rsa_blinding_key_create',
+ 'number',
+ ['number']),
+ rsa_blinding_key_encode: getEmsc('GNUNET_CRYPTO_rsa_blinding_key_encode',
+ 'number',
+ ['number', 'number']),
+ rsa_signature_encode: getEmsc('GNUNET_CRYPTO_rsa_signature_encode',
+ 'number',
+ ['number', 'number']),
+ rsa_blinding_key_decode: getEmsc('GNUNET_CRYPTO_rsa_blinding_key_decode',
+ 'number',
+ ['number', 'number']),
+ rsa_public_key_decode: getEmsc('GNUNET_CRYPTO_rsa_public_key_decode',
+ 'number',
+ ['number', 'number']),
+ rsa_signature_decode: getEmsc('GNUNET_CRYPTO_rsa_signature_decode',
+ 'number',
+ ['number', 'number']),
+ rsa_public_key_encode: getEmsc('GNUNET_CRYPTO_rsa_public_key_encode',
+ 'number',
+ ['number', 'number']),
+ rsa_unblind: getEmsc('GNUNET_CRYPTO_rsa_unblind',
+ 'number',
+ ['number', 'number', 'number']),
+ malloc: (size: number) => Module._malloc(size),
+};
+
+
+enum SignaturePurpose {
+ RESERVE_WITHDRAW = 1200,
+ WALLET_COIN_DEPOSIT = 1201,
+}
+
+enum RandomQuality {
+ WEAK = 0,
+ STRONG = 1,
+ NONCE = 2
+}
+
+
+abstract class ArenaObject {
+ private _nativePtr: number;
+ arena: Arena;
+
+ abstract destroy(): void;
+
+ constructor(arena?: Arena) {
+ this.nativePtr = null;
+ if (!arena) {
+ if (arenaStack.length == 0) {
+ throw Error("No arena available")
+ }
+ arena = arenaStack[arenaStack.length - 1];
+ }
+ arena.put(this);
+ this.arena = arena;
+ }
+
+ getNative(): number {
+ // We want to allow latent allocation
+ // of native wrappers, but we never want to
+ // pass 'undefined' to emscripten.
+ if (this._nativePtr === undefined) {
+ throw Error("Native pointer not initialized");
+ }
+ return this._nativePtr;
+ }
+
+ free() {
+ if (this.nativePtr !== undefined) {
+ emsc.free(this.nativePtr);
+ this.nativePtr = undefined;
+ }
+ }
+
+ alloc(size: number) {
+ if (this.nativePtr !== undefined) {
+ throw Error("Double allocation");
+ }
+ this.nativePtr = emscAlloc.malloc(size);
+ }
+
+ setNative(n: number) {
+ if (n === undefined) {
+ throw Error("Native pointer must be a number or null");
+ }
+ this._nativePtr = n;
+ }
+
+ set nativePtr(v) {
+ this.setNative(v);
+ }
+
+ get nativePtr() {
+ return this.getNative();
+ }
+
+}
+
+interface Arena {
+ put(obj: ArenaObject): void;
+ destroy(): void;
+}
+
+class DefaultArena implements Arena {
+ heap: Array<ArenaObject>;
+
+ constructor() {
+ this.heap = [];
+ }
+
+ put(obj) {
+ this.heap.push(obj);
+ }
+
+ destroy() {
+ for (let obj of this.heap) {
+ obj.destroy();
+ }
+ this.heap = []
+ }
+}
+
+
+function mySetTimeout(ms: number, fn: () => void) {
+ // We need to use different timeouts, depending on whether
+ // we run in node or a web extension
+ if ("function" === typeof setTimeout) {
+ setTimeout(fn, ms);
+ } else {
+ chrome.extension.getBackgroundPage().setTimeout(fn, ms);
+ }
+}
+
+
+/**
+ * Arena that destroys all its objects once control has returned to the message
+ * loop and a small interval has passed.
+ */
+class SyncArena extends DefaultArena {
+ private isScheduled: boolean;
+
+ constructor() {
+ super();
+ }
+
+ pub(obj) {
+ super.put(obj);
+ if (!this.isScheduled) {
+ this.schedule();
+ }
+ this.heap.push(obj);
+ }
+
+ destroy() {
+ super.destroy();
+ }
+
+ private schedule() {
+ this.isScheduled = true;
+ mySetTimeout(50, () => {
+ this.isScheduled = false;
+ this.destroy();
+ });
+ }
+}
+
+let arenaStack: Arena[] = [];
+arenaStack.push(new SyncArena());
+
+
+export class Amount extends ArenaObject {
+ constructor(args?: AmountJson_interface, arena?: Arena) {
+ super(arena);
+ if (args) {
+ this.nativePtr = emscAlloc.get_amount(args.value,
+ 0,
+ args.fraction,
+ args.currency);
+ } else {
+ this.nativePtr = emscAlloc.get_amount(0, 0, 0, "");
+ }
+ }
+
+ destroy() {
+ if (this.nativePtr != 0) {
+ emsc.free(this.nativePtr);
+ }
+ }
+
+
+ static getZero(currency: string, a?: Arena): Amount {
+ let am = new Amount(null, a);
+ let r = emsc.amount_get_zero(currency, am.getNative());
+ if (r != GNUNET_OK) {
+ throw Error("invalid currency");
+ }
+ return am;
+ }
+
+
+ toNbo(a?: Arena): AmountNbo {
+ let x = new AmountNbo(a);
+ x.alloc();
+ emsc.amount_hton(x.nativePtr, this.nativePtr);
+ return x;
+ }
+
+ fromNbo(nbo: AmountNbo): void {
+ emsc.amount_ntoh(this.nativePtr, nbo.nativePtr);
+ }
+
+ get value() {
+ return emsc.get_value(this.nativePtr);
+ }
+
+ get fraction() {
+ return emsc.get_fraction(this.nativePtr);
+ }
+
+ get currency() {
+ return emsc.get_currency(this.nativePtr);
+ }
+
+ toJson() {
+ return {
+ value: emsc.get_value(this.nativePtr),
+ fraction: emsc.get_fraction(this.nativePtr),
+ currency: emsc.get_currency(this.nativePtr)
+ };
+ }
+
+ /**
+ * Add an amount to this amount.
+ */
+ add(a) {
+ let res = emsc.amount_add(this.nativePtr, a.nativePtr, this.nativePtr);
+ if (res < 1) {
+ // Overflow
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Perform saturating subtraction on amounts.
+ */
+ sub(a) {
+ // this = this - a
+ let res = emsc.amount_subtract(this.nativePtr, this.nativePtr, a.nativePtr);
+ if (res == 0) {
+ // Underflow
+ return false;
+ }
+ if (res > 0) {
+ return true;
+ }
+ throw Error("Incompatible currencies");
+ }
+
+ cmp(a) {
+ return emsc.amount_cmp(this.nativePtr, a.nativePtr);
+ }
+
+ normalize() {
+ emsc.amount_normalize(this.nativePtr);
+ }
+}
+
+
+abstract class PackedArenaObject extends ArenaObject {
+ abstract size(): number;
+
+ constructor(a?: Arena) {
+ super(a);
+ }
+
+ toCrock(): string {
+ var d = emscAlloc.data_to_string_alloc(this.nativePtr, this.size());
+ var s = Module.Pointer_stringify(d);
+ emsc.free(d);
+ return s;
+ }
+
+ toJson(): any {
+ // Per default, the json encoding of
+ // packed arena objects is just the crockford encoding.
+ // Subclasses typically want to override this.
+ return this.toCrock();
+ }
+
+ loadCrock(s: string) {
+ this.alloc();
+ // We need to get the javascript string
+ // to the emscripten heap first.
+ let buf = ByteArray.fromString(s);
+ let res = emsc.string_to_data(buf.nativePtr,
+ s.length,
+ this.nativePtr,
+ this.size());
+ buf.destroy();
+ if (res < 1) {
+ throw {error: "wrong encoding"};
+ }
+ }
+
+ alloc() {
+ if (this.nativePtr === null) {
+ this.nativePtr = emscAlloc.malloc(this.size());
+ }
+ }
+
+ destroy() {
+ emsc.free(this.nativePtr);
+ this.nativePtr = 0;
+ }
+
+ hash(): HashCode {
+ var x = new HashCode();
+ x.alloc();
+ emsc.hash(this.nativePtr, this.size(), x.nativePtr);
+ return x;
+ }
+
+ hexdump() {
+ let bytes: string[] = [];
+ for (let i = 0; i < this.size(); i++) {
+ let b = Module.getValue(this.getNative() + i, "i8");
+ b = (b + 256) % 256;
+ bytes.push("0".concat(b.toString(16)).slice(-2));
+ }
+ let lines = [];
+ for (let i = 0; i < bytes.length; i+=8) {
+ lines.push(bytes.slice(i, i+8).join(","));
+ }
+ return lines.join("\n");
+ }
+}
+
+
+export class AmountNbo extends PackedArenaObject {
+ size() {
+ return 24;
+ }
+ toJson(): any {
+ let a = new DefaultArena();
+ let am = new Amount(null, a);
+ am.fromNbo(this);
+ let json = am.toJson();
+ a.destroy();
+ return json;
+ }
+}
+
+
+export class EddsaPrivateKey extends PackedArenaObject {
+ static create(a?: Arena): EddsaPrivateKey {
+ let obj = new EddsaPrivateKey(a);
+ obj.nativePtr = emscAlloc.eddsa_key_create();
+ return obj;
+ }
+
+ size() {
+ return 32;
+ }
+
+ getPublicKey(a?: Arena): EddsaPublicKey {
+ let obj = new EddsaPublicKey(a);
+ obj.nativePtr = emscAlloc.eddsa_public_key_from_private(this.nativePtr);
+ return obj;
+ }
+
+ static fromCrock: (string) => EddsaPrivateKey;
+}
+mixinStatic(EddsaPrivateKey, fromCrock);
+
+
+function fromCrock(s: string) {
+ let x = new this();
+ x.alloc();
+ x.loadCrock(s);
+ return x;
+}
+
+
+function mixin(obj, method, name?: string) {
+ if (!name) {
+ name = method.name;
+ }
+ if (!name) {
+ throw Error("Mixin needs a name.");
+ }
+ obj.prototype[method.name] = method;
+}
+
+
+function mixinStatic(obj, method, name?: string) {
+ if (!name) {
+ name = method.name;
+ }
+ if (!name) {
+ throw Error("Mixin needs a name.");
+ }
+ obj[method.name] = method;
+}
+
+
+export class EddsaPublicKey extends PackedArenaObject {
+ size() {
+ return 32;
+ }
+ static fromCrock: (s: string) => EddsaPublicKey;
+}
+mixinStatic(EddsaPublicKey, fromCrock);
+
+function makeFromCrock(decodeFn: (p: number, s: number) => number) {
+ function fromCrock(s: string, a?: Arena) {
+ let obj = new this(a);
+ let buf = ByteArray.fromCrock(s);
+ obj.setNative(decodeFn(buf.getNative(),
+ buf.size()));
+ buf.destroy();
+ return obj;
+ }
+
+ return fromCrock;
+}
+
+function makeToCrock(encodeFn: (po: number, ps: number) => number): () => string {
+ function toCrock() {
+ let ptr = emscAlloc.malloc(PTR_SIZE);
+ let size = emscAlloc.rsa_blinding_key_encode(this.nativePtr, ptr);
+ let res = new ByteArray(size, Module.getValue(ptr, '*'));
+ let s = res.toCrock();
+ emsc.free(ptr);
+ res.destroy();
+ return s;
+ }
+ return toCrock;
+}
+
+export class RsaBlindingKey extends ArenaObject {
+ static create(len: number, a?: Arena) {
+ let o = new RsaBlindingKey(a);
+ o.nativePtr = emscAlloc.rsa_blinding_key_create(len);
+ return o;
+ }
+
+ static fromCrock: (s: string, a?: Arena) => RsaBlindingKey;
+ toCrock = makeToCrock(emscAlloc.rsa_blinding_key_encode);
+
+ destroy() {
+ // TODO
+ }
+}
+mixinStatic(RsaBlindingKey, makeFromCrock(emscAlloc.rsa_blinding_key_decode));
+
+
+export class HashCode extends PackedArenaObject {
+ size() {
+ return 64;
+ }
+
+ static fromCrock: (s: string) => HashCode;
+
+ random(qualStr: string) {
+ let qual: RandomQuality;
+ switch (qualStr) {
+ case "weak":
+ qual = RandomQuality.WEAK;
+ break;
+ case "strong":
+ case null:
+ case undefined:
+ qual = RandomQuality.STRONG;
+ break;
+ case "nonce":
+ qual = RandomQuality.NONCE;
+ break;
+ default:
+ throw Error(format("unknown crypto quality: {0}", qual));
+ }
+ this.alloc();
+ emsc.hash_create_random(qual, this.nativePtr);
+ }
+}
+mixinStatic(HashCode, fromCrock);
+
+
+export class ByteArray extends PackedArenaObject {
+ private allocatedSize: number;
+
+ size() {
+ return this.allocatedSize;
+ }
+
+ constructor(desiredSize: number, init: number, a?: Arena) {
+ super(a);
+ if (init === undefined || init === null) {
+ this.nativePtr = emscAlloc.malloc(desiredSize);
+ } else {
+ this.nativePtr = init;
+ }
+ this.allocatedSize = desiredSize;
+ }
+
+ static fromString(s: string, a?: Arena): ByteArray {
+ let hstr = emscAlloc.malloc(s.length + 1);
+ Module.writeStringToMemory(s, hstr);
+ return new ByteArray(s.length, hstr, a);
+ }
+
+ static fromCrock(s: string, a?: Arena): ByteArray {
+ let hstr = emscAlloc.malloc(s.length + 1);
+ Module.writeStringToMemory(s, hstr);
+ let decodedLen = Math.floor((s.length * 5) / 8);
+ let ba = new ByteArray(decodedLen, null, a);
+ let res = emsc.string_to_data(hstr, s.length, ba.nativePtr, decodedLen);
+ emsc.free(hstr);
+ if (res != GNUNET_OK) {
+ throw Error("decoding failed");
+ }
+ return ba;
+ }
+}
+
+
+export class EccSignaturePurpose extends PackedArenaObject {
+ size() {
+ return this.payloadSize + 8;
+ }
+
+ payloadSize: number;
+
+ constructor(purpose: SignaturePurpose,
+ payload: PackedArenaObject,
+ a?: Arena) {
+ super(a);
+ this.nativePtr = emscAlloc.purpose_create(purpose,
+ payload.nativePtr,
+ payload.size());
+ this.payloadSize = payload.size();
+ }
+}
+
+
+abstract class SignatureStruct {
+ abstract fieldTypes(): Array<any>;
+
+ abstract purpose(): SignaturePurpose;
+
+ private members: any = {};
+
+ constructor(x: { [name: string]: any }) {
+ for (let k in x) {
+ this.set(k, x[k]);
+ }
+ }
+
+ toPurpose(a?: Arena): EccSignaturePurpose {
+ let totalSize = 0;
+ for (let f of this.fieldTypes()) {
+ let name = f[0];
+ let member = this.members[name];
+ if (!member) {
+ throw Error(format("Member {0} not set", name));
+ }
+ totalSize += member.size();
+ }
+
+ let buf = emscAlloc.malloc(totalSize);
+ let ptr = buf;
+ for (let f of this.fieldTypes()) {
+ let name = f[0];
+ let member = this.members[name];
+ let size = member.size();
+ emsc.memmove(ptr, member.nativePtr, size);
+ ptr += size;
+ }
+ let ba = new ByteArray(totalSize, buf, a);
+ return new EccSignaturePurpose(this.purpose(), ba);
+ }
+
+
+ toJson() {
+ let res: any = {};
+ for (let f of this.fieldTypes()) {
+ let name = f[0];
+ let member = this.members[name];
+ if (!member) {
+ throw Error(format("Member {0} not set", name));
+ }
+ res[name] = member.toJson();
+ }
+ res["purpose"] = this.purpose();
+ return res;
+ }
+
+ protected set(name: string, value: PackedArenaObject) {
+ let typemap: any = {};
+ for (let f of this.fieldTypes()) {
+ typemap[f[0]] = f[1];
+ }
+ if (!(name in typemap)) {
+ throw Error(format("Key {0} not found", name));
+ }
+ if (!(value instanceof typemap[name])) {
+ throw Error(format("Wrong type for {0}", name));
+ }
+ this.members[name] = value;
+ }
+}
+
+
+// It's redundant, but more type safe.
+export interface WithdrawRequestPS_Args {
+ reserve_pub: EddsaPublicKey;
+ amount_with_fee: AmountNbo;
+ withdraw_fee: AmountNbo;
+ h_denomination_pub: HashCode;
+ h_coin_envelope: HashCode;
+}
+
+
+export class WithdrawRequestPS extends SignatureStruct {
+ constructor(w: WithdrawRequestPS_Args) {
+ super(w);
+ }
+
+ purpose() {
+ return SignaturePurpose.RESERVE_WITHDRAW;
+ }
+
+ fieldTypes() {
+ return [
+ ["reserve_pub", EddsaPublicKey],
+ ["amount_with_fee", AmountNbo],
+ ["withdraw_fee", AmountNbo],
+ ["h_denomination_pub", HashCode],
+ ["h_coin_envelope", HashCode]
+ ];
+ }
+}
+
+
+export class AbsoluteTimeNbo extends PackedArenaObject {
+ static fromTalerString(s: string): AbsoluteTimeNbo {
+ let x = new AbsoluteTimeNbo();
+ x.alloc();
+ let r = /Date\(([0-9]+)\)/;
+ let m = r.exec(s);
+ if (m.length != 2) {
+ throw Error();
+ }
+ let n = parseInt(m[1]) * 1000000;
+ // XXX: This only works up to 54 bit numbers.
+ set64(x.getNative(), n);
+ return x;
+ }
+
+ size() {
+ return 8;
+ }
+}
+
+
+// XXX: This only works up to 54 bit numbers.
+function set64(p: number, n: number) {
+ for (let i = 0; i < 8; ++i) {
+ Module.setValue(p + (7 - i), n & 0xFF, "i8");
+ n = Math.floor(n / 256);
+ }
+
+}
+
+
+export class UInt64 extends PackedArenaObject {
+ static fromNumber(n: number): UInt64 {
+ let x = new UInt64();
+ x.alloc();
+ set64(x.getNative(), n);
+ return x;
+ }
+
+ size() {
+ return 8;
+ }
+}
+
+
+// It's redundant, but more type safe.
+export interface DepositRequestPS_Args {
+ h_contract: HashCode;
+ h_wire: HashCode;
+ timestamp: AbsoluteTimeNbo;
+ refund_deadline: AbsoluteTimeNbo;
+ transaction_id: UInt64;
+ amount_with_fee: AmountNbo;
+ deposit_fee: AmountNbo;
+ merchant: EddsaPublicKey;
+ coin_pub: EddsaPublicKey;
+}
+
+
+export class DepositRequestPS extends SignatureStruct {
+ constructor(w: DepositRequestPS_Args) {
+ super(w);
+ }
+
+ purpose() {
+ return SignaturePurpose.WALLET_COIN_DEPOSIT;
+ }
+
+ fieldTypes() {
+ return [
+ ["h_contract", HashCode],
+ ["h_wire", HashCode],
+ ["timestamp", AbsoluteTimeNbo],
+ ["refund_deadline", AbsoluteTimeNbo],
+ ["transaction_id", UInt64],
+ ["amount_with_fee", AmountNbo],
+ ["deposit_fee", AmountNbo],
+ ["merchant", EddsaPublicKey],
+ ["coin_pub", EddsaPublicKey],
+ ];
+ }
+}
+
+
+interface Encodeable {
+ encode(arena?: Arena): ByteArray;
+}
+
+function makeEncode(encodeFn) {
+ function encode(arena?: Arena) {
+ let ptr = emscAlloc.malloc(PTR_SIZE);
+ let len = encodeFn(this.getNative(), ptr);
+ let res = new ByteArray(len, null, arena);
+ res.setNative(Module.getValue(ptr, '*'));
+ emsc.free(ptr);
+ return res;
+ }
+ return encode;
+}
+
+
+export class RsaPublicKey extends ArenaObject implements Encodeable {
+ static fromCrock: (s: string, a?: Arena) => RsaPublicKey;
+
+ toCrock() {
+ return this.encode().toCrock();
+ }
+
+ destroy() {
+ emsc.rsa_public_key_free(this.nativePtr);
+ this.nativePtr = 0;
+ }
+
+ encode: (arena?: Arena) => ByteArray;
+}
+mixinStatic(RsaPublicKey, makeFromCrock(emscAlloc.rsa_public_key_decode));
+mixin(RsaPublicKey, makeEncode(emscAlloc.rsa_public_key_encode));
+
+
+export class EddsaSignature extends PackedArenaObject {
+ size() {
+ return 64;
+ }
+}
+
+
+export class RsaSignature extends ArenaObject implements Encodeable{
+ static fromCrock: (s: string, a?: Arena) => RsaSignature;
+
+ encode: (arena?: Arena) => ByteArray;
+
+ destroy() {
+ emsc.rsa_signature_free(this.getNative());
+ this.setNative(0);
+ }
+}
+mixinStatic(RsaSignature, makeFromCrock(emscAlloc.rsa_signature_decode));
+mixin(RsaSignature, makeEncode(emscAlloc.rsa_signature_encode));
+
+
+export function rsaBlind(hashCode: HashCode,
+ blindingKey: RsaBlindingKey,
+ pkey: RsaPublicKey,
+ arena?: Arena): ByteArray {
+ let ptr = emscAlloc.malloc(PTR_SIZE);
+ let s = emscAlloc.rsa_blind(hashCode.nativePtr,
+ blindingKey.nativePtr,
+ pkey.nativePtr,
+ ptr);
+ return new ByteArray(s, Module.getValue(ptr, '*'), arena);
+}
+
+
+export function eddsaSign(purpose: EccSignaturePurpose,
+ priv: EddsaPrivateKey,
+ a?: Arena): EddsaSignature {
+ let sig = new EddsaSignature(a);
+ sig.alloc();
+ let res = emsc.eddsa_sign(priv.nativePtr, purpose.nativePtr, sig.nativePtr);
+ if (res < 1) {
+ throw Error("EdDSA signing failed");
+ }
+ return sig;
+}
+
+
+export function rsaUnblind(sig: RsaSignature,
+ bk: RsaBlindingKey,
+ pk: RsaPublicKey,
+ a?: Arena): RsaSignature {
+ let x = new RsaSignature(a);
+ x.nativePtr = emscAlloc.rsa_unblind(sig.nativePtr,
+ bk.nativePtr,
+ pk.nativePtr);
+ return x;
+}
diff --git a/extension/lib/wallet/http.ts b/extension/lib/wallet/http.ts
new file mode 100644
index 000000000..d132857b7
--- /dev/null
+++ b/extension/lib/wallet/http.ts
@@ -0,0 +1,85 @@
+/*
+ This file is part of TALER
+ (C) 2016 GNUnet e.V.
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ TALER; see the file COPYING. If not, If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Helpers for doing XMLHttpRequest-s that are based on ES6 promises.
+ * @module Http
+ * @author Florian Dold
+ */
+
+"use strict";
+
+
+
+export interface HttpResponse {
+ status: number;
+ responseText: string;
+}
+
+
+export class BrowserHttpLib {
+ req(method: string,
+ url: string|uri.URI,
+ options?: any): Promise<HttpResponse> {
+ let urlString: string;
+ if (url instanceof URI) {
+ urlString = url.href();
+ } else if (typeof url === "string") {
+ urlString = url;
+ }
+
+ return new Promise((resolve, reject) => {
+ let myRequest = new XMLHttpRequest();
+ myRequest.open(method, urlString);
+ if (options && options.req) {
+ myRequest.send(options.req);
+ } else {
+ myRequest.send();
+ }
+ myRequest.addEventListener("readystatechange", (e) => {
+ if (myRequest.readyState == XMLHttpRequest.DONE) {
+ let resp = {
+ status: myRequest.status,
+ responseText: myRequest.responseText
+ };
+ resolve(resp);
+ }
+ });
+ });
+ }
+
+
+ get(url: string|uri.URI) {
+ return this.req("get", url);
+ }
+
+
+ postJson(url: string|uri.URI, body) {
+ return this.req("post", url, {req: JSON.stringify(body)});
+ }
+
+
+ postForm(url: string|uri.URI, form) {
+ return this.req("post", url, {req: form});
+ }
+}
+
+
+export class RequestException {
+ constructor(detail) {
+
+ }
+} \ No newline at end of file
diff --git a/extension/lib/wallet/query.ts b/extension/lib/wallet/query.ts
new file mode 100644
index 000000000..c67ce0193
--- /dev/null
+++ b/extension/lib/wallet/query.ts
@@ -0,0 +1,283 @@
+/*
+ This file is part of TALER
+ (C) 2016 GNUnet e.V.
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ TALER; see the file COPYING. If not, If not, see <http://www.gnu.org/licenses/>
+ */
+
+/// <reference path="../decl/urijs/URIjs.d.ts" />
+
+
+/**
+ * Database query abstractions.
+ * @module Query
+ * @author Florian Dold
+ */
+
+"use strict";
+
+
+export function Query(db) {
+ return new QueryRoot(db);
+}
+
+
+abstract class QueryStreamBase {
+ abstract subscribe(f: (isDone: boolean, value: any) => void);
+
+ root: QueryRoot;
+
+ constructor(root: QueryRoot) {
+ this.root = root;
+ }
+
+ indexJoin(storeName: string, indexName: string, key: any): QueryStreamBase {
+ // join on the source relation's key, which may be
+ // a path or a transformer function
+ this.root.stores.add(storeName);
+ return new QueryStreamIndexJoin(this, storeName, indexName, key);
+ }
+
+ filter(f: (any) => boolean): QueryStreamBase {
+ return new QueryStreamFilter(this, f);
+ }
+
+ reduce(f, acc?): Promise<any> {
+ let leakedResolve;
+ let p = new Promise((resolve, reject) => {
+ leakedResolve = resolve;
+ });
+
+ this.subscribe((isDone, value) => {
+ if (isDone) {
+ leakedResolve(acc);
+ return;
+ }
+ acc = f(value, acc);
+ });
+
+ return Promise.resolve().then(() => this.root.finish().then(() => p));
+ }
+}
+
+
+class QueryStreamFilter extends QueryStreamBase {
+ s: QueryStreamBase;
+ filterFn;
+
+ constructor(s: QueryStreamBase, filterFn) {
+ super(s.root);
+ this.s = s;
+ this.filterFn = filterFn;
+ }
+
+ subscribe(f) {
+ this.s.subscribe((isDone, value) => {
+ if (isDone) {
+ f(true, undefined);
+ return;
+ }
+ if (this.filterFn(value)) {
+ f(false, value)
+ }
+ });
+ }
+}
+
+
+class QueryStreamIndexJoin extends QueryStreamBase {
+ s: QueryStreamBase;
+ storeName;
+ key;
+ indexName;
+
+ constructor(s, storeName: string, indexName: string, key: any) {
+ super(s.root);
+ this.s = s;
+ this.storeName = storeName;
+ this.key = key;
+ this.indexName = indexName;
+ }
+
+ subscribe(f) {
+ this.s.subscribe((isDone, value) => {
+ if (isDone) {
+ f(true, undefined);
+ return;
+ }
+ let s = this.root.tx.objectStore(this.storeName).index(this.indexName);
+ let req = s.openCursor(IDBKeyRange.only(this.key(value)));
+ req.onsuccess = () => {
+ let cursor = req.result;
+ if (cursor) {
+ f(false, [value, cursor.value]);
+ cursor.continue();
+ } else {
+ f(true, undefined);
+ }
+ }
+ });
+ }
+
+}
+
+
+class IterQueryStream extends QueryStreamBase {
+ private qr: QueryRoot;
+ private storeName;
+ private options;
+
+ constructor(qr, storeName, options?) {
+ super(qr);
+ this.qr = qr;
+ this.options = options;
+ this.storeName = storeName;
+ }
+
+ subscribe(f) {
+ function doIt() {
+ let s;
+ if (this.options && this.options.indexName) {
+ s = this.qr.tx.objectStore(this.storeName)
+ .index(this.options.indexName);
+ } else {
+ s = this.qr.tx.objectStore(this.storeName);
+ }
+ let kr = undefined;
+ if (this.options && ("only" in this.options)) {
+ kr = IDBKeyRange.only(this.options.only);
+ }
+ let req = s.openCursor(kr);
+ req.onsuccess = (e) => {
+ let cursor: IDBCursorWithValue = req.result;
+ if (cursor) {
+ f(false, cursor.value);
+ cursor.continue();
+ } else {
+ f(true, undefined);
+ }
+ }
+ }
+
+ this.qr.work.push(doIt.bind(this));
+ }
+}
+
+
+class QueryRoot {
+ work = [];
+ db: IDBDatabase;
+ tx: IDBTransaction;
+ stores = new Set();
+ kickoffPromise;
+
+ constructor(db) {
+ this.db = db;
+ }
+
+ iter(storeName): QueryStreamBase {
+ this.stores.add(storeName);
+ return new IterQueryStream(this, storeName);
+ }
+
+ iterOnly(storeName, key): QueryStreamBase {
+ this.stores.add(storeName);
+ return new IterQueryStream(this, storeName, {only: key});
+ }
+
+ iterIndex(storeName, indexName, key) {
+ this.stores.add(storeName);
+ return new IterQueryStream(this, storeName, {indexName: indexName});
+ }
+
+ put(storeName, val): QueryRoot {
+ this.stores.add(storeName);
+ function doPut() {
+ this.tx.objectStore(storeName).put(val);
+ }
+
+ this.work.push(doPut.bind(this));
+ return this;
+ }
+
+ putAll(storeName, iterable): QueryRoot {
+ this.stores.add(storeName);
+ function doPutAll() {
+ for (let obj of iterable) {
+ this.tx.objectStore(storeName).put(obj);
+ }
+ }
+
+ this.work.push(doPutAll.bind(this));
+ return this;
+ }
+
+ add(storeName, val): QueryRoot {
+ this.stores.add(storeName);
+ function doAdd() {
+ this.tx.objectStore(storeName).add(val);
+ }
+
+ this.work.push(doAdd.bind(this));
+ return this;
+ }
+
+ get(storeName, key): Promise<any> {
+ this.stores.add(storeName);
+ let leakedResolve;
+ let p = new Promise((resolve, reject) => {
+ leakedResolve = resolve;
+ });
+ if (!leakedResolve) {
+ // According to ES6 spec (paragraph 25.4.3.1), this can't happen.
+ throw Error("assertion failed");
+ }
+ function doGet() {
+ let req = this.tx.objectStore(storeName).get(key);
+ req.onsuccess = (r) => {
+ leakedResolve(req.result);
+ };
+ }
+
+ this.work.push(doGet.bind(this));
+ return Promise.resolve().then(() => {
+ return this.finish().then(() => p);
+ });
+ }
+
+ finish(): Promise<void> {
+ if (this.kickoffPromise) {
+ return this.kickoffPromise;
+ }
+ this.kickoffPromise = new Promise((resolve, reject) => {
+
+ this.tx = this.db.transaction(Array.from(this.stores), "readwrite");
+ this.tx.oncomplete = () => {
+ resolve();
+ };
+ for (let w of this.work) {
+ w();
+ }
+ });
+ return this.kickoffPromise;
+ }
+
+ delete(storeName: string, key): QueryRoot {
+ this.stores.add(storeName);
+ function doDelete() {
+ this.tx.objectStore(storeName).delete(key);
+ }
+
+ this.work.push(doDelete.bind(this));
+ return this;
+ }
+} \ No newline at end of file
diff --git a/extension/lib/wallet/timerThread.ts b/extension/lib/wallet/timerThread.ts
new file mode 100644
index 000000000..6635da009
--- /dev/null
+++ b/extension/lib/wallet/timerThread.ts
@@ -0,0 +1,10 @@
+/**
+ * This file should be used as a WebWorker.
+ * Background pages in the WebExtensions model do
+ * not allow to schedule callbacks that should be called
+ * after a timeout. We can emulate this with WebWorkers.
+ */
+
+onmessage = function(e) {
+ self.setInterval(() => postMessage(true, "timerThread"), e.data.interval);
+}; \ No newline at end of file
diff --git a/extension/lib/wallet/types.ts b/extension/lib/wallet/types.ts
new file mode 100644
index 000000000..33de0ffb9
--- /dev/null
+++ b/extension/lib/wallet/types.ts
@@ -0,0 +1,109 @@
+/*
+ This file is part of TALER
+ (C) 2016 GNUnet e.V.
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ TALER; see the file COPYING. If not, If not, see <http://www.gnu.org/licenses/>
+ */
+
+"use strict";
+
+// TODO: factor into multiple files
+
+export interface Mint {
+ baseUrl: string;
+ keys: Keys
+}
+
+export interface CoinWithDenom {
+ coin: Coin;
+ denom: Denomination;
+}
+
+export interface Keys {
+ denoms: Denomination[];
+}
+
+export interface Denomination {
+ value: AmountJson_interface;
+ denom_pub: string;
+ fee_withdraw: AmountJson_interface;
+ fee_deposit: AmountJson_interface;
+}
+
+export interface PreCoin {
+ coinPub: string;
+ coinPriv: string;
+ reservePub: string;
+ denomPub: string;
+ blindingKey: string;
+ withdrawSig: string;
+ coinEv: string;
+ mintBaseUrl: string;
+ coinValue: AmountJson_interface;
+}
+
+export interface Coin {
+ coinPub: string;
+ coinPriv: string;
+ denomPub: string;
+ denomSig: string;
+ currentAmount: AmountJson_interface;
+ mintBaseUrl: string;
+}
+
+
+export interface AmountJson_interface {
+ value: number;
+ fraction: number
+ currency: string;
+}
+
+export interface ConfirmReserveRequest {
+ /**
+ * Name of the form field for the amount.
+ */
+ field_amount;
+
+ /**
+ * Name of the form field for the reserve public key.
+ */
+ field_reserve_pub;
+
+ /**
+ * Name of the form field for the reserve public key.
+ */
+ field_mint;
+
+ /**
+ * The actual amount in string form.
+ * TODO: where is this format specified?
+ */
+ amount_str;
+
+ /**
+ * Target URL for the reserve creation request.
+ */
+ post_url;
+
+ /**
+ * Mint URL where the bank should create the reserve.
+ */
+ mint;
+}
+
+
+export interface ConfirmReserveResponse {
+ backlink: string;
+ success: boolean;
+ status: number;
+ text: string;
+} \ No newline at end of file
diff --git a/extension/lib/wallet/wallet.ts b/extension/lib/wallet/wallet.ts
new file mode 100644
index 000000000..46bae70a7
--- /dev/null
+++ b/extension/lib/wallet/wallet.ts
@@ -0,0 +1,697 @@
+/*
+ This file is part of TALER
+ (C) 2015 GNUnet e.V.
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ TALER; see the file COPYING. If not, If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * High-level wallet operations that should be indepentent from the underlying
+ * browser extension interface.
+ * @module Wallet
+ * @author Florian Dold
+ */
+
+import {Amount} from "./emscriptif"
+import {AmountJson_interface} from "./types";
+import {CoinWithDenom} from "./types";
+import {DepositRequestPS_Args} from "./emscriptif";
+import {HashCode} from "./emscriptif";
+import {EddsaPublicKey} from "./emscriptif";
+import {Coin} from "./types";
+import {AbsoluteTimeNbo} from "./emscriptif";
+import {UInt64} from "./emscriptif";
+import {DepositRequestPS} from "./emscriptif";
+import {eddsaSign} from "./emscriptif";
+import {EddsaPrivateKey} from "./emscriptif";
+import {ConfirmReserveRequest} from "./types";
+import {ConfirmReserveResponse} from "./types";
+import {RsaPublicKey} from "./emscriptif";
+import {Denomination} from "./types";
+import {RsaBlindingKey} from "./emscriptif";
+import {ByteArray} from "./emscriptif";
+import {rsaBlind} from "./emscriptif";
+import {WithdrawRequestPS} from "./emscriptif";
+import {PreCoin} from "./types";
+import {rsaUnblind} from "./emscriptif";
+import {RsaSignature} from "./emscriptif";
+import {Mint} from "./types";
+import {Checkable} from "./checkable";
+import {HttpResponse} from "./http";
+import {RequestException} from "./http";
+import {Query} from "./query";
+
+"use strict";
+
+@Checkable.Class
+class AmountJson {
+ @Checkable.Number
+ value: number;
+
+ @Checkable.Number
+ fraction: number;
+
+ @Checkable.String
+ currency: string;
+
+ static check: (v: any) => AmountJson;
+}
+
+
+@Checkable.Class
+class CoinPaySig {
+ @Checkable.String
+ coin_sig: string;
+
+ @Checkable.String
+ coin_pub: string;
+
+ @Checkable.String
+ ub_sig: string;
+
+ @Checkable.String
+ denom_pub: string;
+
+ @Checkable.Value(AmountJson)
+ f: AmountJson;
+
+ static check: (v: any) => CoinPaySig;
+}
+
+
+interface ConfirmPayRequest {
+ merchantPageUrl: string;
+ offer: Offer;
+}
+
+interface MintCoins {
+ [mintUrl: string]: CoinWithDenom[];
+}
+
+
+interface MintInfo {
+ master_pub: string;
+ url: string;
+}
+
+interface Offer {
+ contract: Contract;
+ sig: string;
+ H_contract: string;
+ pay_url: string;
+ exec_url: string;
+}
+
+interface Contract {
+ H_wire: string;
+ amount: AmountJson_interface;
+ auditors: string[];
+ expiry: string,
+ locations: string[];
+ max_fee: AmountJson_interface;
+ merchant: any;
+ merchant_pub: string;
+ mints: MintInfo[];
+ products: string[];
+ refund_deadline: string;
+ timestamp: string;
+ transaction_id: number;
+}
+
+
+interface CoinPaySig_interface {
+ coin_sig: string;
+ coin_pub: string;
+ ub_sig: string;
+ denom_pub: string;
+ f: AmountJson_interface;
+}
+
+
+interface Transaction {
+ contractHash: string;
+ contract: any;
+ payUrl: string;
+ payReq: any;
+}
+
+
+interface Reserve {
+ mint_base_url: string
+ reserve_priv: string;
+ reserve_pub: string;
+}
+
+
+interface PaymentResponse {
+ payUrl: string;
+ payReq: any;
+}
+
+
+export interface Badge {
+ setText(s: string): void;
+ setColor(c: string): void;
+}
+
+
+type PayCoinInfo = Array<{ updatedCoin: Coin, sig: CoinPaySig_interface }>;
+
+
+/**
+ * See http://api.taler.net/wallet.html#general
+ */
+function canonicalizeBaseUrl(url) {
+ let x = new URI(url);
+ if (!x.protocol()) {
+ x.protocol("https");
+ }
+ x.path(x.path() + "/").normalizePath();
+ x.fragment();
+ x.query();
+ return x.href()
+}
+
+
+interface HttpRequestLibrary {
+ req(method: string,
+ url: string|uri.URI,
+ options?: any): Promise<HttpResponse>;
+
+ get(url: string|uri.URI): Promise<HttpResponse>;
+
+ postJson(url: string|uri.URI, body): Promise<HttpResponse>;
+
+ postForm(url: string|uri.URI, form): Promise<HttpResponse>;
+}
+
+
+function copy(o) {
+ return JSON.parse(JSON.stringify(o));
+}
+
+
+function rankDenom(denom1: any, denom2: any) {
+ // Slow ... we should find a better way than to convert it evert time.
+ let v1 = new Amount(denom1.value);
+ let v2 = new Amount(denom2.value);
+ return (-1) * v1.cmp(v2);
+}
+
+
+export class Wallet {
+ private db: IDBDatabase;
+ private http: HttpRequestLibrary;
+ private badge: Badge;
+
+ constructor(db: IDBDatabase, http: HttpRequestLibrary, badge: Badge) {
+ this.db = db;
+ this.http = http;
+ this.badge = badge;
+ }
+
+ static signDeposit(offer: Offer,
+ cds: CoinWithDenom[]): PayCoinInfo {
+ let ret = [];
+ let amountSpent = Amount.getZero(cds[0].coin.currentAmount.currency);
+ let amountRemaining = new Amount(offer.contract.amount);
+ cds = copy(cds);
+ for (let cd of cds) {
+ let coinSpend;
+
+ if (amountRemaining.value == 0 && amountRemaining.fraction == 0) {
+ break;
+ }
+
+ if (amountRemaining.cmp(new Amount(cd.coin.currentAmount)) < 0) {
+ coinSpend = new Amount(amountRemaining.toJson());
+ } else {
+ coinSpend = new Amount(cd.coin.currentAmount);
+ }
+
+ amountSpent.add(coinSpend);
+ amountRemaining.sub(coinSpend);
+
+ let newAmount = new Amount(cd.coin.currentAmount);
+ newAmount.sub(coinSpend);
+ cd.coin.currentAmount = newAmount.toJson();
+
+ let args: DepositRequestPS_Args = {
+ h_contract: HashCode.fromCrock(offer.H_contract),
+ h_wire: HashCode.fromCrock(offer.contract.H_wire),
+ amount_with_fee: coinSpend.toNbo(),
+ coin_pub: EddsaPublicKey.fromCrock(cd.coin.coinPub),
+ deposit_fee: new Amount(cd.denom.fee_deposit).toNbo(),
+ merchant: EddsaPublicKey.fromCrock(offer.contract.merchant_pub),
+ refund_deadline: AbsoluteTimeNbo.fromTalerString(offer.contract.refund_deadline),
+ timestamp: AbsoluteTimeNbo.fromTalerString(offer.contract.timestamp),
+ transaction_id: UInt64.fromNumber(offer.contract.transaction_id),
+ };
+
+ let d = new DepositRequestPS(args);
+
+ let coinSig = eddsaSign(d.toPurpose(),
+ EddsaPrivateKey.fromCrock(cd.coin.coinPriv))
+ .toCrock();
+
+ let s: CoinPaySig_interface = {
+ coin_sig: coinSig,
+ coin_pub: cd.coin.coinPub,
+ ub_sig: cd.coin.denomSig,
+ denom_pub: cd.coin.denomPub,
+ f: coinSpend.toJson(),
+ };
+ ret.push({sig: s, updatedCoin: cd.coin});
+ }
+ return ret;
+ }
+
+
+ /**
+ * Get mints and associated coins that are still spendable,
+ * but only if the sum the coins' remaining value exceeds the payment amount.
+ * @param paymentAmount
+ * @param depositFeeLimit
+ * @param allowedMints
+ */
+ getPossibleMintCoins(paymentAmount: AmountJson_interface,
+ depositFeeLimit: AmountJson_interface,
+ allowedMints: MintInfo[]): Promise<MintCoins> {
+
+
+ let m: MintCoins = {};
+
+ function storeMintCoin(mc) {
+ let mint = mc[0];
+ let coin = mc[1];
+ let cd = {
+ coin: coin,
+ denom: mint.keys.denoms.find((e) => e.denom_pub === coin.denomPub)
+ };
+ if (!cd.denom) {
+ throw Error("denom not found (database inconsistent)");
+ }
+ let x = m[mint.baseUrl];
+ if (!x) {
+ m[mint.baseUrl] = [cd];
+ } else {
+ x.push(cd);
+ }
+ }
+
+ let ps = allowedMints.map((info) => {
+ return Query(this.db)
+ .iterIndex("mints", "pubKey", info.master_pub)
+ .indexJoin("coins", "mintBaseUrl", (mint) => mint.baseUrl)
+ .reduce(storeMintCoin);
+ });
+
+ return Promise.all(ps).then(() => {
+ let ret: MintCoins = {};
+
+ nextMint:
+ for (let key in m) {
+ let coins = m[key].map((x) => ({
+ a: new Amount(x.denom.fee_deposit),
+ c: x
+ }));
+ // Sort by ascending deposit fee
+ coins.sort((o1, o2) => o1.a.cmp(o2.a));
+ let maxFee = new Amount(depositFeeLimit);
+ let minAmount = new Amount(paymentAmount);
+ let accFee = new Amount(coins[0].c.denom.fee_deposit);
+ let accAmount = Amount.getZero(coins[0].c.coin.currentAmount.currency);
+ let usableCoins: CoinWithDenom[] = [];
+ nextCoin:
+ for (let i = 0; i < coins.length; i++) {
+ let coinAmount = new Amount(coins[i].c.coin.currentAmount);
+ let coinFee = coins[i].a;
+ if (coinAmount.cmp(coinFee) <= 0) {
+ continue nextCoin;
+ }
+ accFee.add(coinFee);
+ accAmount.add(coinAmount);
+ if (accFee.cmp(maxFee) >= 0) {
+ console.log("too much fees");
+ continue nextMint;
+ }
+ usableCoins.push(coins[i].c);
+ if (accAmount.cmp(minAmount) >= 0) {
+ ret[key] = usableCoins;
+ continue nextMint;
+ }
+ }
+ }
+ return ret;
+ });
+ }
+
+
+ executePay(offer: Offer,
+ payCoinInfo: PayCoinInfo,
+ merchantBaseUrl: string,
+ chosenMint: string): Promise<void> {
+ let payReq = {};
+ payReq["H_wire"] = offer.contract.H_wire;
+ payReq["H_contract"] = offer.H_contract;
+ payReq["transaction_id"] = offer.contract.transaction_id;
+ payReq["refund_deadline"] = offer.contract.refund_deadline;
+ payReq["mint"] = URI(chosenMint).href();
+ payReq["coins"] = payCoinInfo.map((x) => x.sig);
+ payReq["timestamp"] = offer.contract.timestamp;
+ let payUrl = URI(offer.pay_url).absoluteTo(merchantBaseUrl);
+ let t: Transaction = {
+ contractHash: offer.H_contract,
+ contract: offer.contract,
+ payUrl: payUrl.href(),
+ payReq: payReq
+ };
+
+ return Query(this.db)
+ .put("transactions", t)
+ .putAll("coins", payCoinInfo.map((pci) => pci.updatedCoin))
+ .finish();
+ }
+
+ confirmPay(offer: Offer, merchantPageUrl: string): Promise<any> {
+ return Promise.resolve().then(() => {
+ return this.getPossibleMintCoins(offer.contract.amount,
+ offer.contract.max_fee,
+ offer.contract.mints)
+ }).then((mcs) => {
+ if (Object.keys(mcs).length == 0) {
+ throw Error("Not enough coins.");
+ }
+ let mintUrl = Object.keys(mcs)[0];
+ let ds = Wallet.signDeposit(offer, mcs[mintUrl]);
+ return this.executePay(offer, ds, merchantPageUrl, mintUrl);
+ });
+ }
+
+ doPayment(H_contract): Promise<PaymentResponse> {
+ return Promise.resolve().then(() => {
+ return Query(this.db)
+ .get("transactions", H_contract)
+ .then((t) => {
+ if (!t) {
+ throw Error("contract not found");
+ }
+ let resp: PaymentResponse = {
+ payUrl: t.payUrl,
+ payReq: t.payReq
+ };
+ return resp;
+ });
+ });
+ }
+
+ confirmReserve(req: ConfirmReserveRequest): Promise<ConfirmReserveResponse> {
+ let reservePriv = EddsaPrivateKey.create();
+ let reservePub = reservePriv.getPublicKey();
+ let form = new FormData();
+ let now = (new Date()).toString();
+ form.append(req.field_amount, req.amount_str);
+ form.append(req.field_reserve_pub, reservePub.toCrock());
+ form.append(req.field_mint, req.mint);
+ // TODO: set bank-specified fields.
+ let mintBaseUrl = canonicalizeBaseUrl(req.mint);
+
+ return this.http.postForm(req.post_url, form)
+ .then((hresp) => {
+ let resp: ConfirmReserveResponse = {
+ status: hresp.status,
+ text: hresp.responseText,
+ success: undefined,
+ backlink: undefined
+ };
+ let reserveRecord = {
+ reserve_pub: reservePub.toCrock(),
+ reserve_priv: reservePriv.toCrock(),
+ mint_base_url: mintBaseUrl,
+ created: now,
+ last_query: null,
+ current_amount: null,
+ // XXX: set to actual amount
+ initial_amount: null
+ };
+
+ if (hresp.status != 200) {
+ resp.success = false;
+ return resp;
+ }
+
+ resp.success = true;
+ // We can't show the page directly, so
+ // we show some generic page from the wallet.
+ resp.backlink = null;
+ return Query(this.db)
+ .put("reserves", reserveRecord)
+ .finish()
+ .then(() => {
+ // Do this in the background
+ this.updateMintFromUrl(reserveRecord.mint_base_url)
+ .then((mint) =>
+ this.updateReserve(reservePub, mint)
+ .then((reserve) => this.depleteReserve(reserve,
+ mint))
+ );
+ return resp;
+ });
+ });
+ }
+
+ withdrawPrepare(denom: Denomination,
+ reserve: Reserve): Promise<PreCoin> {
+ let reservePriv = new EddsaPrivateKey();
+ reservePriv.loadCrock(reserve.reserve_priv);
+ let reservePub = new EddsaPublicKey();
+ reservePub.loadCrock(reserve.reserve_pub);
+ let denomPub = RsaPublicKey.fromCrock(denom.denom_pub);
+ let coinPriv = EddsaPrivateKey.create();
+ let coinPub = coinPriv.getPublicKey();
+ let blindingFactor = RsaBlindingKey.create(1024);
+ let pubHash: HashCode = coinPub.hash();
+ let ev: ByteArray = rsaBlind(pubHash, blindingFactor, denomPub);
+
+ if (!denom.fee_withdraw) {
+ throw Error("Field fee_withdraw missing");
+ }
+
+ let amountWithFee = new Amount(denom.value);
+ amountWithFee.add(new Amount(denom.fee_withdraw));
+ let withdrawFee = new Amount(denom.fee_withdraw);
+
+ // Signature
+ let withdrawRequest = new WithdrawRequestPS({
+ reserve_pub: reservePub,
+ amount_with_fee: amountWithFee.toNbo(),
+ withdraw_fee: withdrawFee.toNbo(),
+ h_denomination_pub: denomPub.encode().hash(),
+ h_coin_envelope: ev.hash()
+ });
+
+ var sig = eddsaSign(withdrawRequest.toPurpose(), reservePriv);
+
+ let preCoin: PreCoin = {
+ reservePub: reservePub.toCrock(),
+ blindingKey: blindingFactor.toCrock(),
+ coinPub: coinPub.toCrock(),
+ coinPriv: coinPriv.toCrock(),
+ denomPub: denomPub.encode().toCrock(),
+ mintBaseUrl: reserve.mint_base_url,
+ withdrawSig: sig.toCrock(),
+ coinEv: ev.toCrock(),
+ coinValue: denom.value
+ };
+
+ return Query(this.db).put("precoins", preCoin).finish().then(() => preCoin);
+ }
+
+
+ withdrawExecute(pc: PreCoin): Promise<Coin> {
+ return Query(this.db)
+ .get("reserves", pc.reservePub)
+ .then((r) => {
+ let wd: any = {};
+ wd.denom_pub = pc.denomPub;
+ wd.reserve_pub = pc.reservePub;
+ wd.reserve_sig = pc.withdrawSig;
+ wd.coin_ev = pc.coinEv;
+ let reqUrl = URI("reserve/withdraw").absoluteTo(r.mint_base_url);
+ return this.http.postJson(reqUrl, wd);
+ })
+ .then(resp => {
+ if (resp.status != 200) {
+ throw new RequestException({
+ hint: "Withdrawal failed",
+ status: resp.status
+ });
+ }
+ let r = JSON.parse(resp.responseText);
+ let denomSig = rsaUnblind(RsaSignature.fromCrock(r.ev_sig),
+ RsaBlindingKey.fromCrock(pc.blindingKey),
+ RsaPublicKey.fromCrock(pc.denomPub));
+ let coin: Coin = {
+ coinPub: pc.coinPub,
+ coinPriv: pc.coinPriv,
+ denomPub: pc.denomPub,
+ denomSig: denomSig.encode().toCrock(),
+ currentAmount: pc.coinValue,
+ mintBaseUrl: pc.mintBaseUrl,
+ };
+ return coin;
+ });
+ }
+
+
+ updateBadge() {
+ function countNonEmpty(c, n) {
+ if (c.currentAmount.fraction != 0 || c.currentAmount.value != 0) {
+ return n + 1;
+ }
+ return n;
+ }
+
+ function doBadge(n) {
+ this.badge.setText(n.toString());
+ this.badge.setColor("#0F0");
+ }
+
+ Query(this.db)
+ .iter("coins")
+ .reduce(countNonEmpty, 0)
+ .then(doBadge.bind(this));
+ }
+
+ storeCoin(coin: Coin) {
+ Query(this.db)
+ .delete("precoins", coin.coinPub)
+ .add("coins", coin)
+ .finish()
+ .then(() => {
+ this.updateBadge();
+ });
+ }
+
+ withdraw(denom, reserve): Promise<void> {
+ return this.withdrawPrepare(denom, reserve)
+ .then((pc) => this.withdrawExecute(pc))
+ .then((c) => this.storeCoin(c));
+ }
+
+
+ /**
+ * Withdraw coins from a reserve until it is empty.
+ */
+ depleteReserve(reserve, mint): void {
+ let denoms = copy(mint.keys.denoms);
+ let remaining = new Amount(reserve.current_amount);
+ denoms.sort(rankDenom);
+ let workList = [];
+ for (let i = 0; i < 1000; i++) {
+ let found = false;
+ for (let d of denoms) {
+ let cost = new Amount(d.value);
+ cost.add(new Amount(d.fee_withdraw));
+ if (remaining.cmp(cost) < 0) {
+ continue;
+ }
+ found = true;
+ remaining.sub(cost);
+ workList.push(d);
+ }
+ if (!found) {
+ console.log("did not find coins for remaining ", remaining.toJson());
+ break;
+ }
+ }
+
+ // Do the request one by one.
+ let next = () => {
+ if (workList.length == 0) {
+ return;
+ }
+ let d = workList.pop();
+ this.withdraw(d, reserve)
+ .then(() => next());
+ };
+
+ // Asynchronous recursion
+ next();
+ }
+
+ updateReserve(reservePub: EddsaPublicKey,
+ mint): Promise<Reserve> {
+ let reservePubStr = reservePub.toCrock();
+ return Query(this.db)
+ .get("reserves", reservePubStr)
+ .then((reserve) => {
+ let reqUrl = URI("reserve/status").absoluteTo(mint.baseUrl);
+ reqUrl.query({'reserve_pub': reservePubStr});
+ return this.http.get(reqUrl).then(resp => {
+ if (resp.status != 200) {
+ throw Error();
+ }
+ let reserveInfo = JSON.parse(resp.responseText);
+ if (!reserveInfo) {
+ throw Error();
+ }
+ reserve.current_amount = reserveInfo.balance;
+ return Query(this.db)
+ .put("reserves", reserve)
+ .finish()
+ .then(() => reserve);
+ });
+ });
+ }
+
+ /**
+ * Update or add mint DB entry by fetching the /keys information.
+ * Optionally link the reserve entry to the new or existing
+ * mint entry in then DB.
+ */
+ updateMintFromUrl(baseUrl) {
+ let reqUrl = URI("keys").absoluteTo(baseUrl);
+ return this.http.get(reqUrl).then((resp) => {
+ if (resp.status != 200) {
+ throw Error("/keys request failed");
+ }
+ let mintKeysJson = JSON.parse(resp.responseText);
+ if (!mintKeysJson) {
+ throw new RequestException({url: reqUrl, hint: "keys invalid"});
+ }
+ let mint: Mint = {
+ baseUrl: baseUrl,
+ keys: mintKeysJson
+ };
+ return Query(this.db).put("mints", mint).finish().then(() => mint);
+ });
+ }
+
+
+ getBalances(): Promise<any> {
+ function collectBalances(c: Coin, byCurrency) {
+ let acc: AmountJson_interface = byCurrency[c.currentAmount.currency];
+ if (!acc) {
+ acc = Amount.getZero(c.currentAmount.currency).toJson();
+ }
+ let am = new Amount(c.currentAmount);
+ am.add(new Amount(acc));
+ byCurrency[c.currentAmount.currency] = am.toJson();
+ return byCurrency;
+ }
+
+ return Query(this.db)
+ .iter("coins")
+ .reduce(collectBalances, {});
+ }
+}
diff --git a/extension/lib/wallet/wxmessaging.js b/extension/lib/wallet/wxmessaging.js
new file mode 100644
index 000000000..c656f2632
--- /dev/null
+++ b/extension/lib/wallet/wxmessaging.js
@@ -0,0 +1,144 @@
+/*
+ This file is part of TALER
+ (C) 2016 GNUnet e.V.
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ TALER; see the file COPYING. If not, If not, see <http://www.gnu.org/licenses/>
+ */
+System.register(["./wallet", "./db", "./http"], function(exports_1) {
+ "use strict";
+ var wallet_1, db_1, db_2, db_3, http_1;
+ var ChromeBadge;
+ function makeHandlers(wallet) {
+ return (_a = {},
+ _a["balances"] = function (db, detail, sendResponse) {
+ wallet.getBalances().then(sendResponse);
+ return true;
+ },
+ _a["dump-db"] = function (db, detail, sendResponse) {
+ db_1.exportDb(db).then(sendResponse);
+ return true;
+ },
+ _a["reset"] = function (db, detail, sendResponse) {
+ var tx = db.transaction(db.objectStoreNames, 'readwrite');
+ for (var i = 0; i < db.objectStoreNames.length; i++) {
+ tx.objectStore(db.objectStoreNames[i]).clear();
+ }
+ db_2.deleteDb();
+ chrome.browserAction.setBadgeText({ text: "" });
+ console.log("reset done");
+ // Response is synchronous
+ return false;
+ },
+ _a["confirm-reserve"] = function (db, detail, sendResponse) {
+ // TODO: make it a checkable
+ var req = {
+ field_amount: detail.field_amount,
+ field_mint: detail.field_mint,
+ field_reserve_pub: detail.field_reserve_pub,
+ post_url: detail.post_url,
+ mint: detail.mint,
+ amount_str: detail.amount_str
+ };
+ wallet.confirmReserve(req)
+ .then(function (resp) {
+ if (resp.success) {
+ resp.backlink = chrome.extension.getURL("pages/reserve-success.html");
+ }
+ sendResponse(resp);
+ });
+ return true;
+ },
+ _a["confirm-pay"] = function (db, detail, sendResponse) {
+ wallet.confirmPay(detail.offer, detail.merchantPageUrl)
+ .then(function () {
+ sendResponse({ success: true });
+ })
+ .catch(function (e) {
+ sendResponse({ error: e.message });
+ });
+ return true;
+ },
+ _a["execute-payment"] = function (db, detail, sendResponse) {
+ wallet.doPayment(detail.H_contract)
+ .then(function (r) {
+ sendResponse({
+ success: true,
+ payUrl: r.payUrl,
+ payReq: r.payReq
+ });
+ })
+ .catch(function (e) {
+ sendResponse({ success: false, error: e.message });
+ });
+ // async sendResponse
+ return true;
+ },
+ _a
+ );
+ var _a;
+ }
+ function wxMain() {
+ chrome.browserAction.setBadgeText({ text: "" });
+ db_3.openTalerDb().then(function (db) {
+ var http = new http_1.BrowserHttpLib();
+ var badge = new ChromeBadge();
+ var wallet = new wallet_1.Wallet(db, http, badge);
+ var handlers = makeHandlers(wallet);
+ wallet.updateBadge();
+ chrome.runtime.onMessage.addListener(function (req, sender, onresponse) {
+ if (req.type in handlers) {
+ return handlers[req.type](db, req.detail, onresponse);
+ }
+ console.error(format("Request type {1} unknown, req {0}", JSON.stringify(req), req.type));
+ return false;
+ });
+ });
+ }
+ exports_1("wxMain", wxMain);
+ return {
+ setters:[
+ function (wallet_1_1) {
+ wallet_1 = wallet_1_1;
+ },
+ function (db_1_1) {
+ db_1 = db_1_1;
+ db_2 = db_1_1;
+ db_3 = db_1_1;
+ },
+ function (http_1_1) {
+ http_1 = http_1_1;
+ }],
+ execute: function() {
+ /**
+ * Messaging for the WebExtensions wallet. Should contain
+ * parts that are specific for WebExtensions, but as little business
+ * logic as possible.
+ * @module Messaging
+ * @author Florian Dold
+ */
+ "use strict";
+ ChromeBadge = (function () {
+ function ChromeBadge() {
+ }
+ ChromeBadge.prototype.setText = function (s) {
+ chrome.browserAction.setBadgeText({ text: s });
+ };
+ ChromeBadge.prototype.setColor = function (c) {
+ chrome.browserAction.setBadgeBackgroundColor({ color: c });
+ };
+ return ChromeBadge;
+ }());
+ wxMain();
+ }
+ }
+});
+//# sourceMappingURL=wxmessaging.js.map \ No newline at end of file
diff --git a/extension/lib/wallet/wxmessaging.ts b/extension/lib/wallet/wxmessaging.ts
new file mode 100644
index 000000000..1b345e22f
--- /dev/null
+++ b/extension/lib/wallet/wxmessaging.ts
@@ -0,0 +1,138 @@
+/*
+ This file is part of TALER
+ (C) 2016 GNUnet e.V.
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ TALER; see the file COPYING. If not, If not, see <http://www.gnu.org/licenses/>
+ */
+
+
+import {ConfirmReserveRequest} from "./types";
+import {Wallet} from "./wallet";
+import {exportDb} from "./db";
+import {deleteDb} from "./db";
+import {openTalerDb} from "./db";
+import {BrowserHttpLib} from "./http";
+import {Badge} from "./wallet";
+/**
+ * Messaging for the WebExtensions wallet. Should contain
+ * parts that are specific for WebExtensions, but as little business
+ * logic as possible.
+ * @module Messaging
+ * @author Florian Dold
+ */
+
+"use strict";
+
+function makeHandlers(wallet) {
+ return {
+ ["balances"]: function(db, detail, sendResponse) {
+ wallet.getBalances().then(sendResponse);
+ return true;
+ },
+ ["dump-db"]: function(db, detail, sendResponse) {
+ exportDb(db).then(sendResponse);
+ return true;
+ },
+ ["reset"]: function(db, detail, sendResponse) {
+ let tx = db.transaction(db.objectStoreNames, 'readwrite');
+ for (let i = 0; i < db.objectStoreNames.length; i++) {
+ tx.objectStore(db.objectStoreNames[i]).clear();
+ }
+ deleteDb();
+
+ chrome.browserAction.setBadgeText({text: ""});
+ console.log("reset done");
+ // Response is synchronous
+ return false;
+ },
+ ["confirm-reserve"]: function(db, detail, sendResponse) {
+ // TODO: make it a checkable
+ let req: ConfirmReserveRequest = {
+ field_amount: detail.field_amount,
+ field_mint: detail.field_mint,
+ field_reserve_pub: detail.field_reserve_pub,
+ post_url: detail.post_url,
+ mint: detail.mint,
+ amount_str: detail.amount_str
+ };
+ wallet.confirmReserve(req)
+ .then((resp) => {
+ if (resp.success) {
+ resp.backlink = chrome.extension.getURL(
+ "pages/reserve-success.html");
+ }
+ sendResponse(resp);
+ });
+ return true;
+ },
+ ["confirm-pay"]: function(db, detail, sendResponse) {
+ wallet.confirmPay(detail.offer, detail.merchantPageUrl)
+ .then(() => {
+ sendResponse({success: true})
+ })
+ .catch((e) => {
+ sendResponse({error: e.message});
+ });
+ return true;
+ },
+ ["execute-payment"]: function(db, detail, sendResponse) {
+ wallet.doPayment(detail.H_contract)
+ .then((r) => {
+ sendResponse({
+ success: true,
+ payUrl: r.payUrl,
+ payReq: r.payReq
+ });
+ })
+ .catch((e) => {
+ sendResponse({success: false, error: e.message});
+ });
+ // async sendResponse
+ return true;
+ }
+ };
+}
+
+class ChromeBadge implements Badge {
+ setText(s: string) {
+ chrome.browserAction.setBadgeText({text: s});
+ }
+
+ setColor(c: string) {
+ chrome.browserAction.setBadgeBackgroundColor({color: c});
+ }
+}
+
+
+export function wxMain() {
+ chrome.browserAction.setBadgeText({text: ""});
+
+ openTalerDb().then((db) => {
+ let http = new BrowserHttpLib();
+ let badge = new ChromeBadge();
+ let wallet = new Wallet(db, http, badge);
+ let handlers = makeHandlers(wallet);
+ wallet.updateBadge();
+ chrome.runtime.onMessage.addListener(
+ function(req, sender, onresponse) {
+ if (req.type in handlers) {
+ return handlers[req.type](db, req.detail, onresponse);
+ }
+ console.error(format("Request type {1} unknown, req {0}",
+ JSON.stringify(req),
+ req.type));
+ return false;
+ });
+ });
+}
+
+wxMain(); \ No newline at end of file