diff options
author | Florian Dold <florian.dold@gmail.com> | 2016-01-10 20:07:42 +0100 |
---|---|---|
committer | Florian Dold <florian.dold@gmail.com> | 2016-01-10 20:07:42 +0100 |
commit | 473503a246aa7a23539a349710c5549d1d87c147 (patch) | |
tree | 2b1ee9b420df72a0d861759d1fbcd2226e489086 /extension/lib/wallet | |
parent | dd19e0ecbe114ebd71122ff57ea56eabb6258b75 (diff) | |
download | wallet-core-473503a246aa7a23539a349710c5549d1d87c147.tar.xz |
The great modularization.
Use ES6 module syntax and SystemJS modules for everything.
Some testing stubs were added as well.
Diffstat (limited to 'extension/lib/wallet')
-rw-r--r-- | extension/lib/wallet/checkable.ts | 136 | ||||
-rw-r--r-- | extension/lib/wallet/db.ts | 97 | ||||
-rw-r--r-- | extension/lib/wallet/emscriptif.ts | 938 | ||||
-rw-r--r-- | extension/lib/wallet/http.ts | 85 | ||||
-rw-r--r-- | extension/lib/wallet/query.ts | 283 | ||||
-rw-r--r-- | extension/lib/wallet/timerThread.ts | 10 | ||||
-rw-r--r-- | extension/lib/wallet/types.ts | 109 | ||||
-rw-r--r-- | extension/lib/wallet/wallet.ts | 697 | ||||
-rw-r--r-- | extension/lib/wallet/wxmessaging.js | 144 | ||||
-rw-r--r-- | extension/lib/wallet/wxmessaging.ts | 138 |
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 |