From f3fb8be7db6de87dae40d41bd5597a735c800ca1 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Sun, 13 Nov 2016 23:30:18 +0100 Subject: restructuring --- lib/wallet/checkable.ts | 262 ------- lib/wallet/chromeBadge.ts | 227 ------ lib/wallet/cryptoApi-test.ts | 79 -- lib/wallet/cryptoApi.ts | 256 ------- lib/wallet/cryptoLib.ts | 346 --------- lib/wallet/cryptoWorker.ts | 61 -- lib/wallet/db.ts | 117 --- lib/wallet/emscriptif-test.ts | 21 - lib/wallet/emscriptif.ts | 1245 ------------------------------- lib/wallet/helpers.ts | 140 ---- lib/wallet/http.ts | 97 --- lib/wallet/query.ts | 612 --------------- lib/wallet/renderHtml.tsx | 63 -- lib/wallet/types-test.ts | 38 - lib/wallet/types.ts | 554 -------------- lib/wallet/wallet.ts | 1657 ----------------------------------------- lib/wallet/wxApi.ts | 75 -- lib/wallet/wxMessaging.ts | 439 ----------- 18 files changed, 6289 deletions(-) delete mode 100644 lib/wallet/checkable.ts delete mode 100644 lib/wallet/chromeBadge.ts delete mode 100644 lib/wallet/cryptoApi-test.ts delete mode 100644 lib/wallet/cryptoApi.ts delete mode 100644 lib/wallet/cryptoLib.ts delete mode 100644 lib/wallet/cryptoWorker.ts delete mode 100644 lib/wallet/db.ts delete mode 100644 lib/wallet/emscriptif-test.ts delete mode 100644 lib/wallet/emscriptif.ts delete mode 100644 lib/wallet/helpers.ts delete mode 100644 lib/wallet/http.ts delete mode 100644 lib/wallet/query.ts delete mode 100644 lib/wallet/renderHtml.tsx delete mode 100644 lib/wallet/types-test.ts delete mode 100644 lib/wallet/types.ts delete mode 100644 lib/wallet/wallet.ts delete mode 100644 lib/wallet/wxApi.ts delete mode 100644 lib/wallet/wxMessaging.ts (limited to 'lib/wallet') diff --git a/lib/wallet/checkable.ts b/lib/wallet/checkable.ts deleted file mode 100644 index 89d0c7150..000000000 --- a/lib/wallet/checkable.ts +++ /dev/null @@ -1,262 +0,0 @@ -/* - 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, see - */ - - -"use strict"; - -/** - * Decorators for type-checking JSON into - * an object. - * @module Checkable - * @author Florian Dold - */ - -export namespace Checkable { - - type Path = (number | string)[]; - - interface SchemaErrorConstructor { - new (err: string): SchemaError; - } - - interface SchemaError { - name: string; - message: string; - } - - interface Prop { - propertyKey: any; - checker: any; - type: any; - elementChecker?: any; - elementProp?: any; - } - - export let SchemaError = (function SchemaError(message: string) { - this.name = 'SchemaError'; - this.message = message; - this.stack = (new Error()).stack; - }) as any as SchemaErrorConstructor; - - - SchemaError.prototype = new Error; - - let chkSym = Symbol("checkable"); - - - function checkNumber(target: any, prop: Prop, path: Path): any { - if ((typeof target) !== "number") { - throw new SchemaError(`expected number for ${path}`); - } - return target; - } - - - function checkString(target: any, prop: Prop, path: Path): any { - if (typeof target !== "string") { - throw new SchemaError(`expected string for ${path}, got ${typeof target} instead`); - } - return target; - } - - - function checkAnyObject(target: any, prop: Prop, path: Path): any { - if (typeof target !== "object") { - throw new SchemaError(`expected (any) object for ${path}, got ${typeof target} instead`); - } - return target; - } - - - function checkAny(target: any, prop: Prop, path: Path): any { - return target; - } - - - function checkList(target: any, prop: Prop, path: Path): any { - if (!Array.isArray(target)) { - throw new SchemaError(`array expected for ${path}, got ${typeof target} instead`); - } - for (let i = 0; i < target.length; i++) { - let v = target[i]; - prop.elementChecker(v, prop.elementProp, path.concat([i])); - } - return target; - } - - - function checkOptional(target: any, prop: Prop, path: Path): any { - console.assert(prop.propertyKey); - prop.elementChecker(target, - prop.elementProp, - path.concat([prop.propertyKey])); - return target; - } - - - function checkValue(target: any, prop: Prop, path: Path): any { - let type = prop.type; - if (!type) { - throw Error(`assertion failed (prop is ${JSON.stringify(prop)})`); - } - let v = target; - if (!v || typeof v !== "object") { - throw new SchemaError( - `expected object for ${path.join(".")}, got ${typeof v} instead`); - } - 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)) { - if (prop.optional) { - continue; - } - throw new SchemaError("Property missing: " + prop.propertyKey); - } - if (!remainingPropNames.delete(prop.propertyKey)) { - throw new SchemaError("assertion failed"); - } - let propVal = v[prop.propertyKey]; - obj[prop.propertyKey] = prop.checker(propVal, - prop, - path.concat([prop.propertyKey])); - } - - if (remainingPropNames.size != 0) { - throw new SchemaError("superfluous properties " + JSON.stringify(Array.from( - remainingPropNames.values()))); - } - return obj; - } - - - export function Class(target: any) { - target.checked = (v: any) => { - return checkValue(v, { - propertyKey: "(root)", - type: target, - checker: checkValue - }, ["(root)"]); - }; - return target; - } - - - export function Value(type: any) { - if (!type) { - throw Error("Type does not exist yet (wrong order of definitions?)"); - } - 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: any) { - let stub = {}; - type(stub, "(list-element)"); - let elementProp = mkChk(stub).props[0]; - let elementChecker = elementProp.checker; - if (!elementChecker) { - throw Error("assertion failed"); - } - function deco(target: Object, propertyKey: string | symbol): void { - let chk = mkChk(target); - chk.props.push({ - elementChecker, - elementProp, - propertyKey: propertyKey, - checker: checkList, - }); - } - - return deco; - } - - - export function Optional(type: any) { - let stub = {}; - type(stub, "(optional-element)"); - let elementProp = mkChk(stub).props[0]; - let elementChecker = elementProp.checker; - if (!elementChecker) { - throw Error("assertion failed"); - } - function deco(target: Object, propertyKey: string | symbol): void { - let chk = mkChk(target); - chk.props.push({ - elementChecker, - elementProp, - propertyKey: propertyKey, - checker: checkOptional, - optional: true, - }); - } - - 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 Any(target: Object, - propertyKey: string | symbol): void { - let chk = mkChk(target); - chk.props.push({ - propertyKey: propertyKey, - checker: checkAny, - optional: true - }); - } - - - export function String(target: Object, propertyKey: string | symbol): void { - let chk = mkChk(target); - chk.props.push({ propertyKey: propertyKey, checker: checkString }); - } - - - function mkChk(target: any) { - let chk = target[chkSym]; - if (!chk) { - chk = { props: [] }; - target[chkSym] = chk; - } - return chk; - } -} \ No newline at end of file diff --git a/lib/wallet/chromeBadge.ts b/lib/wallet/chromeBadge.ts deleted file mode 100644 index df12fba83..000000000 --- a/lib/wallet/chromeBadge.ts +++ /dev/null @@ -1,227 +0,0 @@ -/* - This file is part of TALER - (C) 2016 INRIA - - 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, see - */ - -import { - Badge -} from "./wallet"; - - -/** - * Polyfill for requestAnimationFrame, which - * doesn't work from a background page. - */ -function rAF(cb: (ts: number) => void) { - window.setTimeout(() => { - cb(performance.now()); - }, 100 /* 100 ms delay between frames */); -} - - -export class ChromeBadge implements Badge { - canvas: HTMLCanvasElement; - ctx: CanvasRenderingContext2D; - /** - * True if animation running. The animation - * might still be running even if we're not busy anymore, - * just to transition to the "normal" state in a animated way. - */ - animationRunning: boolean = false; - - /** - * Is the wallet still busy? Note that we do not stop the - * animation immediately when the wallet goes idle, but - * instead slowly close the gap. - */ - isBusy: boolean = false; - - /** - * Current rotation angle, ranges from 0 to rotationAngleMax. - */ - rotationAngle: number = 0; - - /** - * While animating, how wide is the current gap in the circle? - * Ranges from 0 to openMax. - */ - gapWidth: number = 0; - - /** - * Maximum value for our rotationAngle, corresponds to 2 Pi. - */ - static rotationAngleMax = 1000; - - /** - * How fast do we rotate? Given in rotation angle (relative to rotationAngleMax) per millisecond. - */ - static rotationSpeed = 0.5; - - /** - * How fast to we open? Given in rotation angle (relative to rotationAngleMax) per millisecond. - */ - static openSpeed = 0.15; - - /** - * How fast to we close? Given as a multiplication factor per frame update. - */ - static closeSpeed = 0.7; - - /** - * How far do we open? Given relative to rotationAngleMax. - */ - static openMax = 100; - - constructor(window?: Window) { - // Allow injecting another window for testing - let bg = window || chrome.extension.getBackgroundPage(); - if (!bg) { - throw Error("no window available"); - } - this.canvas = bg.document.createElement("canvas"); - // Note: changing the width here means changing the font - // size in draw() as well! - this.canvas.width = 32; - this.canvas.height = 32; - this.ctx = this.canvas.getContext("2d")!; - this.draw(); - } - - /** - * Draw the badge based on the current state. - */ - private draw() { - this.ctx.setTransform(1, 0, 0, 1, 0, 0); - this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); - - this.ctx.translate(this.canvas.width / 2, this.canvas.height / 2); - - this.ctx.beginPath(); - this.ctx.arc(0, 0, this.canvas.width / 2 - 2, 0, 2 * Math.PI); - this.ctx.fillStyle = "white"; - this.ctx.fill(); - - // move into the center, off by 2 for aligning the "T" with the bottom - // of the circle. - this.ctx.translate(0, 2); - - // pick sans-serif font; note: 14px is based on the 32px width above! - this.ctx.font = "bold 24px sans-serif"; - // draw the "T" perfectly centered (x and y) to the current position - this.ctx.textAlign = "center"; - this.ctx.textBaseline = "middle"; - this.ctx.fillStyle = "black"; - this.ctx.fillText("T", 0, 0); - // now move really into the center - this.ctx.translate(0, -2); - // start drawing the (possibly open) circle - this.ctx.beginPath(); - this.ctx.lineWidth = 2.5; - if (this.animationRunning) { - /* Draw circle around the "T" with an opening of this.gapWidth */ - this.ctx.arc(0, 0, - this.canvas.width / 2 - 2, /* radius */ - this.rotationAngle / ChromeBadge.rotationAngleMax * Math.PI * 2, - ((this.rotationAngle + ChromeBadge.rotationAngleMax - this.gapWidth) / ChromeBadge.rotationAngleMax) * Math.PI * 2, - false); - } - else { - /* Draw full circle */ - this.ctx.arc(0, 0, - this.canvas.width / 2 - 2, /* radius */ - 0, - Math.PI * 2, - false); - } - this.ctx.stroke(); - // go back to the origin - this.ctx.translate(-this.canvas.width / 2, -this.canvas.height / 2); - - // Allow running outside the extension for testing - if (window["chrome"] && window.chrome["browserAction"]) { - let imageData = this.ctx.getImageData(0, - 0, - this.canvas.width, - this.canvas.height); - chrome.browserAction.setIcon({imageData}); - } - } - - private animate() { - if (this.animationRunning) { - return; - } - this.animationRunning = true; - let start: number|undefined = undefined; - let step = (timestamp: number) => { - if (!this.animationRunning) { - return; - } - if (!start) { - start = timestamp; - } - let delta = (timestamp - start); - if (!this.isBusy && 0 == this.gapWidth) { - // stop if we're close enough to origin - this.rotationAngle = 0; - } else { - this.rotationAngle = (this.rotationAngle + (timestamp - start) * ChromeBadge.rotationSpeed) % ChromeBadge.rotationAngleMax; - } - if (this.isBusy) { - if (this.gapWidth < ChromeBadge.openMax) { - this.gapWidth += ChromeBadge.openSpeed * (timestamp - start); - } - if (this.gapWidth > ChromeBadge.openMax) { - this.gapWidth = ChromeBadge.openMax; - } - } - else { - if (this.gapWidth > 0) { - this.gapWidth--; - this.gapWidth *= ChromeBadge.closeSpeed; - } - } - - - if (this.isBusy || this.gapWidth > 0) { - start = timestamp; - rAF(step); - } else { - this.animationRunning = false; - } - this.draw(); - }; - rAF(step); - } - - setText(s: string) { - chrome.browserAction.setBadgeText({text: s}); - } - - setColor(c: string) { - chrome.browserAction.setBadgeBackgroundColor({color: c}); - } - - startBusy() { - if (this.isBusy) { - return; - } - this.isBusy = true; - this.animate(); - } - - stopBusy() { - this.isBusy = false; - } -} diff --git a/lib/wallet/cryptoApi-test.ts b/lib/wallet/cryptoApi-test.ts deleted file mode 100644 index 38ecdb634..000000000 --- a/lib/wallet/cryptoApi-test.ts +++ /dev/null @@ -1,79 +0,0 @@ -import {CryptoApi} from "./cryptoApi"; -import {ReserveRecord, Denomination} from "lib/wallet/types"; -import {test, TestLib} from "testlib/talertest"; - -let masterPub1: string = "CQQZ9DY3MZ1ARMN5K1VKDETS04Y2QCKMMCFHZSWJWWVN82BTTH00"; - -let denomValid1: Denomination = { - "master_sig": "CJFJCQ48Q45PSGJ5KY94N6M2TPARESM2E15BSPBD95YVVPEARAEQ6V6G4Z2XBMS0QM0F3Y9EYVP276FCS90EQ1578ZC8JHFBZ3NGP3G", - "stamp_start": "/Date(1473148381)/", - "stamp_expire_withdraw": "/Date(2482300381)/", - "stamp_expire_deposit": "/Date(1851580381)/", - "denom_pub": "51R7ARKCD5HJTTV5F4G0M818E9SP280A40G2GVH04CR30GHS84R3JHHP6GSM2D9Q6514CGT568R32C9J6CWM4DSH64TM4DSM851K0CA48CVKAC1P6H144C2160T46DHK8CVM4HJ274S38C1M6S338D9N6GWM8DT684T3JCT36S13EC9G88R3EGHQ8S0KJGSQ60SKGD216N33AGJ2651K2E9S60TMCD1N75244HHQ6X33EDJ570R3GGJ2651MACA38D130DA560VK4HHJ68WK2CA26GW3ECSH6D13EC9S88VK2GT66WVK8D9G750K0D9R8RRK4DHQ71332GHK8D23GE26710M2H9K6WVK8HJ38MVKEGA66N23AC9H88VKACT58MV3CCSJ6H1K4DT38GRK0C9M8N33CE1R60V4AHA38H1KECSH6S33JH9N8GRKGH1K68S36GH354520818CMG26C1H60R30C935452081918G2J2G0", - "stamp_expire_legal": "/Date(1567756381)/", - "value": { - "currency": "PUDOS", - "value": 0, - "fraction": 100000 - }, - "fee_withdraw": { - "currency": "PUDOS", - "value": 0, - "fraction": 10000 - }, - "fee_deposit": { - "currency": "PUDOS", - "value": 0, - "fraction": 10000 - }, - "fee_refresh": { - "currency": "PUDOS", - "value": 0, - "fraction": 10000 - }, - "fee_refund": { - "currency": "PUDOS", - "value": 0, - "fraction": 10000 - } -}; - -let denomInvalid1 = JSON.parse(JSON.stringify(denomValid1)); -denomInvalid1.value.value += 1; - -test("string hashing", async (t: TestLib) => { - let crypto = new CryptoApi(); - let s = await crypto.hashString("hello taler"); - let sh = "8RDMADB3YNF3QZBS3V467YZVJAMC2QAQX0TZGVZ6Q5PFRRAJFT70HHN0QF661QR9QWKYMMC7YEMPD679D2RADXCYK8Y669A2A5MKQFR"; - t.assert(s == sh); - t.pass(); -}); - -test("precoin creation", async (t: TestLib) => { - let crypto = new CryptoApi(); - let {priv, pub} = await crypto.createEddsaKeypair(); - let r: ReserveRecord = { - reserve_pub: pub, - reserve_priv: priv, - exchange_base_url: "https://example.com/exchange", - created: 0, - requested_amount: {currency: "PUDOS", value: 0, fraction: 0}, - precoin_amount: {currency: "PUDOS", value: 0, fraction: 0}, - current_amount: null, - confirmed: false, - last_query: null, - }; - - let precoin = await crypto.createPreCoin(denomValid1, r); - t.pass(); -}); - -test("denom validation", async (t: TestLib) => { - let crypto = new CryptoApi(); - let v: boolean; - v = await crypto.isValidDenom(denomValid1, masterPub1); - t.assert(v); - v = await crypto.isValidDenom(denomInvalid1, masterPub1); - t.assert(!v); - t.pass(); -}); diff --git a/lib/wallet/cryptoApi.ts b/lib/wallet/cryptoApi.ts deleted file mode 100644 index 31407b74a..000000000 --- a/lib/wallet/cryptoApi.ts +++ /dev/null @@ -1,256 +0,0 @@ -/* - 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, see - */ - - -/** - * API to access the Taler crypto worker thread. - * @author Florian Dold - */ - - -import {PreCoin, Coin, ReserveRecord, AmountJson} from "./types"; -import {Denomination} from "./types"; -import {Offer} from "./wallet"; -import {CoinWithDenom} from "./wallet"; -import {PayCoinInfo} from "./types"; -import {RefreshSession} from "./types"; - - -interface WorkerState { - /** - * The actual worker thread. - */ - w: Worker|null; - - /** - * Work we're currently executing or null if not busy. - */ - currentWorkItem: WorkItem|null; - - /** - * Timer to terminate the worker if it's not busy enough. - */ - terminationTimerHandle: number|null; -} - -interface WorkItem { - operation: string; - args: any[]; - resolve: any; - reject: any; - - /** - * Serial id to identify a matching response. - */ - rpcId: number; -} - - -/** - * Number of different priorities. Each priority p - * must be 0 <= p < NUM_PRIO. - */ -const NUM_PRIO = 5; - -export class CryptoApi { - private nextRpcId: number = 1; - private workers: WorkerState[]; - private workQueues: WorkItem[][]; - /** - * Number of busy workers. - */ - private numBusy: number = 0; - - /** - * Start a worker (if not started) and set as busy. - */ - wake(ws: WorkerState, work: WorkItem): void { - if (ws.currentWorkItem != null) { - throw Error("assertion failed"); - } - ws.currentWorkItem = work; - this.numBusy++; - if (!ws.w) { - let w = new Worker("/lib/wallet/cryptoWorker.js"); - w.onmessage = (m: MessageEvent) => this.handleWorkerMessage(ws, m); - w.onerror = (e: ErrorEvent) => this.handleWorkerError(ws, e); - ws.w = w; - } - - let msg: any = { - operation: work.operation, args: work.args, - id: work.rpcId - }; - this.resetWorkerTimeout(ws); - ws.w!.postMessage(msg); - } - - resetWorkerTimeout(ws: WorkerState) { - if (ws.terminationTimerHandle != null) { - clearTimeout(ws.terminationTimerHandle); - } - let destroy = () => { - // terminate worker if it's idle - if (ws.w && ws.currentWorkItem == null) { - ws.w!.terminate(); - ws.w = null; - } - }; - ws.terminationTimerHandle = setTimeout(destroy, 20 * 1000); - } - - handleWorkerError(ws: WorkerState, e: ErrorEvent) { - if (ws.currentWorkItem) { - console.error(`error in worker during ${ws.currentWorkItem!.operation}`, - e); - } else { - console.error("error in worker", e); - } - console.error(e.message); - try { - ws.w!.terminate(); - ws.w = null; - } catch (e) { - console.error(e); - } - if (ws.currentWorkItem != null) { - ws.currentWorkItem.reject(e); - ws.currentWorkItem = null; - this.numBusy--; - } - this.findWork(ws); - } - - findWork(ws: WorkerState) { - // try to find more work for this worker - for (let i = 0; i < NUM_PRIO; i++) { - let q = this.workQueues[NUM_PRIO - i - 1]; - if (q.length != 0) { - let work: WorkItem = q.shift()!; - this.wake(ws, work); - return; - } - } - } - - handleWorkerMessage(ws: WorkerState, msg: MessageEvent) { - let id = msg.data.id; - if (typeof id !== "number") { - console.error("rpc id must be number"); - return; - } - let currentWorkItem = ws.currentWorkItem; - ws.currentWorkItem = null; - this.numBusy--; - this.findWork(ws); - if (!currentWorkItem) { - console.error("unsolicited response from worker"); - return; - } - if (id != currentWorkItem.rpcId) { - console.error(`RPC with id ${id} has no registry entry`); - return; - } - currentWorkItem.resolve(msg.data.result); - } - - constructor() { - this.workers = new Array((navigator as any)["hardwareConcurrency"] || 2); - - for (let i = 0; i < this.workers.length; i++) { - this.workers[i] = { - w: null, - terminationTimerHandle: null, - currentWorkItem: null, - }; - } - this.workQueues = []; - for (let i = 0; i < NUM_PRIO; i++) { - this.workQueues.push([]); - } - } - - private doRpc(operation: string, priority: number, - ...args: any[]): Promise { - - return new Promise((resolve, reject) => { - let rpcId = this.nextRpcId++; - let workItem: WorkItem = {operation, args, resolve, reject, rpcId}; - - if (this.numBusy == this.workers.length) { - let q = this.workQueues[priority]; - if (!q) { - throw Error("assertion failed"); - } - this.workQueues[priority].push(workItem); - return; - } - - for (let i = 0; i < this.workers.length; i++) { - let ws = this.workers[i]; - if (ws.currentWorkItem != null) { - continue; - } - - this.wake(ws, workItem); - return; - } - - throw Error("assertion failed"); - }); - } - - - createPreCoin(denom: Denomination, reserve: ReserveRecord): Promise { - return this.doRpc("createPreCoin", 1, denom, reserve); - } - - hashString(str: string): Promise { - return this.doRpc("hashString", 1, str); - } - - isValidDenom(denom: Denomination, - masterPub: string): Promise { - return this.doRpc("isValidDenom", 2, denom, masterPub); - } - - signDeposit(offer: Offer, - cds: CoinWithDenom[]): Promise { - return this.doRpc("signDeposit", 3, offer, cds); - } - - createEddsaKeypair(): Promise<{priv: string, pub: string}> { - return this.doRpc("createEddsaKeypair", 1); - } - - rsaUnblind(sig: string, bk: string, pk: string): Promise { - return this.doRpc("rsaUnblind", 4, sig, bk, pk); - } - - createRefreshSession(exchangeBaseUrl: string, - kappa: number, - meltCoin: Coin, - newCoinDenoms: Denomination[], - meltFee: AmountJson): Promise { - return this.doRpc("createRefreshSession", - 4, - exchangeBaseUrl, - kappa, - meltCoin, - newCoinDenoms, - meltFee); - } -} diff --git a/lib/wallet/cryptoLib.ts b/lib/wallet/cryptoLib.ts deleted file mode 100644 index 6cb5b79d1..000000000 --- a/lib/wallet/cryptoLib.ts +++ /dev/null @@ -1,346 +0,0 @@ -/* - 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, see - */ - -/** - * Web worker for crypto operations. - * @author Florian Dold - */ - -"use strict"; - -import * as native from "./emscriptif"; -import { - PreCoin, PayCoinInfo, AmountJson, - RefreshSession, RefreshPreCoin, ReserveRecord -} from "./types"; -import create = chrome.alarms.create; -import {Offer} from "./wallet"; -import {CoinWithDenom} from "./wallet"; -import {CoinPaySig} from "./types"; -import {Denomination, Amounts} from "./types"; -import {Amount} from "./emscriptif"; -import {Coin} from "../../background/lib/wallet/types"; -import {HashContext} from "./emscriptif"; -import {RefreshMeltCoinAffirmationPS} from "./emscriptif"; -import {EddsaPublicKey} from "./emscriptif"; -import {HashCode} from "./emscriptif"; - - -export function main(worker: Worker) { - worker.onmessage = (msg: MessageEvent) => { - if (!Array.isArray(msg.data.args)) { - console.error("args must be array"); - return; - } - if (typeof msg.data.id != "number") { - console.error("RPC id must be number"); - } - if (typeof msg.data.operation != "string") { - console.error("RPC operation must be string"); - } - let f = (RpcFunctions as any)[msg.data.operation]; - if (!f) { - console.error(`unknown operation: '${msg.data.operation}'`); - return; - } - let res = f(...msg.data.args); - worker.postMessage({result: res, id: msg.data.id}); - } -} - - -namespace RpcFunctions { - - /** - * Create a pre-coin of the given denomination to be withdrawn from then given - * reserve. - */ - export function createPreCoin(denom: Denomination, - reserve: ReserveRecord): PreCoin { - let reservePriv = new native.EddsaPrivateKey(); - reservePriv.loadCrock(reserve.reserve_priv); - let reservePub = new native.EddsaPublicKey(); - reservePub.loadCrock(reserve.reserve_pub); - let denomPub = native.RsaPublicKey.fromCrock(denom.denom_pub); - let coinPriv = native.EddsaPrivateKey.create(); - let coinPub = coinPriv.getPublicKey(); - let blindingFactor = native.RsaBlindingKeySecret.create(); - let pubHash: native.HashCode = coinPub.hash(); - let ev = native.rsaBlind(pubHash, - blindingFactor, - denomPub); - - if (!ev) { - throw Error("couldn't blind (malicious exchange key?)"); - } - - if (!denom.fee_withdraw) { - throw Error("Field fee_withdraw missing"); - } - - let amountWithFee = new native.Amount(denom.value); - amountWithFee.add(new native.Amount(denom.fee_withdraw)); - let withdrawFee = new native.Amount(denom.fee_withdraw); - - // Signature - let withdrawRequest = new native.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 = native.eddsaSign(withdrawRequest.toPurpose(), reservePriv); - - let preCoin: PreCoin = { - reservePub: reservePub.toCrock(), - blindingKey: blindingFactor.toCrock(), - coinPub: coinPub.toCrock(), - coinPriv: coinPriv.toCrock(), - denomPub: denomPub.encode().toCrock(), - exchangeBaseUrl: reserve.exchange_base_url, - withdrawSig: sig.toCrock(), - coinEv: ev.toCrock(), - coinValue: denom.value - }; - return preCoin; - } - - - export function isValidDenom(denom: Denomination, - masterPub: string): boolean { - let p = new native.DenominationKeyValidityPS({ - master: native.EddsaPublicKey.fromCrock(masterPub), - denom_hash: native.RsaPublicKey.fromCrock(denom.denom_pub) - .encode() - .hash(), - expire_legal: native.AbsoluteTimeNbo.fromTalerString(denom.stamp_expire_legal), - expire_spend: native.AbsoluteTimeNbo.fromTalerString(denom.stamp_expire_deposit), - expire_withdraw: native.AbsoluteTimeNbo.fromTalerString(denom.stamp_expire_withdraw), - start: native.AbsoluteTimeNbo.fromTalerString(denom.stamp_start), - value: (new native.Amount(denom.value)).toNbo(), - fee_deposit: (new native.Amount(denom.fee_deposit)).toNbo(), - fee_refresh: (new native.Amount(denom.fee_refresh)).toNbo(), - fee_withdraw: (new native.Amount(denom.fee_withdraw)).toNbo(), - fee_refund: (new native.Amount(denom.fee_refund)).toNbo(), - }); - - let nativeSig = new native.EddsaSignature(); - nativeSig.loadCrock(denom.master_sig); - - let nativePub = native.EddsaPublicKey.fromCrock(masterPub); - - return native.eddsaVerify(native.SignaturePurpose.MASTER_DENOMINATION_KEY_VALIDITY, - p.toPurpose(), - nativeSig, - nativePub); - - } - - - export function createEddsaKeypair(): {priv: string, pub: string} { - const priv = native.EddsaPrivateKey.create(); - const pub = priv.getPublicKey(); - return {priv: priv.toCrock(), pub: pub.toCrock()}; - } - - - export function rsaUnblind(sig: string, bk: string, pk: string): string { - let denomSig = native.rsaUnblind(native.RsaSignature.fromCrock(sig), - native.RsaBlindingKeySecret.fromCrock(bk), - native.RsaPublicKey.fromCrock(pk)); - return denomSig.encode().toCrock() - } - - - /** - * Generate updated coins (to store in the database) - * and deposit permissions for each given coin. - */ - export function signDeposit(offer: Offer, - cds: CoinWithDenom[]): PayCoinInfo { - let ret: PayCoinInfo = []; - let amountSpent = native.Amount.getZero(cds[0].coin.currentAmount.currency); - let amountRemaining = new native.Amount(offer.contract.amount); - for (let cd of cds) { - let coinSpend: Amount; - - if (amountRemaining.value == 0 && amountRemaining.fraction == 0) { - break; - } - - if (amountRemaining.cmp(new native.Amount(cd.coin.currentAmount)) < 0) { - coinSpend = new native.Amount(amountRemaining.toJson()); - } else { - coinSpend = new native.Amount(cd.coin.currentAmount); - } - - amountSpent.add(coinSpend); - amountRemaining.sub(coinSpend); - - let newAmount = new native.Amount(cd.coin.currentAmount); - newAmount.sub(coinSpend); - cd.coin.currentAmount = newAmount.toJson(); - cd.coin.dirty = true; - cd.coin.transactionPending = true; - - let d = new native.DepositRequestPS({ - h_contract: native.HashCode.fromCrock(offer.H_contract), - h_wire: native.HashCode.fromCrock(offer.contract.H_wire), - amount_with_fee: coinSpend.toNbo(), - coin_pub: native.EddsaPublicKey.fromCrock(cd.coin.coinPub), - deposit_fee: new native.Amount(cd.denom.fee_deposit).toNbo(), - merchant: native.EddsaPublicKey.fromCrock(offer.contract.merchant_pub), - refund_deadline: native.AbsoluteTimeNbo.fromTalerString(offer.contract.refund_deadline), - timestamp: native.AbsoluteTimeNbo.fromTalerString(offer.contract.timestamp), - transaction_id: native.UInt64.fromNumber(offer.contract.transaction_id), - }); - - let coinSig = native.eddsaSign(d.toPurpose(), - native.EddsaPrivateKey.fromCrock(cd.coin.coinPriv)) - .toCrock(); - - let s: CoinPaySig = { - 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; - } - - - export function createRefreshSession(exchangeBaseUrl: string, - kappa: number, - meltCoin: Coin, - newCoinDenoms: Denomination[], - meltFee: AmountJson): RefreshSession { - - let valueWithFee = Amounts.getZero(newCoinDenoms[0].value.currency); - - for (let ncd of newCoinDenoms) { - valueWithFee = Amounts.add(valueWithFee, - ncd.value, - ncd.fee_withdraw).amount; - } - - // melt fee - valueWithFee = Amounts.add(valueWithFee, meltFee).amount; - - let sessionHc = new HashContext(); - - let transferPubs: string[] = []; - let transferPrivs: string[] = []; - - let preCoinsForGammas: RefreshPreCoin[][] = []; - - for (let i = 0; i < kappa; i++) { - let t = native.EcdhePrivateKey.create(); - let pub = t.getPublicKey(); - sessionHc.read(pub); - transferPrivs.push(t.toCrock()); - transferPubs.push(pub.toCrock()); - } - - for (let i = 0; i < newCoinDenoms.length; i++) { - let r = native.RsaPublicKey.fromCrock(newCoinDenoms[i].denom_pub); - sessionHc.read(r.encode()); - } - - sessionHc.read(native.EddsaPublicKey.fromCrock(meltCoin.coinPub)); - sessionHc.read((new native.Amount(valueWithFee)).toNbo()); - - for (let i = 0; i < kappa; i++) { - let preCoins: RefreshPreCoin[] = []; - for (let j = 0; j < newCoinDenoms.length; j++) { - - let transferPriv = native.EcdhePrivateKey.fromCrock(transferPrivs[i]); - let oldCoinPub = native.EddsaPublicKey.fromCrock(meltCoin.coinPub); - let transferSecret = native.ecdhEddsa(transferPriv, oldCoinPub); - - let fresh = native.setupFreshCoin(transferSecret, j); - - let coinPriv = fresh.priv; - let coinPub = coinPriv.getPublicKey(); - let blindingFactor = fresh.blindingKey; - let pubHash: native.HashCode = coinPub.hash(); - let denomPub = native.RsaPublicKey.fromCrock(newCoinDenoms[j].denom_pub); - let ev = native.rsaBlind(pubHash, - blindingFactor, - denomPub); - if (!ev) { - throw Error("couldn't blind (malicious exchange key?)"); - } - let preCoin: RefreshPreCoin = { - blindingKey: blindingFactor.toCrock(), - coinEv: ev.toCrock(), - publicKey: coinPub.toCrock(), - privateKey: coinPriv.toCrock(), - }; - preCoins.push(preCoin); - sessionHc.read(ev); - } - preCoinsForGammas.push(preCoins); - } - - let sessionHash = new HashCode(); - sessionHash.alloc(); - sessionHc.finish(sessionHash); - - let confirmData = new RefreshMeltCoinAffirmationPS({ - coin_pub: EddsaPublicKey.fromCrock(meltCoin.coinPub), - amount_with_fee: (new Amount(valueWithFee)).toNbo(), - session_hash: sessionHash, - melt_fee: (new Amount(meltFee)).toNbo() - }); - - - let confirmSig: string = native.eddsaSign(confirmData.toPurpose(), - native.EddsaPrivateKey.fromCrock( - meltCoin.coinPriv)).toCrock(); - - let valueOutput = Amounts.getZero(newCoinDenoms[0].value.currency); - for (let denom of newCoinDenoms) { - valueOutput = Amounts.add(valueOutput, denom.value).amount; - } - - let refreshSession: RefreshSession = { - meltCoinPub: meltCoin.coinPub, - newDenoms: newCoinDenoms.map((d) => d.denom_pub), - confirmSig, - valueWithFee, - transferPubs, - preCoinsForGammas, - hash: sessionHash.toCrock(), - norevealIndex: undefined, - exchangeBaseUrl, - transferPrivs, - finished: false, - valueOutput, - }; - - return refreshSession; - } - - export function hashString(str: string): string { - const b = native.ByteArray.fromStringWithNull(str); - return b.hash().toCrock(); - } -} diff --git a/lib/wallet/cryptoWorker.ts b/lib/wallet/cryptoWorker.ts deleted file mode 100644 index 0689c910e..000000000 --- a/lib/wallet/cryptoWorker.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - 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, see - */ - -/** - * Web worker for crypto operations. - * @author Florian Dold - */ - -"use strict"; - - -importScripts("../emscripten/taler-emscripten-lib.js", - "../vendor/system-csp-production.src.js"); - - -// TypeScript does not allow ".js" extensions in the -// module name, so SystemJS must add it. -System.config({ - defaultJSExtensions: true, - }); - -// We expect that in the manifest, the emscripten js is loaded -// becore the background page. -// Currently it is not possible to use SystemJS to load the emscripten js. -declare var Module: any; -if ("object" !== typeof Module) { - throw Error("emscripten not loaded, no 'Module' defined"); -} - - -// Manually register the emscripten js as a SystemJS, so that -// we can use it from TypeScript by importing it. - -{ - let mod = System.newModule({Module: Module, default: Module}); - let modName = System.normalizeSync("../emscripten/taler-emscripten-lib"); - console.log("registering", modName); - System.set(modName, mod); -} - -System.import("./cryptoLib") - .then((m) => { - m.main(self); - }) - .catch((e) => { - console.log("crypto worker failed"); - console.error(e.stack); - }); diff --git a/lib/wallet/db.ts b/lib/wallet/db.ts deleted file mode 100644 index 9cffc164c..000000000 --- a/lib/wallet/db.ts +++ /dev/null @@ -1,117 +0,0 @@ -/* - 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, see - */ - -"use strict"; -import {IExchangeInfo} from "./types"; - -/** - * 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 = 11; - -import {Stores} from "./wallet"; -import {Store, Index} from "./query"; - - - - - -/** - * Return a promise that resolves - * to the taler wallet db. - */ -export function openTalerDb(): Promise { - return new Promise((resolve, reject) => { - const req = indexedDB.open(DB_NAME, DB_VERSION); - req.onerror = (e) => { - reject(e); - }; - req.onsuccess = (e) => { - resolve(req.result); - }; - req.onupgradeneeded = (e) => { - const db = req.result; - console.log("DB: upgrade needed: oldVersion = " + e.oldVersion); - switch (e.oldVersion) { - case 0: // DB does not exist yet - - for (let n in Stores) { - if ((Stores as any)[n] instanceof Store) { - let si: Store = (Stores as any)[n]; - const s = db.createObjectStore(si.name, si.storeParams); - for (let indexName in (si as any)) { - if ((si as any)[indexName] instanceof Index) { - let ii: Index = (si as any)[indexName]; - s.createIndex(ii.indexName, ii.keyPath); - } - } - } - } - break; - default: - if (e.oldVersion != DB_VERSION) { - window.alert("Incompatible wallet dababase version, please reset" + - " db."); - chrome.browserAction.setBadgeText({text: "err"}); - chrome.browserAction.setBadgeBackgroundColor({color: "#F00"}); - throw Error("incompatible DB"); - } - break; - } - }; - }); -} - - -export function exportDb(db: IDBDatabase): Promise { - let dump = { - name: db.name, - version: db.version, - stores: {} as {[s: string]: any}, - }; - - return new Promise((resolve, reject) => { - - let tx = db.transaction(Array.from(db.objectStoreNames)); - tx.addEventListener("complete", () => { - resolve(dump); - }); - for (let i = 0; i < db.objectStoreNames.length; i++) { - let name = db.objectStoreNames[i]; - let storeDump = {} as {[s: string]: any}; - dump.stores[name] = storeDump; - let store = tx.objectStore(name) - .openCursor() - .addEventListener("success", (e: Event) => { - let cursor = (e.target as any).result; - if (cursor) { - storeDump[cursor.key] = cursor.value; - cursor.continue(); - } - }); - } - }); -} - -export function deleteDb() { - indexedDB.deleteDatabase(DB_NAME); -} diff --git a/lib/wallet/emscriptif-test.ts b/lib/wallet/emscriptif-test.ts deleted file mode 100644 index ddafa32bc..000000000 --- a/lib/wallet/emscriptif-test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import {test, TestLib} from "testlib/talertest"; -import * as native from "./emscriptif"; - -test("string hashing", (t: TestLib) => { - let x = native.ByteArray.fromStringWithNull("hello taler"); - let h = "8RDMADB3YNF3QZBS3V467YZVJAMC2QAQX0TZGVZ6Q5PFRRAJFT70HHN0QF661QR9QWKYMMC7YEMPD679D2RADXCYK8Y669A2A5MKQFR" - let hc = x.hash().toCrock(); - console.log(`# hc ${hc}`); - t.assert(h === hc, "must equal"); - t.pass(); -}); - -test("signing", (t: TestLib) => { - let x = native.ByteArray.fromStringWithNull("hello taler"); - let priv = native.EddsaPrivateKey.create(); - let pub = priv.getPublicKey(); - let purpose = new native.EccSignaturePurpose(native.SignaturePurpose.TEST, x); - let sig = native.eddsaSign(purpose, priv); - t.assert(native.eddsaVerify(native.SignaturePurpose.TEST, purpose, sig, pub)); - t.pass(); -}); diff --git a/lib/wallet/emscriptif.ts b/lib/wallet/emscriptif.ts deleted file mode 100644 index b26f35ebd..000000000 --- a/lib/wallet/emscriptif.ts +++ /dev/null @@ -1,1245 +0,0 @@ -/* - 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, see - */ - -import {AmountJson} from "./types"; -import Module, {EmscFunGen} from "../emscripten/taler-emscripten-lib"; - -/** - * High-level interface to emscripten-compiled modules used - * by the wallet. - * - * @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; - - -const getEmsc: EmscFunGen = (name: string, ret: any, argTypes: any[]) => { - return (...args: any[]) => { - return Module.ccall(name, ret, argTypes, args); - } -}; - - -/** - * Wrapped emscripten functions that do not allocate any memory. - */ -const emsc = { - free: (ptr: number) => 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"]), - eddsa_verify: getEmsc("GNUNET_CRYPTO_eddsa_verify", - "number", - ["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"]), - random_block: getEmsc("GNUNET_CRYPTO_random_block", - "void", - ["number", "number", "number"]), - hash_context_abort: getEmsc("GNUNET_CRYPTO_hash_context_abort", - "void", - ["number"]), - hash_context_read: getEmsc("GNUNET_CRYPTO_hash_context_read", - "void", - ["number", "number", "number"]), - hash_context_finish: getEmsc("GNUNET_CRYPTO_hash_context_finish", - "void", - ["number", "number"]), - ecdh_eddsa: getEmsc("GNUNET_CRYPTO_ecdh_eddsa", - "number", - ["number", "number", "number"]), - - setup_fresh_coin: getEmsc( - "TALER_setup_fresh_coin", - "void", - ["number", "number", "number"]), -}; - -const emscAlloc = { - get_amount: getEmsc("TALER_WRALL_get_amount", - "number", - ["number", "number", "number", "string"]), - eddsa_key_create: getEmsc("GNUNET_CRYPTO_eddsa_key_create", - "number", []), - ecdsa_key_create: getEmsc("GNUNET_CRYPTO_ecdsa_key_create", - "number", []), - ecdhe_key_create: getEmsc("GNUNET_CRYPTO_ecdhe_key_create", - "number", []), - eddsa_public_key_from_private: getEmsc( - "TALER_WRALL_eddsa_public_key_from_private", - "number", - ["number"]), - ecdsa_public_key_from_private: getEmsc( - "TALER_WRALL_ecdsa_public_key_from_private", - "number", - ["number"]), - ecdhe_public_key_from_private: getEmsc( - "TALER_WRALL_ecdhe_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", "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"]), - hash_context_start: getEmsc("GNUNET_CRYPTO_hash_context_start", - "number", - []), - malloc: (size: number) => Module._malloc(size), -}; - - -export enum SignaturePurpose { - RESERVE_WITHDRAW = 1200, - WALLET_COIN_DEPOSIT = 1201, - MASTER_DENOMINATION_KEY_VALIDITY = 1025, - WALLET_COIN_MELT = 1202, - TEST = 4242, -} - -export enum RandomQuality { - WEAK = 0, - STRONG = 1, - NONCE = 2 -} - -interface ArenaObject { - destroy(): void; -} - - -export class HashContext implements ArenaObject { - private hashContextPtr: number | undefined; - - constructor() { - this.hashContextPtr = emscAlloc.hash_context_start(); - } - - read(obj: PackedArenaObject): void { - if (!this.hashContextPtr) { - throw Error("assertion failed"); - } - emsc.hash_context_read(this.hashContextPtr, obj.nativePtr, obj.size()); - } - - finish(h: HashCode) { - if (!this.hashContextPtr) { - throw Error("assertion failed"); - } - h.alloc(); - emsc.hash_context_finish(this.hashContextPtr, h.nativePtr); - } - - destroy(): void { - if (this.hashContextPtr) { - emsc.hash_context_abort(this.hashContextPtr); - } - this.hashContextPtr = undefined; - } -} - - -abstract class MallocArenaObject implements ArenaObject { - protected _nativePtr: number | undefined = undefined; - - /** - * Is this a weak reference to the underlying memory? - */ - isWeak = false; - arena: Arena; - - destroy(): void { - if (this._nativePtr && !this.isWeak) { - emsc.free(this.nativePtr); - this._nativePtr = undefined; - } - } - - constructor(arena?: Arena) { - if (!arena) { - if (arenaStack.length == 0) { - throw Error("No arena available") - } - arena = arenaStack[arenaStack.length - 1]; - } - arena.put(this); - this.arena = arena; - } - - alloc(size: number) { - if (this._nativePtr !== undefined) { - throw Error("Double allocation"); - } - this.nativePtr = emscAlloc.malloc(size); - } - - set nativePtr(v: number) { - if (v === undefined) { - throw Error("Native pointer must be a number or null"); - } - this._nativePtr = v; - } - - get nativePtr() { - // 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; - } -} - - -interface Arena { - put(obj: ArenaObject): void; - destroy(): void; -} - - -/** - * Arena that must be manually destroyed. - */ -class SimpleArena implements Arena { - heap: Array; - - constructor() { - this.heap = []; - } - - put(obj: ArenaObject) { - this.heap.push(obj); - } - - destroy() { - for (let obj of this.heap) { - obj.destroy(); - } - this.heap = [] - } -} - - -/** - * Arena that destroys all its objects once control has returned to the message - * loop. - */ -class SyncArena extends SimpleArena { - private isScheduled: boolean; - - constructor() { - super(); - } - - pub(obj: MallocArenaObject) { - super.put(obj); - if (!this.isScheduled) { - this.schedule(); - } - this.heap.push(obj); - } - - private schedule() { - this.isScheduled = true; - Promise.resolve().then(() => { - this.isScheduled = false; - this.destroy(); - }); - } -} - -let arenaStack: Arena[] = []; -arenaStack.push(new SyncArena()); - - -export class Amount extends MallocArenaObject { - constructor(args?: AmountJson, 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, ""); - } - } - - static getZero(currency: string, a?: Arena): Amount { - let am = new Amount(undefined, a); - let r = emsc.amount_get_zero(currency, am.nativePtr); - 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(): String { - return emsc.get_currency(this.nativePtr); - } - - toJson(): AmountJson { - 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: Amount) { - 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: Amount) { - // 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: Amount) { - // If we don't check this, the c code aborts. - if (this.currency !== a.currency) { - throw Error(`incomparable currencies (${this.currency} and ${a.currency})`); - } - return emsc.amount_cmp(this.nativePtr, a.nativePtr); - } - - normalize() { - emsc.amount_normalize(this.nativePtr); - } -} - - -/** - * Count the UTF-8 characters in a JavaScript string. - */ -function countUtf8Bytes(str: string): number { - var s = str.length; - // JavaScript strings are UTF-16 arrays - for (let i = str.length - 1; i >= 0; i--) { - var code = str.charCodeAt(i); - if (code > 0x7f && code <= 0x7ff) { - // We need an extra byte in utf-8 here - s++; - } else if (code > 0x7ff && code <= 0xffff) { - // We need two extra bytes in utf-8 here - s += 2; - } - // Skip over the other surrogate - if (code >= 0xDC00 && code <= 0xDFFF) { - i--; - } - } - return s; -} - - -/** - * Managed reference to a contiguous block of memory in the Emscripten heap. - * Can be converted from / to a serialized representation. - * Should contain only data, not pointers. - */ -abstract class PackedArenaObject extends MallocArenaObject { - abstract size(): number; - - constructor(a?: Arena) { - super(a); - } - - randomize(qual: RandomQuality = RandomQuality.STRONG): void { - emsc.random_block(qual, this.nativePtr, this.size()); - } - - 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.fromStringWithNull(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() { - // FIXME: should the client be allowed to call alloc multiple times? - if (!this._nativePtr) { - this.nativePtr = emscAlloc.malloc(this.size()); - } - } - - 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.nativePtr + i, "i8"); - b = (b + 256) % 256; - bytes.push("0".concat(b.toString(16)).slice(-2)); - } - let lines: string[] = []; - 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 SimpleArena(); - let am = new Amount(undefined, 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: (s: string) => EddsaPrivateKey; -} -mixinStatic(EddsaPrivateKey, fromCrock); - - -export class EcdsaPrivateKey extends PackedArenaObject { - static create(a?: Arena): EcdsaPrivateKey { - let obj = new EcdsaPrivateKey(a); - obj.nativePtr = emscAlloc.ecdsa_key_create(); - return obj; - } - - size() { - return 32; - } - - getPublicKey(a?: Arena): EcdsaPublicKey { - let obj = new EcdsaPublicKey(a); - obj.nativePtr = emscAlloc.ecdsa_public_key_from_private(this.nativePtr); - return obj; - } - - static fromCrock: (s: string) => EcdsaPrivateKey; -} -mixinStatic(EcdsaPrivateKey, fromCrock); - - -export class EcdhePrivateKey extends PackedArenaObject { - static create(a?: Arena): EcdhePrivateKey { - let obj = new EcdhePrivateKey(a); - obj.nativePtr = emscAlloc.ecdhe_key_create(); - return obj; - } - - size() { - return 32; - } - - getPublicKey(a?: Arena): EcdhePublicKey { - let obj = new EcdhePublicKey(a); - obj.nativePtr = emscAlloc.ecdhe_public_key_from_private(this.nativePtr); - return obj; - } - - static fromCrock: (s: string) => EcdhePrivateKey; -} -mixinStatic(EcdhePrivateKey, fromCrock); - - -function fromCrock(s: string) { - let x = new this(); - x.alloc(); - x.loadCrock(s); - return x; -} - - -function mixin(obj: any, method: any, name?: string) { - if (!name) { - name = method.name; - } - if (!name) { - throw Error("Mixin needs a name."); - } - obj.prototype[method.name] = method; -} - - -function mixinStatic(obj: any, method: any, 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); - -export class EcdsaPublicKey extends PackedArenaObject { - size() { - return 32; - } - - static fromCrock: (s: string) => EcdsaPublicKey; -} -mixinStatic(EcdsaPublicKey, fromCrock); - - -export class EcdhePublicKey extends PackedArenaObject { - size() { - return 32; - } - - static fromCrock: (s: string) => EcdhePublicKey; -} -mixinStatic(EcdhePublicKey, 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.nativePtr, - 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 RsaBlindingKeySecret extends PackedArenaObject { - size() { - return 32; - } - - /** - * Create a random blinding key secret. - */ - static create(a?: Arena): RsaBlindingKeySecret { - let o = new RsaBlindingKeySecret(a); - o.alloc(); - o.randomize(); - return o; - } - - static fromCrock: (s: string) => RsaBlindingKeySecret; -} -mixinStatic(RsaBlindingKeySecret, fromCrock); - - -export class HashCode extends PackedArenaObject { - size() { - return 64; - } - - static fromCrock: (s: string) => HashCode; - - random(qual: RandomQuality = RandomQuality.STRONG) { - 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) { - this.nativePtr = emscAlloc.malloc(desiredSize); - } else { - this.nativePtr = init; - } - this.allocatedSize = desiredSize; - } - - static fromStringWithoutNull(s: string, a?: Arena): ByteArray { - // UTF-8 bytes, including 0-terminator - let terminatedByteLength = countUtf8Bytes(s) + 1; - let hstr = emscAlloc.malloc(terminatedByteLength); - Module.stringToUTF8(s, hstr, terminatedByteLength); - return new ByteArray(terminatedByteLength - 1, hstr, a); - } - - static fromStringWithNull(s: string, a?: Arena): ByteArray { - // UTF-8 bytes, including 0-terminator - let terminatedByteLength = countUtf8Bytes(s) + 1; - let hstr = emscAlloc.malloc(terminatedByteLength); - Module.stringToUTF8(s, hstr, terminatedByteLength); - return new ByteArray(terminatedByteLength, hstr, a); - } - - static fromCrock(s: string, a?: Arena): ByteArray { - let byteLength = countUtf8Bytes(s); - let hstr = emscAlloc.malloc(byteLength + 1); - Module.stringToUTF8(s, hstr, byteLength + 1); - let decodedLen = Math.floor((byteLength * 5) / 8); - let ba = new ByteArray(decodedLen, undefined, a); - let res = emsc.string_to_data(hstr, byteLength, 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; - - 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(`Member ${name} not set`); - } - 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(`Member ${name} not set`); - } - 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(`Key ${name} not found`); - } - if (!(value instanceof typemap[name])) { - throw Error("Wrong type for ${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] - ]; - } -} - - -interface RefreshMeltCoinAffirmationPS_Args { - session_hash: HashCode; - amount_with_fee: AmountNbo; - melt_fee: AmountNbo; - coin_pub: EddsaPublicKey; -} - -export class RefreshMeltCoinAffirmationPS extends SignatureStruct { - - constructor(w: RefreshMeltCoinAffirmationPS_Args) { - super(w); - } - - purpose() { - return SignaturePurpose.WALLET_COIN_MELT; - } - - fieldTypes() { - return [ - ["session_hash", HashCode], - ["amount_with_fee", AmountNbo], - ["melt_fee", AmountNbo], - ["coin_pub", EddsaPublicKey] - ]; - } -} - - -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 || m.length != 2) { - throw Error(); - } - let n = parseInt(m[1]) * 1000000; - // XXX: This only works up to 54 bit numbers. - set64(x.nativePtr, 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); - } -} - -// XXX: This only works up to 54 bit numbers. -function set32(p: number, n: number) { - for (let i = 0; i < 4; ++i) { - Module.setValue(p + (3 - 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.nativePtr, n); - return x; - } - - size() { - return 8; - } -} - - -export class UInt32 extends PackedArenaObject { - static fromNumber(n: number): UInt64 { - let x = new UInt32(); - x.alloc(); - set32(x.nativePtr, n); - return x; - } - - size() { - return 4; - } -} - - -// 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], - ]; - } -} - -export interface DenominationKeyValidityPS_args { - master: EddsaPublicKey; - start: AbsoluteTimeNbo; - expire_withdraw: AbsoluteTimeNbo; - expire_spend: AbsoluteTimeNbo; - expire_legal: AbsoluteTimeNbo; - value: AmountNbo; - fee_withdraw: AmountNbo; - fee_deposit: AmountNbo; - fee_refresh: AmountNbo; - fee_refund: AmountNbo; - denom_hash: HashCode; -} - -export class DenominationKeyValidityPS extends SignatureStruct { - constructor(w: DenominationKeyValidityPS_args) { - super(w); - } - - purpose() { - return SignaturePurpose.MASTER_DENOMINATION_KEY_VALIDITY; - } - - fieldTypes() { - return [ - ["master", EddsaPublicKey], - ["start", AbsoluteTimeNbo], - ["expire_withdraw", AbsoluteTimeNbo], - ["expire_spend", AbsoluteTimeNbo], - ["expire_legal", AbsoluteTimeNbo], - ["value", AmountNbo], - ["fee_withdraw", AmountNbo], - ["fee_deposit", AmountNbo], - ["fee_refresh", AmountNbo], - ["fee_refund", AmountNbo], - ["denom_hash", HashCode] - ]; - } -} - - -interface Encodeable { - encode(arena?: Arena): ByteArray; -} - -function makeEncode(encodeFn: any) { - function encode(arena?: Arena) { - let ptr = emscAlloc.malloc(PTR_SIZE); - let len = encodeFn(this.nativePtr, ptr); - let res = new ByteArray(len, undefined, arena); - res.nativePtr = Module.getValue(ptr, '*'); - emsc.free(ptr); - return res; - } - - return encode; -} - - -export class RsaPublicKey extends MallocArenaObject 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 MallocArenaObject implements Encodeable { - static fromCrock: (s: string, a?: Arena) => RsaSignature; - - encode: (arena?: Arena) => ByteArray; - - destroy() { - emsc.rsa_signature_free(this.nativePtr); - this.nativePtr = 0; - } -} -mixinStatic(RsaSignature, makeFromCrock(emscAlloc.rsa_signature_decode)); -mixin(RsaSignature, makeEncode(emscAlloc.rsa_signature_encode)); - - -export function rsaBlind(hashCode: HashCode, - blindingKey: RsaBlindingKeySecret, - pkey: RsaPublicKey, - arena?: Arena): ByteArray|null { - let buf_ptr_out = emscAlloc.malloc(PTR_SIZE); - let buf_size_out = emscAlloc.malloc(PTR_SIZE); - let res = emscAlloc.rsa_blind(hashCode.nativePtr, - blindingKey.nativePtr, - pkey.nativePtr, - buf_ptr_out, - buf_size_out); - let buf_ptr = Module.getValue(buf_ptr_out, '*'); - let buf_size = Module.getValue(buf_size_out, '*'); - emsc.free(buf_ptr_out); - emsc.free(buf_size_out); - if (res != GNUNET_OK) { - // malicious key - return null; - } - return new ByteArray(buf_size, buf_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 eddsaVerify(purposeNum: number, - verify: EccSignaturePurpose, - sig: EddsaSignature, - pub: EddsaPublicKey, - a?: Arena): boolean { - let r = emsc.eddsa_verify(purposeNum, - verify.nativePtr, - sig.nativePtr, - pub.nativePtr); - return r === GNUNET_OK; -} - - -export function rsaUnblind(sig: RsaSignature, - bk: RsaBlindingKeySecret, - pk: RsaPublicKey, - a?: Arena): RsaSignature { - let x = new RsaSignature(a); - x.nativePtr = emscAlloc.rsa_unblind(sig.nativePtr, - bk.nativePtr, - pk.nativePtr); - return x; -} - - -type TransferSecretP = HashCode; - - -export interface FreshCoin { - priv: EddsaPrivateKey; - blindingKey: RsaBlindingKeySecret; -} - -export function ecdhEddsa(priv: EcdhePrivateKey, - pub: EddsaPublicKey): HashCode { - let h = new HashCode(); - h.alloc(); - let res = emsc.ecdh_eddsa(priv.nativePtr, pub.nativePtr, h.nativePtr); - if (res != GNUNET_OK) { - throw Error("ecdh_eddsa failed"); - } - return h; -} - -export function setupFreshCoin(secretSeed: TransferSecretP, - coinIndex: number): FreshCoin { - let priv = new EddsaPrivateKey(); - priv.isWeak = true; - let blindingKey = new RsaBlindingKeySecret(); - blindingKey.isWeak = true; - let buf = new ByteArray(priv.size() + blindingKey.size()); - - emsc.setup_fresh_coin(secretSeed.nativePtr, coinIndex, buf.nativePtr); - - priv.nativePtr = buf.nativePtr; - blindingKey.nativePtr = buf.nativePtr + priv.size(); - - return {priv, blindingKey}; -} diff --git a/lib/wallet/helpers.ts b/lib/wallet/helpers.ts deleted file mode 100644 index 26cd350ee..000000000 --- a/lib/wallet/helpers.ts +++ /dev/null @@ -1,140 +0,0 @@ -/* - 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, see - */ - -/** - * Smaller helper functions that do not depend - * on the emscripten machinery. - * - * @author Florian Dold - */ - -/// - -import {AmountJson} from "./types"; -import URI = uri.URI; - -export function substituteFulfillmentUrl(url: string, vars: any) { - url = url.replace("${H_contract}", vars.H_contract); - url = url.replace("${$}", "$"); - return url; -} - - -export function amountToPretty(amount: AmountJson): string { - let x = amount.value + amount.fraction / 1e6; - return `${x} ${amount.currency}`; -} - - -/** - * Canonicalize a base url, typically for the exchange. - * - * See http://api.taler.net/wallet.html#general - */ -export function canonicalizeBaseUrl(url: string) { - let x: URI = new URI(url); - if (!x.protocol()) { - x.protocol("https"); - } - x.path(x.path() + "/").normalizePath(); - x.fragment(); - x.query(); - return x.href() -} - - -export function parsePrettyAmount(pretty: string): AmountJson|undefined { - const res = /([0-9]+)(.[0-9]+)?\s*(\w+)/.exec(pretty); - if (!res) { - return undefined; - } - return { - value: parseInt(res[1], 10), - fraction: res[2] ? (parseFloat(`0.${res[2]}`) * 1e-6) : 0, - currency: res[3] - } -} - - - -/** - * Convert object to JSON with canonical ordering of keys - * and whitespace omitted. - */ -export function canonicalJson(obj: any): string { - // Check for cycles, etc. - JSON.stringify(obj); - if (typeof obj == "string" || typeof obj == "number" || obj === null) { - return JSON.stringify(obj) - } - if (Array.isArray(obj)) { - let objs: string[] = obj.map((e) => canonicalJson(e)); - return `[${objs.join(',')}]`; - } - let keys: string[] = []; - for (let key in obj) { - keys.push(key); - } - keys.sort(); - let s = "{"; - for (let i = 0; i < keys.length; i++) { - let key = keys[i]; - s += JSON.stringify(key) + ":" + canonicalJson(obj[key]); - if (i != keys.length - 1) { - s += ","; - } - } - return s + "}"; -} - - -export function deepEquals(x: any, y: any): boolean { - if (x === y) { - return true; - } - - if (Array.isArray(x) && x.length !== y.length) { - return false; - } - - var p = Object.keys(x); - return Object.keys(y).every((i) => p.indexOf(i) !== -1) && - p.every((i) => deepEquals(x[i], y[i])); -} - - -export function flatMap(xs: T[], f: (x: T) => U[]): U[] { - return xs.reduce((acc: U[], next: T) => [...f(next), ...acc], []); -} - - -export function getTalerStampSec(stamp: string): number | null { - const m = stamp.match(/\/?Date\(([0-9]*)\)\/?/); - if (!m) { - return null; - } - return parseInt(m[1]); -} - - -export function getTalerStampDate(stamp: string): Date | null { - let sec = getTalerStampSec(stamp); - if (sec == null) { - return null; - } - return new Date(sec * 1000); -} - diff --git a/lib/wallet/http.ts b/lib/wallet/http.ts deleted file mode 100644 index 1d22c4eb2..000000000 --- a/lib/wallet/http.ts +++ /dev/null @@ -1,97 +0,0 @@ -/* - 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, see - */ - -/** - * 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 interface HttpRequestLibrary { - req(method: string, - url: string | uri.URI, - options?: any): Promise; - - get(url: string | uri.URI): Promise; - - postJson(url: string | uri.URI, body: any): Promise; - - postForm(url: string | uri.URI, form: any): Promise; -} - - -export class BrowserHttpLib { - req(method: string, - url: string|uri.URI, - options?: any): Promise { - 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: any) { - return this.req("post", url, {req: JSON.stringify(body)}); - } - - - postForm(url: string|uri.URI, form: any) { - return this.req("post", url, {req: form}); - } -} - - -export class RequestException { - constructor(detail: any) { - - } -} diff --git a/lib/wallet/query.ts b/lib/wallet/query.ts deleted file mode 100644 index 08e270ea6..000000000 --- a/lib/wallet/query.ts +++ /dev/null @@ -1,612 +0,0 @@ -/* - 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, see - */ - - -/** - * Database query abstractions. - * @module Query - * @author Florian Dold - */ - -"use strict"; - - -export interface JoinResult { - left: L; - right: R; -} - - -export class Store { - name: string; - validator?: (v: T) => T; - storeParams: IDBObjectStoreParameters; - - constructor(name: string, storeParams: IDBObjectStoreParameters, - validator?: (v: T) => T) { - this.name = name; - this.validator = validator; - this.storeParams = storeParams; - } -} - -export class Index { - indexName: string; - storeName: string; - keyPath: string | string[]; - - constructor(s: Store, indexName: string, keyPath: string | string[]) { - this.storeName = s.name; - this.indexName = indexName; - this.keyPath = keyPath; - } -} - -/** - * Stream that can be filtered, reduced or joined - * with indices. - */ -export interface QueryStream { - indexJoin(index: Index, - keyFn: (obj: T) => I): QueryStream>; - keyJoin(store: Store, - keyFn: (obj: T) => I): QueryStream>; - filter(f: (T: any) => boolean): QueryStream; - reduce(f: (v: T, acc: S) => S, start?: S): Promise; - map(f: (x:T) => S): QueryStream; - flatMap(f: (x: T) => S[]): QueryStream; - toArray(): Promise; - - then(onfulfill: any, onreject: any): any; -} - -export let AbortTransaction = Symbol("abort_transaction"); - -/** - * Get an unresolved promise together with its extracted resolve / reject - * function. - */ -function openPromise() { - let resolve: ((value?: T | PromiseLike) => void) | null = null; - let reject: ((reason?: any) => void) | null = null; - const promise = new Promise((res, rej) => { - resolve = res; - reject = rej; - }); - if (!(resolve && reject)) { - // Never happens, unless JS implementation is broken - throw Error(); - } - return {resolve, reject, promise}; -} - - -abstract class QueryStreamBase implements QueryStream, PromiseLike { - abstract subscribe(f: (isDone: boolean, - value: any, - tx: IDBTransaction) => void): void; - - root: QueryRoot; - - constructor(root: QueryRoot) { - this.root = root; - } - - then(onfulfilled: (value: void) => R | PromiseLike, onrejected: (reason: any) => R | PromiseLike): PromiseLike { - return this.root.then(onfulfilled, onrejected); - } - - flatMap(f: (x: T) => S[]): QueryStream { - return new QueryStreamFlatMap(this, f); - } - - map(f: (x: T) => S): QueryStream { - return new QueryStreamMap(this, f); - } - - indexJoin(index: Index, - keyFn: (obj: T) => I): QueryStream> { - this.root.addStoreAccess(index.storeName, false); - return new QueryStreamIndexJoin(this, index.storeName, index.indexName, keyFn); - } - - keyJoin(store: Store, - keyFn: (obj: T) => I): QueryStream> { - this.root.addStoreAccess(store.name, false); - return new QueryStreamKeyJoin(this, store.name, keyFn); - } - - filter(f: (x: any) => boolean): QueryStream { - return new QueryStreamFilter(this, f); - } - - toArray(): Promise { - let {resolve, promise} = openPromise(); - let values: T[] = []; - - this.subscribe((isDone, value) => { - if (isDone) { - resolve(values); - return; - } - values.push(value); - }); - - return Promise.resolve() - .then(() => this.root.finish()) - .then(() => promise); - } - - reduce(f: (x: any, acc?: A) => A, init?: A): Promise { - let {resolve, promise} = openPromise(); - let acc = init; - - this.subscribe((isDone, value) => { - if (isDone) { - resolve(acc); - return; - } - acc = f(value, acc); - }); - - return Promise.resolve() - .then(() => this.root.finish()) - .then(() => promise); - } -} - -type FilterFn = (e: any) => boolean; -type SubscribeFn = (done: boolean, value: any, tx: IDBTransaction) => void; - -interface FlatMapFn { - (v: T): T[]; -} - -class QueryStreamFilter extends QueryStreamBase { - s: QueryStreamBase; - filterFn: FilterFn; - - constructor(s: QueryStreamBase, filterFn: FilterFn) { - super(s.root); - this.s = s; - this.filterFn = filterFn; - } - - subscribe(f: SubscribeFn) { - this.s.subscribe((isDone, value, tx) => { - if (isDone) { - f(true, undefined, tx); - return; - } - if (this.filterFn(value)) { - f(false, value, tx); - } - }); - } -} - - -class QueryStreamFlatMap extends QueryStreamBase { - s: QueryStreamBase; - flatMapFn: (v: T) => S[]; - - constructor(s: QueryStreamBase, flatMapFn: (v: T) => S[]) { - super(s.root); - this.s = s; - this.flatMapFn = flatMapFn; - } - - subscribe(f: SubscribeFn) { - this.s.subscribe((isDone, value, tx) => { - if (isDone) { - f(true, undefined, tx); - return; - } - let values = this.flatMapFn(value); - for (let v in values) { - f(false, value, tx) - } - }); - } -} - - -class QueryStreamMap extends QueryStreamBase { - s: QueryStreamBase; - mapFn: (v: S) => T; - - constructor(s: QueryStreamBase, mapFn: (v: S) => T) { - super(s.root); - this.s = s; - this.mapFn = mapFn; - } - - subscribe(f: SubscribeFn) { - this.s.subscribe((isDone, value, tx) => { - if (isDone) { - f(true, undefined, tx); - return; - } - let mappedValue = this.mapFn(value); - f(false, mappedValue, tx); - }); - } -} - - -class QueryStreamIndexJoin extends QueryStreamBase> { - s: QueryStreamBase; - storeName: string; - key: any; - indexName: string; - - constructor(s: QueryStreamBase, storeName: string, indexName: string, - key: any) { - super(s.root); - this.s = s; - this.storeName = storeName; - this.key = key; - this.indexName = indexName; - } - - subscribe(f: SubscribeFn) { - this.s.subscribe((isDone, value, tx) => { - if (isDone) { - f(true, undefined, tx); - return; - } - console.log("joining on", this.key(value)); - let s = 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, {left: value, right: cursor.value}, tx); - cursor.continue(); - } else { - f(true, undefined, tx); - } - } - }); - } -} - - -class QueryStreamKeyJoin extends QueryStreamBase> { - s: QueryStreamBase; - storeName: string; - key: any; - - constructor(s: QueryStreamBase, storeName: string, - key: any) { - super(s.root); - this.s = s; - this.storeName = storeName; - this.key = key; - } - - subscribe(f: SubscribeFn) { - this.s.subscribe((isDone, value, tx) => { - if (isDone) { - f(true, undefined, tx); - return; - } - console.log("joining on", this.key(value)); - let s = tx.objectStore(this.storeName); - let req = s.openCursor(IDBKeyRange.only(this.key(value))); - req.onsuccess = () => { - let cursor = req.result; - if (cursor) { - f(false, {left:value, right: cursor.value}, tx); - cursor.continue(); - } else { - f(true, undefined, tx); - } - } - }); - } -} - - -class IterQueryStream extends QueryStreamBase { - private storeName: string; - private options: any; - private subscribers: SubscribeFn[]; - - constructor(qr: QueryRoot, storeName: string, options: any) { - super(qr); - this.options = options; - this.storeName = storeName; - this.subscribers = []; - - let doIt = (tx: IDBTransaction) => { - const {indexName = void 0, only = void 0} = this.options; - let s: any; - if (indexName !== void 0) { - s = tx.objectStore(this.storeName) - .index(this.options.indexName); - } else { - s = tx.objectStore(this.storeName); - } - let kr: IDBKeyRange | undefined = undefined; - if (only !== undefined) { - kr = IDBKeyRange.only(this.options.only); - } - let req = s.openCursor(kr); - req.onsuccess = () => { - let cursor: IDBCursorWithValue = req.result; - if (cursor) { - for (let f of this.subscribers) { - f(false, cursor.value, tx); - } - cursor.continue(); - } else { - for (let f of this.subscribers) { - f(true, undefined, tx); - } - } - } - }; - - this.root.addWork(doIt); - } - - subscribe(f: SubscribeFn) { - this.subscribers.push(f); - } -} - - -export class QueryRoot implements PromiseLike { - private work: ((t: IDBTransaction) => void)[] = []; - private db: IDBDatabase; - private stores = new Set(); - private kickoffPromise: Promise; - - /** - * Some operations is a write operation, - * and we need to do a "readwrite" transaction/ - */ - private hasWrite: boolean; - - private finishScheduled: boolean; - - constructor(db: IDBDatabase) { - this.db = db; - } - - then(onfulfilled: (value: void) => R | PromiseLike, onrejected: (reason: any) => R | PromiseLike): PromiseLike { - return this.finish().then(onfulfilled, onrejected); - } - - iter(store: Store): QueryStream { - this.stores.add(store.name); - this.scheduleFinish(); - return new IterQueryStream(this, store.name, {}); - } - - iterIndex(index: Index, - only?: S): QueryStream { - this.stores.add(index.storeName); - this.scheduleFinish(); - return new IterQueryStream(this, index.storeName, { - only, - indexName: index.indexName - }); - } - - /** - * Put an object into the given object store. - * Overrides if an existing object with the same key exists - * in the store. - */ - put(store: Store, val: T): QueryRoot { - let doPut = (tx: IDBTransaction) => { - tx.objectStore(store.name).put(val); - }; - this.scheduleFinish(); - this.addWork(doPut, store.name, true); - return this; - } - - - putWithResult(store: Store, val: T): Promise { - const {resolve, promise} = openPromise(); - let doPutWithResult = (tx: IDBTransaction) => { - let req = tx.objectStore(store.name).put(val); - req.onsuccess = () => { - resolve(req.result); - } - this.scheduleFinish(); - }; - this.addWork(doPutWithResult, store.name, true); - return Promise.resolve() - .then(() => this.finish()) - .then(() => promise); - } - - - mutate(store: Store, key: any, f: (v: T) => T): QueryRoot { - let doPut = (tx: IDBTransaction) => { - let reqGet = tx.objectStore(store.name).get(key); - reqGet.onsuccess = () => { - let r = reqGet.result; - let m: T; - try { - m = f(r); - } catch (e) { - if (e == AbortTransaction) { - tx.abort(); - return; - } - throw e; - } - - tx.objectStore(store.name).put(m); - } - }; - this.scheduleFinish(); - this.addWork(doPut, store.name, true); - return this; - } - - - /** - * Add all object from an iterable to the given object store. - * Fails if the object's key is already present - * in the object store. - */ - putAll(store: Store, iterable: T[]): QueryRoot { - const doPutAll = (tx: IDBTransaction) => { - for (let obj of iterable) { - tx.objectStore(store.name).put(obj); - } - }; - this.scheduleFinish(); - this.addWork(doPutAll, store.name, true); - return this; - } - - /** - * Add an object to the given object store. - * Fails if the object's key is already present - * in the object store. - */ - add(store: Store, val: T): QueryRoot { - const doAdd = (tx: IDBTransaction) => { - tx.objectStore(store.name).add(val); - }; - this.scheduleFinish(); - this.addWork(doAdd, store.name, true); - return this; - } - - /** - * Get one object from a store by its key. - */ - get(store: Store, key: any): Promise { - if (key === void 0) { - throw Error("key must not be undefined"); - } - - const {resolve, promise} = openPromise(); - - const doGet = (tx: IDBTransaction) => { - const req = tx.objectStore(store.name).get(key); - req.onsuccess = () => { - resolve(req.result); - }; - }; - - this.addWork(doGet, store.name, false); - return Promise.resolve() - .then(() => this.finish()) - .then(() => promise); - } - - /** - * Get one object from a store by its key. - */ - getIndexed(index: Index, - key: I): Promise { - if (key === void 0) { - throw Error("key must not be undefined"); - } - - const {resolve, promise} = openPromise(); - - const doGetIndexed = (tx: IDBTransaction) => { - const req = tx.objectStore(index.storeName) - .index(index.indexName) - .get(key); - req.onsuccess = () => { - resolve(req.result); - }; - }; - - this.addWork(doGetIndexed, index.storeName, false); - return Promise.resolve() - .then(() => this.finish()) - .then(() => promise); - } - - private scheduleFinish() { - if (!this.finishScheduled) { - Promise.resolve().then(() => this.finish()); - this.finishScheduled = true; - } - } - - /** - * Finish the query, and start the query in the first place if necessary. - */ - finish(): Promise { - if (this.kickoffPromise) { - return this.kickoffPromise; - } - this.kickoffPromise = new Promise((resolve, reject) => { - if (this.work.length == 0) { - resolve(); - return; - } - const mode = this.hasWrite ? "readwrite" : "readonly"; - const tx = this.db.transaction(Array.from(this.stores), mode); - tx.oncomplete = () => { - resolve(); - }; - tx.onabort = () => { - reject(Error("transaction aborted")); - }; - for (let w of this.work) { - w(tx); - } - }); - return this.kickoffPromise; - } - - /** - * Delete an object by from the given object store. - */ - delete(storeName: string, key: any): QueryRoot { - const doDelete = (tx: IDBTransaction) => { - tx.objectStore(storeName).delete(key); - }; - this.scheduleFinish(); - this.addWork(doDelete, storeName, true); - return this; - } - - /** - * Low-level function to add a task to the internal work queue. - */ - addWork(workFn: (t: IDBTransaction) => void, - storeName?: string, - isWrite?: boolean) { - this.work.push(workFn); - if (storeName) { - this.addStoreAccess(storeName, isWrite); - } - } - - addStoreAccess(storeName: string, isWrite?: boolean) { - if (storeName) { - this.stores.add(storeName); - } - if (isWrite) { - this.hasWrite = true; - } - } -} diff --git a/lib/wallet/renderHtml.tsx b/lib/wallet/renderHtml.tsx deleted file mode 100644 index 940d5c425..000000000 --- a/lib/wallet/renderHtml.tsx +++ /dev/null @@ -1,63 +0,0 @@ -/* - This file is part of TALER - (C) 2016 INRIA - - 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, see - */ - -/** - * Helpers functions to render Taler-related data structures to HTML. - * - * @author Florian Dold - */ - - -import {AmountJson, Contract} from "./types"; - -export function prettyAmount(amount: AmountJson) { - let v = amount.value + amount.fraction / 1e6; - return `${v.toFixed(2)} ${amount.currency}`; -} - -export function renderContract(contract: Contract): JSX.Element { - let merchantName = {contract.merchant.name}; - let amount = {prettyAmount(contract.amount)}; - - return ( -
-

- The merchant {merchantName} - wants to enter a contract over {amount}{" "} - with you. -

-

{i18n`You are about to purchase:`}

-
    - {contract.products.map( - (p: any, i: number) => (
  • {`${p.description}: ${prettyAmount(p.price)}`}
  • )) - } -
-
- ); -} - - -export function abbrev(s: string, n: number = 5) { - let sAbbrev = s; - if (s.length > n) { - sAbbrev = s.slice(0, n) + ".."; - } - return ( - - {sAbbrev} - - ); -} diff --git a/lib/wallet/types-test.ts b/lib/wallet/types-test.ts deleted file mode 100644 index 3ebb1a5db..000000000 --- a/lib/wallet/types-test.ts +++ /dev/null @@ -1,38 +0,0 @@ -import {test, TestLib} from "testlib/talertest"; -import {Amounts} from "./types"; -import * as types from "./types"; - -let amt = (value: number, fraction: number, currency: string): types.AmountJson => ({value, fraction, currency}); - -test("amount addition (simple)", (t: TestLib) => { - let a1 = amt(1,0,"EUR"); - let a2 = amt(1,0,"EUR"); - let a3 = amt(2,0,"EUR"); - t.assert(0 == types.Amounts.cmp(Amounts.add(a1, a2).amount, a3)); - t.pass(); -}); - -test("amount addition (saturation)", (t: TestLib) => { - let a1 = amt(1,0,"EUR"); - let res = Amounts.add(Amounts.getMaxAmount("EUR"), a1); - t.assert(res.saturated); - t.pass(); -}); - -test("amount subtraction (simple)", (t: TestLib) => { - let a1 = amt(2,5,"EUR"); - let a2 = amt(1,0,"EUR"); - let a3 = amt(1,5,"EUR"); - t.assert(0 == types.Amounts.cmp(Amounts.sub(a1, a2).amount, a3)); - t.pass(); -}); - -test("amount subtraction (saturation)", (t: TestLib) => { - let a1 = amt(0,0,"EUR"); - let a2 = amt(1,0,"EUR"); - let res = Amounts.sub(a1, a2); - t.assert(res.saturated); - res = Amounts.sub(a1, a1); - t.assert(!res.saturated); - t.pass(); -}); diff --git a/lib/wallet/types.ts b/lib/wallet/types.ts deleted file mode 100644 index 39d374069..000000000 --- a/lib/wallet/types.ts +++ /dev/null @@ -1,554 +0,0 @@ -/* - 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, see - */ - -/** - * Common types that are used by Taler. - * - * Note most types are defined in wallet.ts, types that - * are defined in types.ts are intended to be used by components - * that do not depend on the whole wallet implementation (which depends on - * emscripten). - * - * @author Florian Dold - */ - -import { Checkable } from "./checkable"; - -@Checkable.Class -export class AmountJson { - @Checkable.Number - value: number; - - @Checkable.Number - fraction: number; - - @Checkable.String - currency: string; - - static checked: (obj: any) => AmountJson; -} - - -export interface SignedAmountJson { - amount: AmountJson; - isNegative: boolean; -} - - -export interface ReserveRecord { - reserve_pub: string; - reserve_priv: string, - exchange_base_url: string, - created: number, - last_query: number | null, - /** - * Current amount left in the reserve - */ - current_amount: AmountJson | null, - /** - * Amount requested when the reserve was created. - * When a reserve is re-used (rare!) the current_amount can - * be higher than the requested_amount - */ - requested_amount: AmountJson, - - - /** - * What's the current amount that sits - * in precoins? - */ - precoin_amount: AmountJson; - - - confirmed: boolean, -} - - -@Checkable.Class -export class CreateReserveResponse { - /** - * Exchange URL where the bank should create the reserve. - * The URL is canonicalized in the response. - */ - @Checkable.String - exchange: string; - - @Checkable.String - reservePub: string; - - static checked: (obj: any) => CreateReserveResponse; -} - - -@Checkable.Class -export class Denomination { - @Checkable.Value(AmountJson) - value: AmountJson; - - @Checkable.String - denom_pub: string; - - @Checkable.Value(AmountJson) - fee_withdraw: AmountJson; - - @Checkable.Value(AmountJson) - fee_deposit: AmountJson; - - @Checkable.Value(AmountJson) - fee_refresh: AmountJson; - - @Checkable.Value(AmountJson) - fee_refund: AmountJson; - - @Checkable.String - stamp_start: string; - - @Checkable.String - stamp_expire_withdraw: string; - - @Checkable.String - stamp_expire_legal: string; - - @Checkable.String - stamp_expire_deposit: string; - - @Checkable.String - master_sig: string; - - static checked: (obj: any) => Denomination; -} - - -export interface IExchangeInfo { - baseUrl: string; - masterPublicKey: string; - - /** - * All denominations we ever received from the exchange. - * Expired denominations may be garbage collected. - */ - all_denoms: Denomination[]; - - /** - * Denominations we received with the last update. - * Subset of "denoms". - */ - active_denoms: Denomination[]; - - /** - * Timestamp for last update. - */ - last_update_time: number; -} - -export interface WireInfo { - [type: string]: any; -} - -export interface ReserveCreationInfo { - exchangeInfo: IExchangeInfo; - wireInfo: WireInfo; - selectedDenoms: Denomination[]; - withdrawFee: AmountJson; - overhead: AmountJson; -} - - -/** - * A coin that isn't yet signed by an exchange. - */ -export interface PreCoin { - coinPub: string; - coinPriv: string; - reservePub: string; - denomPub: string; - blindingKey: string; - withdrawSig: string; - coinEv: string; - exchangeBaseUrl: string; - coinValue: AmountJson; -} - -export interface RefreshPreCoin { - publicKey: string; - privateKey: string; - coinEv: string; - blindingKey: string -} - - -/** - * Ongoing refresh - */ -export interface RefreshSession { - /** - * Public key that's being melted in this session. - */ - meltCoinPub: string; - - /** - * How much of the coin's value is melted away - * with this refresh session? - */ - valueWithFee: AmountJson - - /** - * Sum of the value of denominations we want - * to withdraw in this session, without fees. - */ - valueOutput: AmountJson; - - /** - * Signature to confirm the melting. - */ - confirmSig: string; - - /** - * Denominations of the newly requested coins - */ - newDenoms: string[]; - - - preCoinsForGammas: RefreshPreCoin[][]; - - - /** - * The transfer keys, kappa of them. - */ - transferPubs: string[]; - - transferPrivs: string[]; - - /** - * The no-reveal-index after we've done the melting. - */ - norevealIndex?: number; - - /** - * Hash of the session. - */ - hash: string; - - exchangeBaseUrl: string; - - finished: boolean; -} - - -export interface CoinPaySig { - coin_sig: string; - coin_pub: string; - ub_sig: string; - denom_pub: string; - f: AmountJson; -} - -/** - * Coin as stored in the "coins" data store - * of the wallet database. - */ -export interface Coin { - /** - * Public key of the coin. - */ - coinPub: string; - - /** - * Private key to authorize operations on the coin. - */ - coinPriv: string; - - /** - * Key used by the exchange used to sign the coin. - */ - denomPub: string; - - /** - * Unblinded signature by the exchange. - */ - denomSig: string; - - /** - * Amount that's left on the coin. - */ - currentAmount: AmountJson; - - /** - * Base URL that identifies the exchange from which we got the - * coin. - */ - exchangeBaseUrl: string; - - /** - * We have withdrawn the coin, but it's not accepted by the exchange anymore. - * We have to tell an auditor and wait for compensation or for the exchange - * to fix it. - */ - suspended?: boolean; - - /** - * Was the coin revealed in a transaction? - */ - dirty: boolean; - - /** - * Is the coin currently involved in a transaction? - * - * This delays refreshing until the transaction is finished or - * aborted. - */ - transactionPending: boolean; -} - - -@Checkable.Class -export class ExchangeHandle { - @Checkable.String - master_pub: string; - - @Checkable.String - url: string; - - static checked: (obj: any) => ExchangeHandle; -} - -export interface WalletBalance { - [currency: string]: WalletBalanceEntry; -} - -export interface WalletBalanceEntry { - available: AmountJson; - pendingIncoming: AmountJson; - pendingPayment: AmountJson; -} - - -interface Merchant { - /** - * label for a location with the business address of the merchant - */ - address: string; - - /** - * the merchant's legal name of business - */ - name: string; - - /** - * label for a location that denotes the jurisdiction for disputes. - * Some of the typical fields for a location (such as a street address) may be absent. - */ - jurisdiction: string; - - /** - * Instance of the merchant, in case one merchant - * represents multiple receivers. - */ - instance?: string; -} - -@Checkable.Class -export class Contract { - @Checkable.String - H_wire: string; - - @Checkable.String - summary: string; - - @Checkable.Value(AmountJson) - amount: AmountJson; - - @Checkable.List(Checkable.AnyObject) - auditors: any[]; - - /** - * DEPRECATED alias for pay_deadline. - */ - @Checkable.Optional(Checkable.String) - expiry: string; - - @Checkable.Optional(Checkable.String) - pay_deadline: string; - - @Checkable.Any - locations: any; - - @Checkable.Value(AmountJson) - max_fee: AmountJson; - - @Checkable.Any - merchant: any; - - @Checkable.String - merchant_pub: string; - - @Checkable.List(Checkable.Value(ExchangeHandle)) - exchanges: ExchangeHandle[]; - - @Checkable.List(Checkable.AnyObject) - products: any[]; - - @Checkable.String - refund_deadline: string; - - @Checkable.String - timestamp: string; - - @Checkable.Number - transaction_id: number; - - @Checkable.String - fulfillment_url: string; - - @Checkable.Optional(Checkable.String) - repurchase_correlation_id: string; - - /** - * DEPRECATED alias for instance - */ - @Checkable.Optional(Checkable.String) - receiver: string; - - @Checkable.Optional(Checkable.String) - instance: string; - - static checked: (obj: any) => Contract; -} - - -export type PayCoinInfo = Array<{ updatedCoin: Coin, sig: CoinPaySig }>; - - -export namespace Amounts { - export interface Result { - amount: AmountJson; - // Was there an over-/underflow? - saturated: boolean; - } - - export function getMaxAmount(currency: string): AmountJson { - return { - currency, - value: Number.MAX_SAFE_INTEGER, - fraction: 2 ** 32, - } - } - - export function getZero(currency: string): AmountJson { - return { - currency, - value: 0, - fraction: 0, - } - } - - export function add(first: AmountJson, ...rest: AmountJson[]): Result { - let currency = first.currency; - let value = first.value + Math.floor(first.fraction / 1e6); - if (value > Number.MAX_SAFE_INTEGER) { - return { amount: getMaxAmount(currency), saturated: true }; - } - let fraction = first.fraction % 1e6; - for (let x of rest) { - if (x.currency !== currency) { - throw Error(`Mismatched currency: ${x.currency} and ${currency}`); - } - - value = value + x.value + Math.floor((fraction + x.fraction) / 1e6); - fraction = (fraction + x.fraction) % 1e6; - if (value > Number.MAX_SAFE_INTEGER) { - return { amount: getMaxAmount(currency), saturated: true }; - } - } - return { amount: { currency, value, fraction }, saturated: false }; - } - - - export function sub(a: AmountJson, ...rest: AmountJson[]): Result { - let currency = a.currency; - let value = a.value; - let fraction = a.fraction; - - for (let b of rest) { - if (b.currency !== currency) { - throw Error(`Mismatched currency: ${b.currency} and ${currency}`); - } - if (fraction < b.fraction) { - if (value < 1) { - return { amount: { currency, value: 0, fraction: 0 }, saturated: true }; - } - value--; - fraction += 1e6; - } - console.assert(fraction >= b.fraction); - fraction -= b.fraction; - if (value < b.value) { - return { amount: { currency, value: 0, fraction: 0 }, saturated: true }; - } - value -= b.value; - } - - return { amount: { currency, value, fraction }, saturated: false }; - } - - export function cmp(a: AmountJson, b: AmountJson): number { - if (a.currency !== b.currency) { - throw Error(`Mismatched currency: ${a.currency} and ${b.currency}`); - } - let av = a.value + Math.floor(a.fraction / 1e6); - let af = a.fraction % 1e6; - let bv = b.value + Math.floor(b.fraction / 1e6); - let bf = b.fraction % 1e6; - switch (true) { - case av < bv: - return -1; - case av > bv: - return 1; - case af < bf: - return -1; - case af > bf: - return 1; - case af == bf: - return 0; - default: - throw Error("assertion failed"); - } - } - - export function copy(a: AmountJson): AmountJson { - return { - value: a.value, - fraction: a.fraction, - currency: a.currency, - } - } - - export function isNonZero(a: AmountJson) { - return a.value > 0 || a.fraction > 0; - } -} - - -export interface CheckRepurchaseResult { - isRepurchase: boolean; - existingContractHash?: string; - existingFulfillmentUrl?: string; -} - - -export interface Notifier { - notify(): void; -} diff --git a/lib/wallet/wallet.ts b/lib/wallet/wallet.ts deleted file mode 100644 index 9fb6e5a27..000000000 --- a/lib/wallet/wallet.ts +++ /dev/null @@ -1,1657 +0,0 @@ -/* - 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, see - */ - -/** - * High-level wallet operations that should be indepentent from the underlying - * browser extension interface. - * @module Wallet - * @author Florian Dold - */ - -import { - AmountJson, - Amounts, - CheckRepurchaseResult, - Coin, - CoinPaySig, - Contract, - CreateReserveResponse, - Denomination, - ExchangeHandle, - IExchangeInfo, - Notifier, - PayCoinInfo, - PreCoin, - RefreshSession, - ReserveCreationInfo, - ReserveRecord, - WalletBalance, - WalletBalanceEntry, - WireInfo, -} from "./types"; -import { - HttpRequestLibrary, - HttpResponse, - RequestException, -} from "./http"; -import { - AbortTransaction, - Index, - JoinResult, - QueryRoot, - Store, -} from "./query"; -import {Checkable} from "./checkable"; -import { - amountToPretty, - canonicalizeBaseUrl, - canonicalJson, - deepEquals, - flatMap, - getTalerStampSec, -} from "./helpers"; -import {CryptoApi} from "./cryptoApi"; - -"use strict"; - -export interface CoinWithDenom { - coin: Coin; - denom: Denomination; -} - - -@Checkable.Class -export class KeysJson { - @Checkable.List(Checkable.Value(Denomination)) - denoms: Denomination[]; - - @Checkable.String - master_public_key: string; - - @Checkable.Any - auditors: any[]; - - @Checkable.String - list_issue_date: string; - - @Checkable.Any - signkeys: any; - - @Checkable.String - eddsa_pub: string; - - @Checkable.String - eddsa_sig: string; - - static checked: (obj: any) => KeysJson; -} - - -@Checkable.Class -export class CreateReserveRequest { - /** - * The initial amount for the reserve. - */ - @Checkable.Value(AmountJson) - amount: AmountJson; - - /** - * Exchange URL where the bank should create the reserve. - */ - @Checkable.String - exchange: string; - - static checked: (obj: any) => CreateReserveRequest; -} - - -@Checkable.Class -export class ConfirmReserveRequest { - /** - * Public key of then reserve that should be marked - * as confirmed. - */ - @Checkable.String - reservePub: string; - - static checked: (obj: any) => ConfirmReserveRequest; -} - - -@Checkable.Class -export class Offer { - @Checkable.Value(Contract) - contract: Contract; - - @Checkable.String - merchant_sig: string; - - @Checkable.String - H_contract: string; - - @Checkable.Number - offer_time: number; - - /** - * Serial ID when the offer is stored in the wallet DB. - */ - @Checkable.Optional(Checkable.Number) - id?: number; - - static checked: (obj: any) => Offer; -} - -export interface HistoryRecord { - type: string; - timestamp: number; - subjectId?: string; - detail: any; - level: HistoryLevel; -} - - -interface ExchangeCoins { - [exchangeUrl: string]: CoinWithDenom[]; -} - -interface PayReq { - amount: AmountJson; - coins: CoinPaySig[]; - H_contract: string; - max_fee: AmountJson; - merchant_sig: string; - exchange: string; - refund_deadline: string; - timestamp: string; - transaction_id: number; - pay_deadline: string; - /** - * Merchant instance identifier that should receive the - * payment, if applicable. - */ - instance?: string; -} - -interface Transaction { - contractHash: string; - contract: Contract; - payReq: PayReq; - merchantSig: string; - - /** - * The transaction isn't active anymore, it's either successfully paid - * or refunded/aborted. - */ - finished: boolean; -} - -export enum HistoryLevel { - Trace = 1, - Developer = 2, - Expert = 3, - User = 4, -} - - -export interface Badge { - setText(s: string): void; - setColor(c: string): void; - startBusy(): void; - stopBusy(): void; -} - - -function setTimeout(f: any, t: number) { - return chrome.extension.getBackgroundPage().setTimeout(f, t); -} - - -function isWithdrawableDenom(d: Denomination) { - const now_sec = (new Date).getTime() / 1000; - const stamp_withdraw_sec = getTalerStampSec(d.stamp_expire_withdraw); - const stamp_start_sec = getTalerStampSec(d.stamp_start); - // Withdraw if still possible to withdraw within a minute - if ((stamp_withdraw_sec + 60 > now_sec) && (now_sec >= stamp_start_sec)) { - return true; - } - return false; -} - - -/** - * Result of updating exisiting information - * about an exchange with a new '/keys' response. - */ -interface KeyUpdateInfo { - updatedExchangeInfo: IExchangeInfo; - addedDenominations: Denomination[]; - removedDenominations: Denomination[]; -} - - -/** - * Get a list of denominations (with repetitions possible) - * whose total value is as close as possible to the available - * amount, but never larger. - */ -function getWithdrawDenomList(amountAvailable: AmountJson, - denoms: Denomination[]): Denomination[] { - let remaining = Amounts.copy(amountAvailable); - const ds: Denomination[] = []; - - denoms = denoms.filter(isWithdrawableDenom); - denoms.sort((d1, d2) => Amounts.cmp(d2.value, d1.value)); - - // This is an arbitrary number of coins - // we can withdraw in one go. It's not clear if this limit - // is useful ... - for (let i = 0; i < 1000; i++) { - let found = false; - for (let d of denoms) { - let cost = Amounts.add(d.value, d.fee_withdraw).amount; - if (Amounts.cmp(remaining, cost) < 0) { - continue; - } - found = true; - remaining = Amounts.sub(remaining, cost).amount; - ds.push(d); - break; - } - if (!found) { - break; - } - } - return ds; -} - - -export namespace Stores { - class ExchangeStore extends Store { - constructor() { - super("exchanges", {keyPath: "baseUrl"}); - } - - pubKeyIndex = new Index(this, "pubKey", "masterPublicKey"); - } - - class CoinsStore extends Store { - constructor() { - super("coins", {keyPath: "coinPub"}); - } - - exchangeBaseUrlIndex = new Index(this, "exchangeBaseUrl", "exchangeBaseUrl"); - } - - class HistoryStore extends Store { - constructor() { - super("history", { - keyPath: "id", - autoIncrement: true - }); - } - - timestampIndex = new Index(this, "timestamp", "timestamp"); - } - - class OffersStore extends Store { - constructor() { - super("offers", { - keyPath: "id", - autoIncrement: true - }); - } - } - - class TransactionsStore extends Store { - constructor() { - super("transactions", {keyPath: "contractHash"}); - } - - repurchaseIndex = new Index<[string,string],Transaction>(this, "repurchase", [ - "contract.merchant_pub", - "contract.repurchase_correlation_id" - ]); - } - - export let exchanges: ExchangeStore = new ExchangeStore(); - export let transactions: TransactionsStore = new TransactionsStore(); - export let reserves: Store = new Store("reserves", {keyPath: "reserve_pub"}); - export let coins: CoinsStore = new CoinsStore(); - export let refresh: Store = new Store("refresh", {keyPath: "meltCoinPub"}); - export let history: HistoryStore = new HistoryStore(); - export let offers: OffersStore = new OffersStore(); - export let precoins: Store = new Store("precoins", {keyPath: "coinPub"}); -} - - -export class Wallet { - private db: IDBDatabase; - private http: HttpRequestLibrary; - private badge: Badge; - private notifier: Notifier; - public cryptoApi: CryptoApi; - - /** - * Set of identifiers for running operations. - */ - private runningOperations: Set = new Set(); - - q(): QueryRoot { - return new QueryRoot(this.db); - } - - constructor(db: IDBDatabase, - http: HttpRequestLibrary, - badge: Badge, - notifier: Notifier) { - this.db = db; - this.http = http; - this.badge = badge; - this.notifier = notifier; - this.cryptoApi = new CryptoApi(); - - this.resumePendingFromDb(); - } - - - private startOperation(operationId: string) { - this.runningOperations.add(operationId); - this.badge.startBusy(); - } - - private stopOperation(operationId: string) { - this.runningOperations.delete(operationId); - if (this.runningOperations.size == 0) { - this.badge.stopBusy(); - } - } - - async updateExchanges(): Promise { - console.log("updating exchanges"); - - let exchangesUrls = await this.q() - .iter(Stores.exchanges) - .map((e) => e.baseUrl) - .toArray(); - - for (let url of exchangesUrls) { - this.updateExchangeFromUrl(url) - .catch((e) => { - console.error("updating exchange failed", e); - }); - } - } - - /** - * Resume various pending operations that are pending - * by looking at the database. - */ - private resumePendingFromDb(): void { - console.log("resuming pending operations from db"); - - this.q() - .iter(Stores.reserves) - .reduce((reserve) => { - console.log("resuming reserve", reserve.reserve_pub); - this.processReserve(reserve); - }); - - this.q() - .iter(Stores.precoins) - .reduce((preCoin) => { - console.log("resuming precoin"); - this.processPreCoin(preCoin); - }); - - this.q() - .iter(Stores.refresh) - .reduce((r: RefreshSession) => { - this.continueRefreshSession(r); - }); - - // FIXME: optimize via index - this.q() - .iter(Stores.coins) - .reduce((c: Coin) => { - if (c.dirty && !c.transactionPending) { - this.refresh(c.coinPub); - } - }); - } - - - /** - * Get exchanges and associated coins that are still spendable, - * but only if the sum the coins' remaining value exceeds the payment amount. - */ - private async getPossibleExchangeCoins(paymentAmount: AmountJson, - depositFeeLimit: AmountJson, - allowedExchanges: ExchangeHandle[]): Promise { - // Mapping from exchange base URL to list of coins together with their - // denomination - let m: ExchangeCoins = {}; - - let x: number; - - function storeExchangeCoin(mc: JoinResult, - url: string) { - let exchange: IExchangeInfo = mc.left; - console.log("got coin for exchange", url); - let coin: Coin = mc.right; - if (coin.suspended) { - console.log("skipping suspended coin", - coin.denomPub, - "from exchange", - exchange.baseUrl); - return; - } - let denom = exchange.active_denoms.find((e) => e.denom_pub === coin.denomPub); - if (!denom) { - console.warn("denom not found (database inconsistent)"); - return; - } - if (denom.value.currency !== paymentAmount.currency) { - console.warn("same pubkey for different currencies"); - return; - } - let cd = {coin, denom}; - let x = m[url]; - if (!x) { - m[url] = [cd]; - } else { - x.push(cd); - } - } - - // Make sure that we don't look up coins - // for the same URL twice ... - let handledExchanges = new Set(); - - let ps = flatMap(allowedExchanges, (info: ExchangeHandle) => { - if (handledExchanges.has(info.url)) { - return []; - } - handledExchanges.add(info.url); - console.log("Checking for merchant's exchange", JSON.stringify(info)); - return [ - this.q() - .iterIndex(Stores.exchanges.pubKeyIndex, info.master_pub) - .indexJoin(Stores.coins.exchangeBaseUrlIndex, - (exchange) => exchange.baseUrl) - .reduce((x) => storeExchangeCoin(x, info.url)) - ]; - }); - - await Promise.all(ps); - - let ret: ExchangeCoins = {}; - - if (Object.keys(m).length == 0) { - console.log("not suitable exchanges found"); - } - - console.log("exchange coins:"); - console.dir(m); - - // We try to find the first exchange where we have - // enough coins to cover the paymentAmount with fees - // under depositFeeLimit - - nextExchange: - for (let key in m) { - let coins = m[key]; - // Sort by ascending deposit fee - coins.sort((o1, o2) => Amounts.cmp(o1.denom.fee_deposit, - o2.denom.fee_deposit)); - let maxFee = Amounts.copy(depositFeeLimit); - let minAmount = Amounts.copy(paymentAmount); - let accFee = Amounts.copy(coins[0].denom.fee_deposit); - let accAmount = Amounts.getZero(coins[0].coin.currentAmount.currency); - let usableCoins: CoinWithDenom[] = []; - nextCoin: - for (let i = 0; i < coins.length; i++) { - let coinAmount = Amounts.copy(coins[i].coin.currentAmount); - let coinFee = coins[i].denom.fee_deposit; - if (Amounts.cmp(coinAmount, coinFee) <= 0) { - continue nextCoin; - } - accFee = Amounts.add(accFee, coinFee).amount; - accAmount = Amounts.add(accAmount, coinAmount).amount; - if (Amounts.cmp(accFee, maxFee) >= 0) { - // FIXME: if the fees are too high, we have - // to cover them ourselves .... - console.log("too much fees"); - continue nextExchange; - } - usableCoins.push(coins[i]); - if (Amounts.cmp(accAmount, minAmount) >= 0) { - ret[key] = usableCoins; - continue nextExchange; - } - } - } - return ret; - } - - - /** - * Record all information that is necessary to - * pay for a contract in the wallet's database. - */ - private async recordConfirmPay(offer: Offer, - payCoinInfo: PayCoinInfo, - chosenExchange: string): Promise { - let payReq: PayReq = { - amount: offer.contract.amount, - coins: payCoinInfo.map((x) => x.sig), - H_contract: offer.H_contract, - max_fee: offer.contract.max_fee, - merchant_sig: offer.merchant_sig, - exchange: URI(chosenExchange).href(), - refund_deadline: offer.contract.refund_deadline, - pay_deadline: offer.contract.pay_deadline, - timestamp: offer.contract.timestamp, - transaction_id: offer.contract.transaction_id, - instance: offer.contract.merchant.instance - }; - let t: Transaction = { - contractHash: offer.H_contract, - contract: offer.contract, - payReq: payReq, - merchantSig: offer.merchant_sig, - finished: false, - }; - - let historyEntry: HistoryRecord = { - type: "pay", - timestamp: (new Date).getTime(), - subjectId: `contract-${offer.H_contract}`, - detail: { - merchantName: offer.contract.merchant.name, - amount: offer.contract.amount, - contractHash: offer.H_contract, - fulfillmentUrl: offer.contract.fulfillment_url, - }, - level: HistoryLevel.User - }; - - await this.q() - .put(Stores.transactions, t) - .put(Stores.history, historyEntry) - .putAll(Stores.coins, payCoinInfo.map((pci) => pci.updatedCoin)) - .finish(); - - this.notifier.notify(); - } - - - async putHistory(historyEntry: HistoryRecord): Promise { - await this.q().put(Stores.history, historyEntry).finish(); - this.notifier.notify(); - } - - - async saveOffer(offer: Offer): Promise { - console.log(`saving offer in wallet.ts`); - let id = await this.q().putWithResult(Stores.offers, offer); - this.notifier.notify(); - console.log(`saved offer with id ${id}`); - if (typeof id !== "number") { - throw Error("db schema wrong"); - } - return id; - } - - - /** - * Add a contract to the wallet and sign coins, - * but do not send them yet. - */ - async confirmPay(offer: Offer): Promise { - console.log("executing confirmPay"); - - let transaction = await this.q().get(Stores.transactions, offer.H_contract); - - if (transaction) { - // Already payed ... - return {}; - } - - let mcs = await this.getPossibleExchangeCoins(offer.contract.amount, - offer.contract.max_fee, - offer.contract.exchanges); - - if (Object.keys(mcs).length == 0) { - console.log("not confirming payment, insufficient coins"); - return { - error: "coins-insufficient", - }; - } - let exchangeUrl = Object.keys(mcs)[0]; - - let ds = await this.cryptoApi.signDeposit(offer, mcs[exchangeUrl]); - await this.recordConfirmPay(offer, - ds, - exchangeUrl); - return {}; - } - - - /** - * Add a contract to the wallet and sign coins, - * but do not send them yet. - */ - async checkPay(offer: Offer): Promise { - // First check if we already payed for it. - let transaction = await this.q().get(Stores.transactions, offer.H_contract); - if (transaction) { - return {isPayed: true}; - } - - // If not already payed, check if we could pay for it. - let mcs = await this.getPossibleExchangeCoins(offer.contract.amount, - offer.contract.max_fee, - offer.contract.exchanges); - - if (Object.keys(mcs).length == 0) { - console.log("not confirming payment, insufficient coins"); - return { - error: "coins-insufficient", - }; - } - return {isPayed: false}; - } - - - /** - * Retrieve all necessary information for looking up the contract - * with the given hash. - */ - async executePayment(H_contract: string): Promise { - let t = await this.q().get(Stores.transactions, H_contract); - if (!t) { - return { - success: false, - contractFound: false, - } - } - let resp = { - success: true, - payReq: t.payReq, - contract: t.contract, - }; - return resp; - } - - - /** - * First fetch information requred to withdraw from the reserve, - * then deplete the reserve, withdrawing coins until it is empty. - */ - private async processReserve(reserveRecord: ReserveRecord, - retryDelayMs: number = 250): Promise { - const opId = "reserve-" + reserveRecord.reserve_pub; - this.startOperation(opId); - - try { - let exchange = await this.updateExchangeFromUrl(reserveRecord.exchange_base_url); - let reserve = await this.updateReserve(reserveRecord.reserve_pub, - exchange); - let n = await this.depleteReserve(reserve, exchange); - - if (n != 0) { - let depleted: HistoryRecord = { - type: "depleted-reserve", - subjectId: `reserve-progress-${reserveRecord.reserve_pub}`, - timestamp: (new Date).getTime(), - detail: { - exchangeBaseUrl: reserveRecord.exchange_base_url, - reservePub: reserveRecord.reserve_pub, - requestedAmount: reserveRecord.requested_amount, - currentAmount: reserveRecord.current_amount, - }, - level: HistoryLevel.User - }; - await this.q().put(Stores.history, depleted).finish(); - } - } catch (e) { - // random, exponential backoff truncated at 3 minutes - let nextDelay = Math.min(2 * retryDelayMs + retryDelayMs * Math.random(), - 3000 * 60); - console.warn(`Failed to deplete reserve, trying again in ${retryDelayMs} ms`); - setTimeout(() => this.processReserve(reserveRecord, nextDelay), - retryDelayMs); - } finally { - this.stopOperation(opId); - } - } - - - private async processPreCoin(preCoin: PreCoin, - retryDelayMs = 100): Promise { - - let exchange = await this.q().get(Stores.exchanges, - preCoin.exchangeBaseUrl); - if (!exchange) { - console.error("db inconsistend: exchange for precoin not found"); - return; - } - let denom = exchange.all_denoms.find((d) => d.denom_pub == preCoin.denomPub); - if (!denom) { - console.error("db inconsistent: denom for precoin not found"); - return; - } - - try { - const coin = await this.withdrawExecute(preCoin); - - const mutateReserve = (r: ReserveRecord) => { - - console.log(`before committing coin: current ${amountToPretty(r.current_amount!)}, precoin: ${amountToPretty( - r.precoin_amount)})}`); - - let x = Amounts.sub(r.precoin_amount, - preCoin.coinValue, - denom!.fee_withdraw); - if (x.saturated) { - console.error("database inconsistent"); - throw AbortTransaction; - } - r.precoin_amount = x.amount; - return r; - }; - - let historyEntry: HistoryRecord = { - type: "withdraw", - timestamp: (new Date).getTime(), - level: HistoryLevel.Expert, - detail: { - coinPub: coin.coinPub, - } - }; - - await this.q() - .mutate(Stores.reserves, preCoin.reservePub, mutateReserve) - .delete("precoins", coin.coinPub) - .add(Stores.coins, coin) - .add(Stores.history, historyEntry) - .finish(); - - this.notifier.notify(); - } catch (e) { - console.error("Failed to withdraw coin from precoin, retrying in", - retryDelayMs, - "ms", e); - // exponential backoff truncated at one minute - let nextRetryDelayMs = Math.min(retryDelayMs * 2, 1000 * 60); - setTimeout(() => this.processPreCoin(preCoin, nextRetryDelayMs), - retryDelayMs); - } - } - - - /** - * Create a reserve, but do not flag it as confirmed yet. - */ - async createReserve(req: CreateReserveRequest): Promise { - let keypair = await this.cryptoApi.createEddsaKeypair(); - const now = (new Date).getTime(); - const canonExchange = canonicalizeBaseUrl(req.exchange); - - const reserveRecord: ReserveRecord = { - reserve_pub: keypair.pub, - reserve_priv: keypair.priv, - exchange_base_url: canonExchange, - created: now, - last_query: null, - current_amount: null, - requested_amount: req.amount, - confirmed: false, - precoin_amount: Amounts.getZero(req.amount.currency), - }; - - const historyEntry = { - type: "create-reserve", - level: HistoryLevel.Expert, - timestamp: now, - subjectId: `reserve-progress-${reserveRecord.reserve_pub}`, - detail: { - requestedAmount: req.amount, - reservePub: reserveRecord.reserve_pub, - } - }; - - await this.q() - .put(Stores.reserves, reserveRecord) - .put(Stores.history, historyEntry) - .finish(); - - let r: CreateReserveResponse = { - exchange: canonExchange, - reservePub: keypair.pub, - }; - return r; - } - - - /** - * Mark an existing reserve as confirmed. The wallet will start trying - * to withdraw from that reserve. This may not immediately succeed, - * since the exchange might not know about the reserve yet, even though the - * bank confirmed its creation. - * - * A confirmed reserve should be shown to the user in the UI, while - * an unconfirmed reserve should be hidden. - */ - async confirmReserve(req: ConfirmReserveRequest): Promise { - const now = (new Date).getTime(); - let reserve: ReserveRecord|undefined = await ( - this.q().get(Stores.reserves, - req.reservePub)); - if (!reserve) { - console.error("Unable to confirm reserve, not found in DB"); - return; - } - console.log("reserve confirmed"); - const historyEntry: HistoryRecord = { - type: "confirm-reserve", - timestamp: now, - subjectId: `reserve-progress-${reserve.reserve_pub}`, - detail: { - exchangeBaseUrl: reserve.exchange_base_url, - reservePub: req.reservePub, - requestedAmount: reserve.requested_amount, - }, - level: HistoryLevel.User, - }; - reserve.confirmed = true; - await this.q() - .put(Stores.reserves, reserve) - .put(Stores.history, historyEntry) - .finish(); - this.notifier.notify(); - - this.processReserve(reserve); - } - - - private async withdrawExecute(pc: PreCoin): Promise { - let reserve = await this.q().get(Stores.reserves, - pc.reservePub); - - if (!reserve) { - throw Error("db inconsistent"); - } - - 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(reserve.exchange_base_url); - let resp = await this.http.postJson(reqUrl, wd); - - - if (resp.status != 200) { - throw new RequestException({ - hint: "Withdrawal failed", - status: resp.status - }); - } - let r = JSON.parse(resp.responseText); - let denomSig = await this.cryptoApi.rsaUnblind(r.ev_sig, - pc.blindingKey, - pc.denomPub); - let coin: Coin = { - coinPub: pc.coinPub, - coinPriv: pc.coinPriv, - denomPub: pc.denomPub, - denomSig: denomSig, - currentAmount: pc.coinValue, - exchangeBaseUrl: pc.exchangeBaseUrl, - dirty: false, - transactionPending: false, - }; - return coin; - } - - - /** - * Withdraw coins from a reserve until it is empty. - */ - private async depleteReserve(reserve: ReserveRecord, - exchange: IExchangeInfo): Promise { - if (!reserve.current_amount) { - throw Error("can't withdraw when amount is unknown"); - } - let denomsAvailable: Denomination[] = Array.from(exchange.active_denoms); - let denomsForWithdraw = getWithdrawDenomList(reserve.current_amount!, - denomsAvailable); - - let ps = denomsForWithdraw.map(async(denom) => { - function mutateReserve(r: ReserveRecord): ReserveRecord { - let currentAmount = r.current_amount; - if (!currentAmount) { - throw Error("can't withdraw when amount is unknown"); - } - r.precoin_amount = Amounts.add(r.precoin_amount, - denom.value, - denom.fee_withdraw).amount; - let result = Amounts.sub(currentAmount, - denom.value, - denom.fee_withdraw); - if (result.saturated) { - console.error("can't create precoin, saturated"); - throw AbortTransaction; - } - r.current_amount = result.amount; - - console.log(`after creating precoin: current ${amountToPretty(r.current_amount)}, precoin: ${amountToPretty( - r.precoin_amount)})}`); - - return r; - } - - let preCoin = await this.cryptoApi - .createPreCoin(denom, reserve); - await this.q() - .put(Stores.precoins, preCoin) - .mutate(Stores.reserves, reserve.reserve_pub, mutateReserve); - await this.processPreCoin(preCoin); - }); - - await Promise.all(ps); - return ps.length; - } - - - /** - * Update the information about a reserve that is stored in the wallet - * by quering the reserve's exchange. - */ - private async updateReserve(reservePub: string, - exchange: IExchangeInfo): Promise { - let reserve = await this.q() - .get(Stores.reserves, reservePub); - if (!reserve) { - throw Error("reserve not in db"); - } - let reqUrl = URI("reserve/status").absoluteTo(exchange.baseUrl); - reqUrl.query({'reserve_pub': reservePub}); - let resp = await this.http.get(reqUrl); - if (resp.status != 200) { - throw Error(); - } - let reserveInfo = JSON.parse(resp.responseText); - if (!reserveInfo) { - throw Error(); - } - let oldAmount = reserve.current_amount; - let newAmount = reserveInfo.balance; - reserve.current_amount = reserveInfo.balance; - let historyEntry = { - type: "reserve-update", - timestamp: (new Date).getTime(), - subjectId: `reserve-progress-${reserve.reserve_pub}`, - detail: { - reservePub, - requestedAmount: reserve.requested_amount, - oldAmount, - newAmount - } - }; - await this.q() - .put(Stores.reserves, reserve) - .finish(); - this.notifier.notify(); - return reserve; - } - - - /** - * Get the wire information for the exchange with the given base URL. - */ - async getWireInfo(exchangeBaseUrl: string): Promise { - exchangeBaseUrl = canonicalizeBaseUrl(exchangeBaseUrl); - let reqUrl = URI("wire").absoluteTo(exchangeBaseUrl); - let resp = await this.http.get(reqUrl); - - if (resp.status != 200) { - throw Error("/wire request failed"); - } - - let wiJson = JSON.parse(resp.responseText); - if (!wiJson) { - throw Error("/wire response malformed") - } - return wiJson; - } - - async getReserveCreationInfo(baseUrl: string, - amount: AmountJson): Promise { - let exchangeInfo = await this.updateExchangeFromUrl(baseUrl); - - let selectedDenoms = getWithdrawDenomList(amount, - exchangeInfo.active_denoms); - let acc = Amounts.getZero(amount.currency); - for (let d of selectedDenoms) { - acc = Amounts.add(acc, d.fee_withdraw).amount; - } - let actualCoinCost = selectedDenoms - .map((d: Denomination) => Amounts.add(d.value, - d.fee_withdraw).amount) - .reduce((a, b) => Amounts.add(a, b).amount); - - let wireInfo = await this.getWireInfo(baseUrl); - - let ret: ReserveCreationInfo = { - exchangeInfo, - selectedDenoms, - wireInfo, - withdrawFee: acc, - overhead: Amounts.sub(amount, actualCoinCost).amount, - }; - return ret; - } - - - /** - * Update or add exchange DB entry by fetching the /keys information. - * Optionally link the reserve entry to the new or existing - * exchange entry in then DB. - */ - async updateExchangeFromUrl(baseUrl: string): Promise { - baseUrl = canonicalizeBaseUrl(baseUrl); - let reqUrl = URI("keys").absoluteTo(baseUrl); - let resp = await this.http.get(reqUrl); - if (resp.status != 200) { - throw Error("/keys request failed"); - } - let exchangeKeysJson = KeysJson.checked(JSON.parse(resp.responseText)); - return this.updateExchangeFromJson(baseUrl, exchangeKeysJson); - } - - private async suspendCoins(exchangeInfo: IExchangeInfo): Promise { - let suspendedCoins = await ( - this.q() - .iterIndex(Stores.coins.exchangeBaseUrlIndex, exchangeInfo.baseUrl) - .reduce((coin: Coin, suspendedCoins: Coin[]) => { - if (!exchangeInfo.active_denoms.find((c) => c.denom_pub == coin.denomPub)) { - return Array.prototype.concat(suspendedCoins, [coin]); - } - return Array.prototype.concat(suspendedCoins); - }, [])); - - let q = this.q(); - suspendedCoins.map((c) => { - console.log("suspending coin", c); - c.suspended = true; - q.put(Stores.coins, c); - }); - await q.finish(); - } - - - private async updateExchangeFromJson(baseUrl: string, - exchangeKeysJson: KeysJson): Promise { - const updateTimeSec = getTalerStampSec(exchangeKeysJson.list_issue_date); - if (updateTimeSec === null) { - throw Error("invalid update time"); - } - - let r = await this.q().get(Stores.exchanges, baseUrl); - - let exchangeInfo: IExchangeInfo; - - if (!r) { - exchangeInfo = { - baseUrl, - all_denoms: [], - active_denoms: [], - last_update_time: updateTimeSec, - masterPublicKey: exchangeKeysJson.master_public_key, - }; - console.log("making fresh exchange"); - } else { - if (updateTimeSec < r.last_update_time) { - console.log("outdated /keys, not updating"); - return r - } - exchangeInfo = r; - console.log("updating old exchange"); - } - - let updatedExchangeInfo = await this.updateExchangeInfo(exchangeInfo, - exchangeKeysJson); - await this.suspendCoins(updatedExchangeInfo); - - await this.q() - .put(Stores.exchanges, updatedExchangeInfo) - .finish(); - - return updatedExchangeInfo; - } - - - private async updateExchangeInfo(exchangeInfo: IExchangeInfo, - newKeys: KeysJson): Promise { - if (exchangeInfo.masterPublicKey != newKeys.master_public_key) { - throw Error("public keys do not match"); - } - - exchangeInfo.active_denoms = []; - - let denomsToCheck = newKeys.denoms.filter((newDenom) => { - // did we find the new denom in the list of all (old) denoms? - let found = false; - for (let oldDenom of exchangeInfo.all_denoms) { - if (oldDenom.denom_pub === newDenom.denom_pub) { - let a: any = Object.assign({}, oldDenom); - let b: any = Object.assign({}, newDenom); - // pub hash is only there for convenience in the wallet - delete a["pub_hash"]; - delete b["pub_hash"]; - if (!deepEquals(a, b)) { - console.error("denomination parameters were modified, old/new:"); - console.dir(a); - console.dir(b); - // FIXME: report to auditors - } - found = true; - break; - } - } - - if (found) { - exchangeInfo.active_denoms.push(newDenom); - // No need to check signatures - return false; - } - return true; - }); - - let ps = denomsToCheck.map(async(denom) => { - let valid = await this.cryptoApi - .isValidDenom(denom, - exchangeInfo.masterPublicKey); - if (!valid) { - console.error("invalid denomination", - denom, - "with key", - exchangeInfo.masterPublicKey); - // FIXME: report to auditors - } - exchangeInfo.active_denoms.push(denom); - exchangeInfo.all_denoms.push(denom); - }); - - await Promise.all(ps); - - return exchangeInfo; - } - - - /** - * Retrieve a mapping from currency name to the amount - * that is currenctly available for spending in the wallet. - */ - async getBalances(): Promise { - function ensureEntry(balance: WalletBalance, currency: string) { - let entry: WalletBalanceEntry|undefined = balance[currency]; - let z = Amounts.getZero(currency); - if (!entry) { - balance[currency] = entry = { - available: z, - pendingIncoming: z, - pendingPayment: z, - }; - } - return entry; - } - - function collectBalances(c: Coin, balance: WalletBalance) { - if (c.suspended) { - return balance; - } - let currency = c.currentAmount.currency; - let entry = ensureEntry(balance, currency); - entry.available = Amounts.add(entry.available, c.currentAmount).amount; - return balance; - } - - function collectPendingWithdraw(r: ReserveRecord, balance: WalletBalance) { - if (!r.confirmed) { - return balance; - } - let entry = ensureEntry(balance, r.requested_amount.currency); - let amount = r.current_amount; - if (!amount) { - amount = r.requested_amount; - } - amount = Amounts.add(amount, r.precoin_amount).amount; - if (Amounts.cmp(smallestWithdraw[r.exchange_base_url], amount) < 0) { - entry.pendingIncoming = Amounts.add(entry.pendingIncoming, - amount).amount; - } - return balance; - } - - function collectPendingRefresh(r: RefreshSession, balance: WalletBalance) { - if (!r.finished) { - return balance; - } - let entry = ensureEntry(balance, r.valueWithFee.currency); - entry.pendingIncoming = Amounts.add(entry.pendingIncoming, - r.valueOutput).amount; - - return balance; - } - - function collectPayments(t: Transaction, balance: WalletBalance) { - if (t.finished) { - return balance; - } - let entry = ensureEntry(balance, t.contract.amount.currency); - entry.pendingPayment = Amounts.add(entry.pendingPayment, - t.contract.amount).amount; - - return balance; - } - - function collectSmallestWithdraw(e: IExchangeInfo, sw: any) { - let min: AmountJson|undefined; - for (let d of e.active_denoms) { - let v = Amounts.add(d.value, d.fee_withdraw).amount; - if (!min) { - min = v; - continue; - } - if (Amounts.cmp(v, min) < 0) { - min = v; - } - } - sw[e.baseUrl] = min; - return sw; - } - - let balance = {}; - // Mapping from exchange pub to smallest - // possible amount we can withdraw - let smallestWithdraw: {[baseUrl: string]: AmountJson} = {}; - - smallestWithdraw = await (this.q() - .iter(Stores.exchanges) - .reduce(collectSmallestWithdraw, {})); - - console.log("smallest withdraws", smallestWithdraw); - - let tx = this.q(); - tx.iter(Stores.coins) - .reduce(collectBalances, balance); - tx.iter(Stores.refresh) - .reduce(collectPendingRefresh, balance); - tx.iter(Stores.reserves) - .reduce(collectPendingWithdraw, balance); - tx.iter(Stores.transactions) - .reduce(collectPayments, balance); - await tx.finish(); - return balance; - - } - - - async createRefreshSession(oldCoinPub: string): Promise { - let coin = await this.q().get(Stores.coins, oldCoinPub); - - if (!coin) { - throw Error("coin not found"); - } - - let exchange = await this.updateExchangeFromUrl(coin.exchangeBaseUrl); - - if (!exchange) { - throw Error("db inconsistent"); - } - - let oldDenom = exchange.all_denoms.find((d) => d.denom_pub == coin!.denomPub); - - if (!oldDenom) { - throw Error("db inconsistent"); - } - - let availableDenoms: Denomination[] = exchange.active_denoms; - - let availableAmount = Amounts.sub(coin.currentAmount, - oldDenom.fee_refresh).amount; - - let newCoinDenoms = getWithdrawDenomList(availableAmount, - availableDenoms); - - console.log("refreshing into", newCoinDenoms); - - if (newCoinDenoms.length == 0) { - console.log("not refreshing, value too small"); - return undefined; - } - - - let refreshSession: RefreshSession = await ( - this.cryptoApi.createRefreshSession(exchange.baseUrl, - 3, - coin, - newCoinDenoms, - oldDenom.fee_refresh)); - - function mutateCoin(c: Coin): Coin { - let r = Amounts.sub(c.currentAmount, - refreshSession.valueWithFee); - if (r.saturated) { - // Something else must have written the coin value - throw AbortTransaction; - } - c.currentAmount = r.amount; - return c; - } - - await this.q() - .put(Stores.refresh, refreshSession) - .mutate(Stores.coins, coin.coinPub, mutateCoin) - .finish(); - - return refreshSession; - } - - - async refresh(oldCoinPub: string): Promise { - let refreshSession: RefreshSession|undefined; - let oldSession = await this.q().get(Stores.refresh, oldCoinPub); - if (oldSession) { - refreshSession = oldSession; - } else { - refreshSession = await this.createRefreshSession(oldCoinPub); - } - if (!refreshSession) { - // refreshing not necessary - return; - } - this.continueRefreshSession(refreshSession); - } - - async continueRefreshSession(refreshSession: RefreshSession) { - if (refreshSession.finished) { - return; - } - if (typeof refreshSession.norevealIndex !== "number") { - let coinPub = refreshSession.meltCoinPub; - await this.refreshMelt(refreshSession); - let r = await this.q().get(Stores.refresh, coinPub); - if (!r) { - throw Error("refresh session does not exist anymore"); - } - refreshSession = r; - } - - await this.refreshReveal(refreshSession); - } - - - async refreshMelt(refreshSession: RefreshSession): Promise { - if (refreshSession.norevealIndex != undefined) { - console.error("won't melt again"); - return; - } - - let coin = await this.q().get(Stores.coins, - refreshSession.meltCoinPub); - if (!coin) { - console.error("can't melt coin, it does not exist"); - return; - } - - let reqUrl = URI("refresh/melt").absoluteTo(refreshSession.exchangeBaseUrl); - let meltCoin = { - coin_pub: coin.coinPub, - denom_pub: coin.denomPub, - denom_sig: coin.denomSig, - confirm_sig: refreshSession.confirmSig, - value_with_fee: refreshSession.valueWithFee, - }; - let coinEvs = refreshSession.preCoinsForGammas.map((x) => x.map((y) => y.coinEv)); - let req = { - "new_denoms": refreshSession.newDenoms, - "melt_coin": meltCoin, - "transfer_pubs": refreshSession.transferPubs, - "coin_evs": coinEvs, - }; - console.log("melt request:", req); - let resp = await this.http.postJson(reqUrl, req); - - console.log("melt request:", req); - console.log("melt response:", resp.responseText); - - if (resp.status != 200) { - console.error(resp.responseText); - throw Error("refresh failed"); - } - - let respJson = JSON.parse(resp.responseText); - - if (!respJson) { - throw Error("exchange responded with garbage"); - } - - let norevealIndex = respJson.noreveal_index; - - if (typeof norevealIndex != "number") { - throw Error("invalid response"); - } - - refreshSession.norevealIndex = norevealIndex; - - await this.q().put(Stores.refresh, refreshSession).finish(); - } - - - async refreshReveal(refreshSession: RefreshSession): Promise { - let norevealIndex = refreshSession.norevealIndex; - if (norevealIndex == undefined) { - throw Error("can't reveal without melting first"); - } - let privs = Array.from(refreshSession.transferPrivs); - privs.splice(norevealIndex, 1); - - let req = { - "session_hash": refreshSession.hash, - "transfer_privs": privs, - }; - - let reqUrl = URI("refresh/reveal") - .absoluteTo(refreshSession.exchangeBaseUrl); - console.log("reveal request:", req); - let resp = await this.http.postJson(reqUrl, req); - - console.log("session:", refreshSession); - console.log("reveal response:", resp); - - if (resp.status != 200) { - console.log("error: /refresh/reveal returned status " + resp.status); - return; - } - - let respJson = JSON.parse(resp.responseText); - - if (!respJson.ev_sigs || !Array.isArray(respJson.ev_sigs)) { - console.log("/refresh/reveal did not contain ev_sigs"); - } - - let exchange = await this.q().get(Stores.exchanges, - refreshSession.exchangeBaseUrl); - if (!exchange) { - console.error(`exchange ${refreshSession.exchangeBaseUrl} not found`); - return; - } - - let coins: Coin[] = []; - - for (let i = 0; i < respJson.ev_sigs.length; i++) { - let denom = exchange.all_denoms.find((d) => d.denom_pub == refreshSession.newDenoms[i]); - if (!denom) { - console.error("denom not found"); - continue; - } - let pc = refreshSession.preCoinsForGammas[refreshSession.norevealIndex!][i]; - let denomSig = await this.cryptoApi.rsaUnblind(respJson.ev_sigs[i].ev_sig, - pc.blindingKey, - denom.denom_pub); - let coin: Coin = { - coinPub: pc.publicKey, - coinPriv: pc.privateKey, - denomPub: denom.denom_pub, - denomSig: denomSig, - currentAmount: denom.value, - exchangeBaseUrl: refreshSession.exchangeBaseUrl, - dirty: false, - transactionPending: false, - }; - - coins.push(coin); - } - - refreshSession.finished = true; - - await this.q() - .putAll(Stores.coins, coins) - .put(Stores.refresh, refreshSession) - .finish(); - } - - - /** - * Retrive the full event history for this wallet. - */ - async getHistory(): Promise { - function collect(x: any, acc: any) { - acc.push(x); - return acc; - } - - let history = await ( - this.q() - .iterIndex(Stores.history.timestampIndex) - .reduce(collect, [])); - - return {history}; - } - - - async getOffer(offerId: number): Promise { - let offer = await this.q() .get(Stores.offers, offerId); - return offer; - } - - async getExchanges(): Promise { - return this.q() - .iter(Stores.exchanges) - .flatMap((e) => [e]) - .toArray(); - } - - async getReserves(exchangeBaseUrl: string): Promise { - return this.q() - .iter(Stores.reserves) - .filter((r: ReserveRecord) => r.exchange_base_url === exchangeBaseUrl) - .toArray(); - } - - async getCoins(exchangeBaseUrl: string): Promise { - return this.q() - .iter(Stores.coins) - .filter((c: Coin) => c.exchangeBaseUrl === exchangeBaseUrl) - .toArray(); - } - - async getPreCoins(exchangeBaseUrl: string): Promise { - return this.q() - .iter(Stores.precoins) - .filter((c: PreCoin) => c.exchangeBaseUrl === exchangeBaseUrl) - .toArray(); - } - - async hashContract(contract: Contract): Promise { - return this.cryptoApi.hashString(canonicalJson(contract)); - } - - /** - * Check if there's an equivalent contract we've already purchased. - */ - async checkRepurchase(contract: Contract): Promise { - if (!contract.repurchase_correlation_id) { - console.log("no repurchase: no correlation id"); - return {isRepurchase: false}; - } - let result: Transaction|undefined = await ( - this.q() - .getIndexed(Stores.transactions.repurchaseIndex, - [ - contract.merchant_pub, - contract.repurchase_correlation_id - ])); - - if (result) { - console.assert(result.contract.repurchase_correlation_id == contract.repurchase_correlation_id); - return { - isRepurchase: true, - existingContractHash: result.contractHash, - existingFulfillmentUrl: result.contract.fulfillment_url, - }; - } else { - return {isRepurchase: false}; - } - } - - - async paymentSucceeded(contractHash: string): Promise { - const doPaymentSucceeded = async() => { - let t = await this.q().get(Stores.transactions, - contractHash); - if (!t) { - console.error("contract not found"); - return; - } - t.finished = true; - let modifiedCoins: Coin[] = []; - for (let pc of t.payReq.coins) { - let c = await this.q().get(Stores.coins, pc.coin_pub); - if (!c) { - console.error("coin not found"); - return; - } - c.transactionPending = false; - modifiedCoins.push(c); - } - - await this.q() - .putAll(Stores.coins, modifiedCoins) - .put(Stores.transactions, t) - .finish(); - for (let c of t.payReq.coins) { - this.refresh(c.coin_pub); - } - }; - doPaymentSucceeded(); - return; - } -} diff --git a/lib/wallet/wxApi.ts b/lib/wallet/wxApi.ts deleted file mode 100644 index a85b56c28..000000000 --- a/lib/wallet/wxApi.ts +++ /dev/null @@ -1,75 +0,0 @@ -/* - 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, see - */ - -import { - AmountJson, - Coin, - PreCoin, - ReserveCreationInfo, - IExchangeInfo, - ReserveRecord -} from "./types"; - -/** - * Interface to the wallet through WebExtension messaging. - * @author Florian Dold - */ - - -export function getReserveCreationInfo(baseUrl: string, - amount: AmountJson): Promise { - let m = { type: "reserve-creation-info", detail: { baseUrl, amount } }; - return new Promise((resolve, reject) => { - chrome.runtime.sendMessage(m, (resp) => { - if (resp.error) { - console.error("error response", resp); - let e = Error("call to reserve-creation-info failed"); - (e as any).errorResponse = resp; - reject(e); - return; - } - resolve(resp); - }); - }); -} - -export async function callBackend(type: string, detail?: any): Promise { - return new Promise((resolve, reject) => { - chrome.runtime.sendMessage({ type, detail }, (resp) => { - resolve(resp); - }); - }); -} - -export async function getExchanges(): Promise { - return await callBackend("get-exchanges"); -} - -export async function getReserves(exchangeBaseUrl: string): Promise { - return await callBackend("get-reserves", { exchangeBaseUrl }); -} - -export async function getCoins(exchangeBaseUrl: string): Promise { - return await callBackend("get-coins", { exchangeBaseUrl }); -} - -export async function getPreCoins(exchangeBaseUrl: string): Promise { - return await callBackend("get-precoins", { exchangeBaseUrl }); -} - -export async function refresh(coinPub: string): Promise { - return await callBackend("refresh-coin", { coinPub }); -} diff --git a/lib/wallet/wxMessaging.ts b/lib/wallet/wxMessaging.ts deleted file mode 100644 index 07f16f24f..000000000 --- a/lib/wallet/wxMessaging.ts +++ /dev/null @@ -1,439 +0,0 @@ -/* - 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, see - */ - - -import { - Wallet, - Offer, - Badge, - ConfirmReserveRequest, - CreateReserveRequest -} from "./wallet"; -import { deleteDb, exportDb, openTalerDb } from "./db"; -import { BrowserHttpLib } from "./http"; -import { Checkable } from "./checkable"; -import { AmountJson } from "./types"; -import Port = chrome.runtime.Port; -import { Notifier } from "./types"; -import { Contract } from "./types"; -import MessageSender = chrome.runtime.MessageSender; -import { ChromeBadge } from "./chromeBadge"; - -"use strict"; - -/** - * Messaging for the WebExtensions wallet. Should contain - * parts that are specific for WebExtensions, but as little business - * logic as possible. - * - * @author Florian Dold - */ - - -type Handler = (detail: any, sender: MessageSender) => Promise; - -function makeHandlers(db: IDBDatabase, - wallet: Wallet): { [msg: string]: Handler } { - return { - ["balances"]: function (detail, sender) { - return wallet.getBalances(); - }, - ["dump-db"]: function (detail, sender) { - return exportDb(db); - }, - ["get-tab-cookie"]: function (detail, sender) { - if (!sender || !sender.tab || !sender.tab.id) { - return Promise.resolve(); - } - let id: number = sender.tab.id; - let info: any = paymentRequestCookies[id]; - delete paymentRequestCookies[id]; - return Promise.resolve(info); - }, - ["ping"]: function (detail, sender) { - return Promise.resolve(); - }, - ["reset"]: function (detail, sender) { - if (db) { - let tx = db.transaction(Array.from(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 Promise.resolve({}); - }, - ["create-reserve"]: function (detail, sender) { - const d = { - exchange: detail.exchange, - amount: detail.amount, - }; - const req = CreateReserveRequest.checked(d); - return wallet.createReserve(req); - }, - ["confirm-reserve"]: function (detail, sender) { - // TODO: make it a checkable - const d = { - reservePub: detail.reservePub - }; - const req = ConfirmReserveRequest.checked(d); - return wallet.confirmReserve(req); - }, - ["confirm-pay"]: function (detail, sender) { - let offer: Offer; - try { - offer = Offer.checked(detail.offer); - } catch (e) { - if (e instanceof Checkable.SchemaError) { - console.error("schema error:", e.message); - return Promise.resolve({ - error: "invalid contract", - hint: e.message, - detail: detail - }); - } else { - throw e; - } - } - - return wallet.confirmPay(offer); - }, - ["check-pay"]: function (detail, sender) { - let offer: Offer; - try { - offer = Offer.checked(detail.offer); - } catch (e) { - if (e instanceof Checkable.SchemaError) { - console.error("schema error:", e.message); - return Promise.resolve({ - error: "invalid contract", - hint: e.message, - detail: detail - }); - } else { - throw e; - } - } - return wallet.checkPay(offer); - }, - ["execute-payment"]: function (detail: any, sender: MessageSender) { - if (sender.tab && sender.tab.id) { - rateLimitCache[sender.tab.id]++; - if (rateLimitCache[sender.tab.id] > 10) { - console.warn("rate limit for execute payment exceeded"); - let msg = { - error: "rate limit exceeded for execute-payment", - rateLimitExceeded: true, - hint: "Check for redirect loops", - }; - return Promise.resolve(msg); - } - } - return wallet.executePayment(detail.H_contract); - }, - ["exchange-info"]: function (detail) { - if (!detail.baseUrl) { - return Promise.resolve({ error: "bad url" }); - } - return wallet.updateExchangeFromUrl(detail.baseUrl); - }, - ["hash-contract"]: function (detail) { - if (!detail.contract) { - return Promise.resolve({ error: "contract missing" }); - } - return wallet.hashContract(detail.contract).then((hash) => { - return { hash }; - }); - }, - ["put-history-entry"]: function (detail: any) { - if (!detail.historyEntry) { - return Promise.resolve({ error: "historyEntry missing" }); - } - return wallet.putHistory(detail.historyEntry); - }, - ["save-offer"]: function (detail: any) { - let offer = detail.offer; - if (!offer) { - return Promise.resolve({ error: "offer missing" }); - } - console.log("handling safe-offer"); - return wallet.saveOffer(offer); - }, - ["reserve-creation-info"]: function (detail, sender) { - if (!detail.baseUrl || typeof detail.baseUrl !== "string") { - return Promise.resolve({ error: "bad url" }); - } - let amount = AmountJson.checked(detail.amount); - return wallet.getReserveCreationInfo(detail.baseUrl, amount); - }, - ["check-repurchase"]: function (detail, sender) { - let contract = Contract.checked(detail.contract); - return wallet.checkRepurchase(contract); - }, - ["get-history"]: function (detail, sender) { - // TODO: limit history length - return wallet.getHistory(); - }, - ["get-offer"]: function (detail, sender) { - return wallet.getOffer(detail.offerId); - }, - ["get-exchanges"]: function (detail, sender) { - return wallet.getExchanges(); - }, - ["get-reserves"]: function (detail, sender) { - if (typeof detail.exchangeBaseUrl !== "string") { - return Promise.reject(Error("exchangeBaseUrl missing")); - } - return wallet.getReserves(detail.exchangeBaseUrl); - }, - ["get-coins"]: function (detail, sender) { - if (typeof detail.exchangeBaseUrl !== "string") { - return Promise.reject(Error("exchangBaseUrl missing")); - } - return wallet.getCoins(detail.exchangeBaseUrl); - }, - ["get-precoins"]: function (detail, sender) { - if (typeof detail.exchangeBaseUrl !== "string") { - return Promise.reject(Error("exchangBaseUrl missing")); - } - return wallet.getPreCoins(detail.exchangeBaseUrl); - }, - ["refresh-coin"]: function (detail, sender) { - if (typeof detail.coinPub !== "string") { - return Promise.reject(Error("coinPub missing")); - } - return wallet.refresh(detail.coinPub); - }, - ["payment-failed"]: function (detail, sender) { - // For now we just update exchanges (maybe the exchange did something - // wrong and the keys were messed up). - // FIXME: in the future we should look at what actually went wrong. - console.error("payment reported as failed"); - wallet.updateExchanges(); - return Promise.resolve(); - }, - ["payment-succeeded"]: function (detail, sender) { - let contractHash = detail.contractHash; - if (!contractHash) { - return Promise.reject(Error("contractHash missing")); - } - return wallet.paymentSucceeded(contractHash); - }, - }; -} - - -function dispatch(handlers: any, req: any, sender: any, sendResponse: any) { - if (req.type in handlers) { - Promise - .resolve() - .then(() => { - const p = handlers[req.type](req.detail, sender); - - return p.then((r: any) => { - try { - sendResponse(r); - } catch (e) { - // might fail if tab disconnected - } - }) - }) - .catch((e) => { - console.log(`exception during wallet handler for '${req.type}'`); - console.log("request", req); - console.error(e); - try { - sendResponse({ - error: "exception", - hint: e.message, - stack: e.stack.toString() - }); - - } catch (e) { - // might fail if tab disconnected - } - }); - // The sendResponse call is async - return true; - } else { - console.error(`Request type ${JSON.stringify(req)} unknown, req ${req.type}`); - try { - sendResponse({ error: "request unknown" }); - } catch (e) { - // might fail if tab disconnected - } - - // The sendResponse call is sync - return false; - } -} - -class ChromeNotifier implements Notifier { - ports: Port[] = []; - - constructor() { - chrome.runtime.onConnect.addListener((port) => { - console.log("got connect!"); - this.ports.push(port); - port.onDisconnect.addListener(() => { - let i = this.ports.indexOf(port); - if (i >= 0) { - this.ports.splice(i, 1); - } else { - console.error("port already removed"); - } - }); - }); - } - - notify() { - for (let p of this.ports) { - p.postMessage({ notify: true }); - } - } -} - - -/** - * Mapping from tab ID to payment information (if any). - */ -let paymentRequestCookies: { [n: number]: any } = {}; - -function handleHttpPayment(headerList: chrome.webRequest.HttpHeader[], - url: string, tabId: number): any { - const headers: { [s: string]: string } = {}; - for (let kv of headerList) { - if (kv.value) { - headers[kv.name.toLowerCase()] = kv.value; - } - } - - const contractUrl = headers["x-taler-contract-url"]; - if (contractUrl !== undefined) { - paymentRequestCookies[tabId] = { type: "fetch", contractUrl }; - return; - } - - const contractHash = headers["x-taler-contract-hash"]; - - if (contractHash !== undefined) { - const payUrl = headers["x-taler-pay-url"]; - if (payUrl === undefined) { - console.log("malformed 402, X-Taler-Pay-Url missing"); - return; - } - - // Offer URL is optional - const offerUrl = headers["x-taler-offer-url"]; - paymentRequestCookies[tabId] = { - type: "execute", - offerUrl, - payUrl, - contractHash - }; - return; - } - - // looks like it's not a taler request, it might be - // for a different payment system (or the shop is buggy) - console.log("ignoring non-taler 402 response"); -} - -// Useful for debugging ... -export let wallet: Wallet | undefined = undefined; -export let badge: ChromeBadge | undefined = undefined; - -// Rate limit cache for executePayment operations, to break redirect loops -let rateLimitCache: { [n: number]: number } = {}; - -function clearRateLimitCache() { - rateLimitCache = {}; -} - -export function wxMain() { - chrome.browserAction.setBadgeText({ text: "" }); - badge = new ChromeBadge(); - - chrome.tabs.query({}, function (tabs) { - for (let tab of tabs) { - if (!tab.url || !tab.id) { - return; - } - let uri = URI(tab.url); - if (uri.protocol() == "http" || uri.protocol() == "https") { - console.log("injecting into existing tab", tab.id); - chrome.tabs.executeScript(tab.id, { file: "lib/vendor/URI.js" }); - chrome.tabs.executeScript(tab.id, { file: "lib/taler-wallet-lib.js" }); - chrome.tabs.executeScript(tab.id, { file: "content_scripts/notify.js" }); - } - } - }); - - chrome.extension.getBackgroundPage().setInterval(clearRateLimitCache, 5000); - - Promise.resolve() - .then(() => { - return openTalerDb(); - }) - .catch((e) => { - console.error("could not open database"); - console.error(e); - }) - .then((db: IDBDatabase) => { - let http = new BrowserHttpLib(); - let notifier = new ChromeNotifier(); - console.log("setting wallet"); - wallet = new Wallet(db, http, badge!, notifier); - - // Handlers for messages coming directly from the content - // script on the page - let handlers = makeHandlers(db, wallet!); - chrome.runtime.onMessage.addListener((req, sender, sendResponse) => { - try { - return dispatch(handlers, req, sender, sendResponse) - } catch (e) { - console.log(`exception during wallet handler (dispatch)`); - console.log("request", req); - console.error(e); - sendResponse({ - error: "exception", - hint: e.message, - stack: e.stack.toString() - }); - return false; - } - }); - - // Handlers for catching HTTP requests - chrome.webRequest.onHeadersReceived.addListener((details) => { - if (details.statusCode != 402) { - return; - } - console.log(`got 402 from ${details.url}`); - return handleHttpPayment(details.responseHeaders || [], - details.url, - details.tabId); - }, { urls: [""] }, ["responseHeaders", "blocking"]); - }) - .catch((e) => { - console.error("could not initialize wallet messaging"); - console.error(e); - }); -} -- cgit v1.2.3