diff --git a/extension/background/emscriptif.js b/extension/background/emscriptif.js index 914cc4a48..83bf8b5bb 100644 --- a/extension/background/emscriptif.js +++ b/extension/background/emscriptif.js @@ -401,7 +401,6 @@ class HashCode extends PackedArenaObject { case "nonce": qual = RandomQuality.NONCE; break; - break; default: throw Error(format("unknown crypto quality: {0}", qual)); } diff --git a/extension/background/emscriptif.ts b/extension/background/emscriptif.ts index b5b040dc5..46937d088 100644 --- a/extension/background/emscriptif.ts +++ b/extension/background/emscriptif.ts @@ -302,7 +302,7 @@ class Amount extends ArenaObject { } - static getZero(currency: string, a?: Arena) { + static getZero(currency: string, a?: Arena): Amount { let am = new Amount(null, a); let r = emsc.amount_get_zero(currency, am.getNative()); if (r != GNUNET_OK) { @@ -590,7 +590,6 @@ class HashCode extends PackedArenaObject { case "nonce": qual = RandomQuality.NONCE; break; - break; default: throw Error(format("unknown crypto quality: {0}", qual)); } diff --git a/extension/background/query.ts b/extension/background/query.ts new file mode 100644 index 000000000..bfe3102f3 --- /dev/null +++ b/extension/background/query.ts @@ -0,0 +1,157 @@ +/* + This file is part of TALER + (C) 2015 GNUnet e.V. + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, If not, see + */ + +/// + +"use strict"; + + +function Query(db) { + return new QueryRoot(db); +} + +class QueryStream { + qr: QueryRoot; + storeName; + constructor(qr, storeName) { + this.qr = qr; + this.storeName = storeName; + } + join(indexName: string, key: any) { + // join on the source relation's key, which may be + // a path or a transformer function + throw Error("Not implemented"); + } + reduce(f, acc): Promise { + let leakedResolve; + let p = new Promise((resolve, reject) => { + leakedResolve = resolve; + }); + let qr = this.qr; + let storeName = this.storeName; + + function doReduce() { + let req = qr.tx.objectStore(storeName).openCursor(); + req.onsuccess = (e) => { + let cursor: IDBCursorWithValue = req.result; + if (cursor) { + acc = f(acc, cursor.value); + cursor.continue(); + } else { + leakedResolve(acc); + } + } + } + + this.qr.work.push(doReduce); + // We need this one level of indirection so that the kickoff + // is run asynchronously. + return Promise.resolve().then(() => this.qr.finish().then(() => p)); + } +} + + +class QueryRoot { + work = []; + db: IDBDatabase; + tx: IDBTransaction; + stores = new Set(); + kickoffPromise; + + constructor(db) { + this.db = db; + } + + iter(storeName): QueryStream { + this.stores.add(storeName); + return new QueryStream(this, storeName); + } + + put(storeName, val): QueryRoot { + this.stores.add(storeName); + function doPut() { + this.tx.objectStore(storeName).put(val); + } + this.work.push(doPut); + return this; + } + + putAll(storeName, iterable): QueryRoot { + this.stores.add(storeName); + function doPutAll() { + for (let obj of iterable) { + this.tx.objectStore(storeName).put(obj); + } + } + this.work.push(doPutAll); + return this; + } + + add(storeName, val): QueryRoot { + this.stores.add(storeName); + function doAdd() { + this.tx.objectStore(storeName).add(val); + } + this.work.push(doAdd); + return this; + } + + get(storeName, key): Promise { + this.stores.add(storeName); + let leakedResolve; + let p = new Promise((resolve, reject) => { + leakedResolve = resolve; + }); + if (!leakedResolve) { + // According to ES6 spec (paragraph 25.4.3.1), this can't happen. + throw Error("assertion failed"); + } + function doGet() { + let req = this.tx.objectStore(storeName).get(key); + req.onsuccess = (r) => { + leakedResolve(r); + }; + } + this.work.push(doGet); + return p; + } + + finish(): Promise { + if (this.kickoffPromise) { + return this.kickoffPromise; + } + this.kickoffPromise = new Promise((resolve, reject) => { + + this.tx = this.db.transaction(Array.from(this.stores), "readwrite"); + this.tx.oncomplete = () => { + resolve(); + }; + for (let w of this.work) { + w(); + } + }); + return this.kickoffPromise; + } + + delete(storeName: string, key): QueryRoot { + this.stores.add(storeName); + function doDelete() { + this.tx.objectStore(storeName).delete(key); + } + this.work.push(doDelete); + return this; + } +} \ No newline at end of file diff --git a/extension/background/wallet.js b/extension/background/wallet.js index fc84fdfb0..268ca32ed 100644 --- a/extension/background/wallet.js +++ b/extension/background/wallet.js @@ -13,8 +13,6 @@ You should have received a copy of the GNU General Public License along with TALER; see the file COPYING. If not, If not, see */ -/// -/// 'use strict'; /** * See http://api.taler.net/wallet.html#general @@ -36,9 +34,7 @@ function signDeposit(db, offer, cds) { cds = copy(cds); for (let cd of cds) { let coinSpend; - console.log("amount remaining:", amountRemaining.toJson()); if (amountRemaining.value == 0 && amountRemaining.fraction == 0) { - console.log("full amount spent"); break; } if (amountRemaining.cmp(new Amount(cd.coin.currentAmount)) < 0) { @@ -64,9 +60,6 @@ function signDeposit(db, offer, cds) { transaction_id: UInt64.fromNumber(offer.contract.transaction_id), }; let d = new DepositRequestPS(args); - console.log("Deposit request #" + ret.length); - console.log("DepositRequestPS: \n", d.toJson()); - console.log("DepositRequestPS sig: \n", d.toPurpose().hexdump()); let coinSig = eddsaSign(d.toPurpose(), EddsaPrivateKey.fromCrock(cd.coin.coinPriv)) .toCrock(); let s = { @@ -163,7 +156,6 @@ function getPossibleMintCoins(db, paymentAmount, depositFeeLimit, allowedMints) continue nextMint; } } - console.log(format("mint {0}: acc {1} is not enough for {2}", key, JSON.stringify(accAmount.toJson()), JSON.stringify(minAmount.toJson()))); } resolve(ret); }; @@ -173,32 +165,25 @@ function getPossibleMintCoins(db, paymentAmount, depositFeeLimit, allowedMints) }); } function executePay(db, offer, payCoinInfo, merchantBaseUrl, chosenMint) { - return new Promise((resolve, reject) => { - let payReq = {}; - payReq["H_wire"] = offer.contract.H_wire; - payReq["H_contract"] = offer.H_contract; - payReq["transaction_id"] = offer.contract.transaction_id; - payReq["refund_deadline"] = offer.contract.refund_deadline; - payReq["mint"] = URI(chosenMint).href(); - payReq["coins"] = payCoinInfo.map((x) => x.sig); - payReq["timestamp"] = offer.contract.timestamp; - let payUrl = URI(offer.pay_url).absoluteTo(merchantBaseUrl); - let t = { - contractHash: offer.H_contract, - contract: offer.contract, - payUrl: payUrl.href(), - payReq: payReq - }; - let tx = db.transaction(["transactions", "coins"], "readwrite"); - tx.objectStore('transactions').put(t); - for (let c of payCoinInfo) { - tx.objectStore("coins").put(c.updatedCoin); - } - tx.oncomplete = (e) => { - updateBadge(db); - resolve(); - }; - }); + let payReq = {}; + payReq["H_wire"] = offer.contract.H_wire; + payReq["H_contract"] = offer.H_contract; + payReq["transaction_id"] = offer.contract.transaction_id; + payReq["refund_deadline"] = offer.contract.refund_deadline; + payReq["mint"] = URI(chosenMint).href(); + payReq["coins"] = payCoinInfo.map((x) => x.sig); + payReq["timestamp"] = offer.contract.timestamp; + let payUrl = URI(offer.pay_url).absoluteTo(merchantBaseUrl); + let t = { + contractHash: offer.H_contract, + contract: offer.contract, + payUrl: payUrl.href(), + payReq: payReq + }; + return Query(db) + .put("transactions", t) + .putAll("coins", payCoinInfo.map((pci) => pci.updatedCoin)) + .finish(); } function confirmPay(db, detail, sendResponse) { let offer = detail.offer; @@ -206,37 +191,36 @@ function confirmPay(db, detail, sendResponse) { .then((mcs) => { if (Object.keys(mcs).length == 0) { sendResponse({ error: "Not enough coins." }); + // FIXME: does not work like expected here ... return; } let mintUrl = Object.keys(mcs)[0]; let ds = signDeposit(db, offer, mcs[mintUrl]); - return executePay(db, offer, ds, detail.merchantPageUrl, mintUrl); - }) - .then(() => { - sendResponse({ - success: true, + return executePay(db, offer, ds, detail.merchantPageUrl, mintUrl) + .then(() => { + sendResponse({ + success: true, + }); }); }); return true; } function doPayment(db, detail, sendResponse) { let H_contract = detail.H_contract; - let req = db.transaction(['transactions']) - .objectStore("transactions") - .get(H_contract); - console.log("executing contract", H_contract); - req.onsuccess = (e) => { - console.log("got db response for existing contract"); - if (!req.result) { + Query(db) + .get("transactions", H_contract) + .then((r) => { + if (!r) { sendResponse({ success: false, error: "contract not found" }); return; } sendResponse({ success: true, - payUrl: req.result.payUrl, - payReq: req.result.payReq + payUrl: r.payUrl, + payReq: r.payReq }); - }; + }); + // async sendResponse return true; } function confirmReserve(db, detail, sendResponse) { @@ -249,7 +233,6 @@ function confirmReserve(db, detail, sendResponse) { form.append(detail.field_mint, detail.mint); // XXX: set bank-specified fields. let myRequest = new XMLHttpRequest(); - console.log("making request to " + detail.post_url); myRequest.open('post', detail.post_url); myRequest.send(form); let mintBaseUrl = canonicalizeBaseUrl(detail.mint); @@ -336,10 +319,7 @@ function withdrawPrepare(db, denom, reserve) { h_denomination_pub: denomPub.encode().hash(), h_coin_envelope: ev.hash() }); - console.log("about to sign"); var sig = eddsaSign(withdrawRequest.toPurpose(), reservePriv); - console.log("signed"); - console.log("crypto done, doing request"); let preCoin = { reservePub: reservePub.toCrock(), blindingKey: blindingFactor.toCrock(), @@ -351,93 +331,63 @@ function withdrawPrepare(db, denom, reserve) { coinEv: ev.toCrock(), coinValue: denom.value }; - console.log("storing precoin", JSON.stringify(preCoin)); - let tx = db.transaction(['precoins'], 'readwrite'); - tx.objectStore('precoins').add(preCoin); - return new Promise((resolve, reject) => { - tx.oncomplete = (e) => { - resolve(preCoin); - }; - }); -} -function dbGet(db, store, key) { - let tx = db.transaction([store]); - let req = tx.objectStore(store).get(key); - return new Promise((resolve, reject) => { - req.onsuccess = (e) => resolve(req.result); - }); + return Query(db).put("precoins", preCoin).finish().then(() => preCoin); } function withdrawExecute(db, pc) { - return dbGet(db, 'reserves', pc.reservePub) - .then((r) => new Promise((resolve, reject) => { - console.log("loading precoin", JSON.stringify(pc)); + return Query(db) + .get("reserves", pc.reservePub) + .then((r) => { let wd = {}; wd.denom_pub = pc.denomPub; wd.reserve_pub = pc.reservePub; wd.reserve_sig = pc.withdrawSig; wd.coin_ev = pc.coinEv; let reqUrl = URI("reserve/withdraw").absoluteTo(r.mint_base_url); - let myRequest = new XMLHttpRequest(); - console.log("making request to " + reqUrl.href()); - myRequest.open('post', reqUrl.href()); - myRequest.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); - myRequest.send(JSON.stringify(wd)); - myRequest.addEventListener('readystatechange', (e) => { - if (myRequest.readyState == XMLHttpRequest.DONE) { - if (myRequest.status != 200) { - console.log("Withdrawal failed, status ", myRequest.status); - reject(); - return; - } - console.log("Withdrawal successful"); - console.log(myRequest.responseText); - let resp = JSON.parse(myRequest.responseText); - let denomSig = rsaUnblind(RsaSignature.fromCrock(resp.ev_sig), RsaBlindingKey.fromCrock(pc.blindingKey), RsaPublicKey.fromCrock(pc.denomPub)); - let coin = { - coinPub: pc.coinPub, - coinPriv: pc.coinPriv, - denomPub: pc.denomPub, - denomSig: denomSig.encode().toCrock(), - currentAmount: pc.coinValue, - mintBaseUrl: pc.mintBaseUrl, - }; - console.log("unblinded coin"); - resolve(coin); - } - else { - console.log("ready state change to", myRequest.status); - } - }); - })); + return httpPost(reqUrl, wd); + }) + .then(resp => { + if (resp.status != 200) { + throw new RequestException({ + hint: "Withdrawal failed", + status: resp.status + }); + } + let r = JSON.parse(resp.responseText); + let denomSig = rsaUnblind(RsaSignature.fromCrock(r.ev_sig), RsaBlindingKey.fromCrock(pc.blindingKey), RsaPublicKey.fromCrock(pc.denomPub)); + let coin = { + coinPub: pc.coinPub, + coinPriv: pc.coinPriv, + denomPub: pc.denomPub, + denomSig: denomSig.encode().toCrock(), + currentAmount: pc.coinValue, + mintBaseUrl: pc.mintBaseUrl, + }; + return coin; + }); } function updateBadge(db) { - let tx = db.transaction(['coins'], 'readwrite'); - let req = tx.objectStore('coins').openCursor(); - let n = 0; - req.onsuccess = (e) => { - let cursor = req.result; - if (cursor) { - let c = cursor.value; - if (c.currentAmount.fraction != 0 || c.currentAmount.value != 0) { - n++; - } - cursor.continue(); + function countNonEmpty(n, c) { + if (c.currentAmount.fraction != 0 || c.currentAmount.value != 0) { + return n + 1; } - else { - chrome.browserAction.setBadgeText({ text: "" + n }); - chrome.browserAction.setBadgeBackgroundColor({ color: "#0F0" }); - } - }; + return n; + } + function doBadge(n) { + chrome.browserAction.setBadgeText({ text: "" + n }); + chrome.browserAction.setBadgeBackgroundColor({ color: "#0F0" }); + } + Query(db) + .iter("coins") + .reduce(countNonEmpty, 0) + .then(doBadge); } function storeCoin(db, coin) { - let tx = db.transaction(['coins', 'precoins'], 'readwrite'); - tx.objectStore('precoins').delete(coin.coinPub); - tx.objectStore('coins').add(coin); - return new Promise((resolve, reject) => { - tx.oncomplete = (e) => { - resolve(); - updateBadge(db); - }; + Query(db) + .delete("precoins", coin.coinPub) + .add("coins", coin) + .finish() + .then(() => { + updateBadge(db); }); } function withdraw(db, denom, reserve) { @@ -482,86 +432,81 @@ function depleteReserve(db, reserve, mint) { next(); } function updateReserve(db, reservePub, mint) { - let reserve; - return new Promise((resolve, reject) => { - let tx = db.transaction(['reserves']); - tx.objectStore('reserves').get(reservePub.toCrock()).onsuccess = (e) => { - let reserve = e.target.result; - let reqUrl = URI("reserve/status").absoluteTo(mint.baseUrl); - reqUrl.query({ 'reserve_pub': reservePub.toCrock() }); - let myRequest = new XMLHttpRequest(); - console.log("making request to " + reqUrl.href()); - myRequest.open('get', reqUrl.href()); - myRequest.send(); - myRequest.addEventListener('readystatechange', (e) => { - if (myRequest.readyState == XMLHttpRequest.DONE) { - if (myRequest.status != 200) { - reject(); - return; - } - let reserveInfo = JSON.parse(myRequest.responseText); - console.log("got response " + JSON.stringify(reserveInfo)); - reserve.current_amount = reserveInfo.balance; - let tx = db.transaction(['reserves'], 'readwrite'); - console.log("putting updated reserve " + JSON.stringify(reserve)); - tx.objectStore('reserves').put(reserve); - tx.oncomplete = (e) => { - resolve(reserve); - }; - } - }); - }; + let reservePubStr = reservePub.toCrock(); + return Query(db) + .get("reserves", reservePubStr) + .then((reserve) => { + let reqUrl = URI("reserve/status").absoluteTo(mint.baseUrl); + reqUrl.query({ 'reserve_pub': reservePubStr }); + return httpGet(reqUrl).then(resp => { + if (resp.status != 200) { + throw Error(); + } + let reserveInfo = JSON.parse(resp.responseText); + if (!reserveInfo) { + throw Error(); + } + reserve.current_amount = reserveInfo.balance; + let q = Query(db); + return q.put("reserves", reserve).finish().then(() => reserve); + }); }); } +function httpReq(method, url, options) { + let urlString; + 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); + } + myRequest.addEventListener("readystatechange", (e) => { + if (myRequest.readyState == XMLHttpRequest.DONE) { + let resp = { + status: myRequest.status, + responseText: myRequest.responseText + }; + resolve(resp); + } + }); + }); +} +function httpGet(url) { + return httpReq("get", url); +} +function httpPost(url, body) { + return httpReq("put", url, { req: JSON.stringify(body) }); +} +class RequestException { + constructor(detail) { + } +} /** * Update or add mint DB entry by fetching the /keys information. * Optionally link the reserve entry to the new or existing * mint entry in then DB. */ function updateMintFromUrl(db, baseUrl) { - console.log("base url is " + baseUrl); let reqUrl = URI("keys").absoluteTo(baseUrl); - let myRequest = new XMLHttpRequest(); - myRequest.open('get', reqUrl.href()); - myRequest.send(); - return new Promise((resolve, reject) => { - myRequest.addEventListener('readystatechange', (e) => { - console.log("state change to " + myRequest.readyState); - if (myRequest.readyState == XMLHttpRequest.DONE) { - if (myRequest.status == 200) { - console.log("got /keys"); - let mintKeysJson = JSON.parse(myRequest.responseText); - if (!mintKeysJson) { - console.log("keys invalid"); - reject(); - } - else { - let mint = { - baseUrl: baseUrl, - keys: mintKeysJson - }; - let tx = db.transaction(['mints', 'denoms'], 'readwrite'); - tx.objectStore('mints').put(mint); - for (let d of mintKeysJson.denoms) { - // TODO: verify and complete - let di = { - denomPub: d.denom_pub, - value: d.value - }; - tx.objectStore('denoms').put(di); - } - tx.oncomplete = (e) => { - resolve(mint); - }; - } - } - else { - console.log("/keys request failed with status " + myRequest.status); - // XXX: also write last error to DB to show in the UI - reject(); - } - } - }); + return httpGet(reqUrl).then((resp) => { + if (resp.status != 200) { + throw Error("/keys request failed"); + } + let mintKeysJson = JSON.parse(resp.responseText); + if (!mintKeysJson) { + throw new RequestException({ url: reqUrl, hint: "keys invalid" }); + } + let mint = { + baseUrl: baseUrl, + keys: mintKeysJson + }; + return Query(db).put("mints", mint).finish().then(() => mint); }); } function dumpDb(db, detail, sendResponse) { @@ -570,7 +515,6 @@ function dumpDb(db, detail, sendResponse) { version: db.version, stores: {} }; - console.log("stores: " + JSON.stringify(db.objectStoreNames)); let tx = db.transaction(db.objectStoreNames); tx.addEventListener('complete', (e) => { sendResponse(dump); @@ -600,52 +544,44 @@ function reset(db, detail, sendResponse) { indexedDB.deleteDatabase(DB_NAME); chrome.browserAction.setBadgeText({ text: "" }); console.log("reset done"); + // Response is synchronous return false; } function balances(db, detail, sendResponse) { - let byCurrency = {}; - let tx = db.transaction(['coins', 'denoms']); - let req = tx.objectStore('coins').openCursor(); - req.onsuccess = (e) => { - let cursor = req.result; - if (cursor) { - let c = cursor.value; - tx.objectStore('denoms').get(c.denomPub).onsuccess = (e2) => { - let d = e2.target.result; - let acc = byCurrency[d.value.currency]; - if (!acc) { - acc = Amount.getZero(c.currentAmount.currency); - } - let am = new Amount(c.currentAmount); - am.add(new Amount(acc)); - byCurrency[d.value.currency] = am.toJson(); - console.log("counting", byCurrency[d.value.currency]); - }; - cursor.continue(); + function collectBalances(c, byCurrency) { + let acc = byCurrency[c.currentAmount.currency]; + if (!acc) { + acc = Amount.getZero(c.currentAmount.currency).toJson(); } - else { - sendResponse(byCurrency); - } - }; + let am = new Amount(c.currentAmount); + am.add(new Amount(acc)); + byCurrency[c.currentAmount.currency] = am.toJson(); + } + Query(db) + .iter("coins") + .reduce(collectBalances, {}) + .then(sendResponse); return true; } -chrome.browserAction.setBadgeText({ text: "" }); -openTalerDb().then((db) => { - console.log("db loaded"); - updateBadge(db); - chrome.runtime.onMessage.addListener(function (req, sender, onresponse) { - let dispatch = { - "confirm-reserve": confirmReserve, - "confirm-pay": confirmPay, - "dump-db": dumpDb, - "balances": balances, - "execute-payment": doPayment, - "reset": reset - }; - if (req.type in dispatch) { - return dispatch[req.type](db, req.detail, onresponse); - } - console.error(format("Request type {1} unknown, req {0}", JSON.stringify(req), req.type)); - return false; +function wxMain() { + chrome.browserAction.setBadgeText({ text: "" }); + openTalerDb().then((db) => { + updateBadge(db); + chrome.runtime.onMessage.addListener(function (req, sender, onresponse) { + let dispatch = { + "confirm-reserve": confirmReserve, + "confirm-pay": confirmPay, + "dump-db": dumpDb, + "balances": balances, + "execute-payment": doPayment, + "reset": reset + }; + if (req.type in dispatch) { + return dispatch[req.type](db, req.detail, onresponse); + } + console.error(format("Request type {1} unknown, req {0}", JSON.stringify(req), req.type)); + return false; + }); }); -}); +} +wxMain(); diff --git a/extension/background/wallet.ts b/extension/background/wallet.ts index 1abd09fd8..c6152253e 100644 --- a/extension/background/wallet.ts +++ b/extension/background/wallet.ts @@ -16,6 +16,8 @@ /// /// +import URIStatic = uri.URIStatic; +import Request = chrome.devtools.network.Request; 'use strict'; @@ -25,22 +27,6 @@ interface AmountJson { currency: string; } - -/** - * See http://api.taler.net/wallet.html#general - */ -function canonicalizeBaseUrl(url) { - let x = new URI(url); - if (!x.protocol()) { - x.protocol("https"); - } - x.path(x.path() + "/").normalizePath(); - x.fragment(); - x.query(); - return x.href() -} - - interface ConfirmPayRequest { merchantPageUrl: string; offer: Offer; @@ -50,6 +36,12 @@ interface MintCoins { [mintUrl: string]: Db.CoinWithDenom[]; } + +interface MintInfo { + master_pub: string; + url: string; +} + interface Offer { contract: Contract; sig: string; @@ -74,7 +66,6 @@ interface Contract { transaction_id: number; } - interface CoinPaySig { coin_sig: string; coin_pub: string; @@ -84,9 +75,39 @@ interface CoinPaySig { } +interface Transaction { + contractHash: string; + contract: any; + payUrl: string; + payReq: any; +} + + +interface Reserve { + mint_base_url: string + reserve_priv: string; + reserve_pub: string; +} + + type PayCoinInfo = Array<{ updatedCoin: Db.Coin, sig: CoinPaySig }>; +/** + * See http://api.taler.net/wallet.html#general + */ +function canonicalizeBaseUrl(url) { + let x = new URI(url); + if (!x.protocol()) { + x.protocol("https"); + } + x.path(x.path() + "/").normalizePath(); + x.fragment(); + x.query(); + return x.href() +} + + function signDeposit(db: IDBDatabase, offer: Offer, cds: Db.CoinWithDenom[]): PayCoinInfo { @@ -97,10 +118,7 @@ function signDeposit(db: IDBDatabase, for (let cd of cds) { let coinSpend; - console.log("amount remaining:", amountRemaining.toJson()); - if (amountRemaining.value == 0 && amountRemaining.fraction == 0) { - console.log("full amount spent"); break; } @@ -131,10 +149,6 @@ function signDeposit(db: IDBDatabase, let d = new DepositRequestPS(args); - console.log("Deposit request #" + ret.length); - console.log("DepositRequestPS: \n", d.toJson()); - console.log("DepositRequestPS sig: \n", d.toPurpose().hexdump()); - let coinSig = eddsaSign(d.toPurpose(), EddsaPrivateKey.fromCrock(cd.coin.coinPriv)) .toCrock(); @@ -151,11 +165,6 @@ function signDeposit(db: IDBDatabase, return ret; } -interface MintInfo { - master_pub: string; - url: string; -} - /** * Get mints and associated coins that are still spendable, @@ -228,28 +237,24 @@ function getPossibleMintCoins(db: IDBDatabase, let accAmount = Amount.getZero(coins[0].c.coin.currentAmount.currency); let usableCoins: Db.CoinWithDenom[] = []; nextCoin: - for (let i = 0; i < coins.length; i++) { - let coinAmount = new Amount(coins[i].c.coin.currentAmount); - let coinFee = coins[i].a; - if (coinAmount.cmp(coinFee) <= 0) { - continue nextCoin; + for (let i = 0; i < coins.length; i++) { + let coinAmount = new Amount(coins[i].c.coin.currentAmount); + let coinFee = coins[i].a; + if (coinAmount.cmp(coinFee) <= 0) { + continue nextCoin; + } + accFee.add(coinFee); + accAmount.add(coinAmount); + if (accFee.cmp(maxFee) >= 0) { + console.log("too much fees"); + continue nextMint; + } + usableCoins.push(coins[i].c); + if (accAmount.cmp(minAmount) >= 0) { + ret[key] = usableCoins; + continue nextMint; + } } - accFee.add(coinFee); - accAmount.add(coinAmount); - if (accFee.cmp(maxFee) >= 0) { - console.log("too much fees"); - continue nextMint; - } - usableCoins.push(coins[i].c); - if (accAmount.cmp(minAmount) >= 0) { - ret[key] = usableCoins; - continue nextMint; - } - } - console.log(format("mint {0}: acc {1} is not enough for {2}", - key, - JSON.stringify(accAmount.toJson()), - JSON.stringify(minAmount.toJson()))); } resolve(ret); }; @@ -261,45 +266,31 @@ function getPossibleMintCoins(db: IDBDatabase, } -interface Transaction { - contractHash: string; - contract: any; - payUrl: string; - payReq: any; -} - - function executePay(db, offer: Offer, payCoinInfo: PayCoinInfo, merchantBaseUrl: string, - chosenMint: string) { - return new Promise((resolve, reject) => { - let payReq = {}; - payReq["H_wire"] = offer.contract.H_wire; - payReq["H_contract"] = offer.H_contract; - payReq["transaction_id"] = offer.contract.transaction_id; - payReq["refund_deadline"] = offer.contract.refund_deadline; - payReq["mint"] = URI(chosenMint).href(); - payReq["coins"] = payCoinInfo.map((x) => x.sig); - payReq["timestamp"] = offer.contract.timestamp; - let payUrl = URI(offer.pay_url).absoluteTo(merchantBaseUrl); - let t: Transaction = { - contractHash: offer.H_contract, - contract: offer.contract, - payUrl: payUrl.href(), - payReq: payReq - }; - let tx = db.transaction(["transactions", "coins"], "readwrite"); - tx.objectStore('transactions').put(t); - for (let c of payCoinInfo) { - tx.objectStore("coins").put(c.updatedCoin); - } - tx.oncomplete = (e) => { - updateBadge(db); - resolve(); - }; - }); + chosenMint: string): Promise { + let payReq = {}; + payReq["H_wire"] = offer.contract.H_wire; + payReq["H_contract"] = offer.H_contract; + payReq["transaction_id"] = offer.contract.transaction_id; + payReq["refund_deadline"] = offer.contract.refund_deadline; + payReq["mint"] = URI(chosenMint).href(); + payReq["coins"] = payCoinInfo.map((x) => x.sig); + payReq["timestamp"] = offer.contract.timestamp; + let payUrl = URI(offer.pay_url).absoluteTo(merchantBaseUrl); + let t: Transaction = { + contractHash: offer.H_contract, + contract: offer.contract, + payUrl: payUrl.href(), + payReq: payReq + }; + + return Query(db) + .put("transactions", t) + .putAll("coins", payCoinInfo.map((pci) => pci.updatedCoin)) + .finish(); } @@ -312,16 +303,17 @@ function confirmPay(db, detail: ConfirmPayRequest, sendResponse) { .then((mcs) => { if (Object.keys(mcs).length == 0) { sendResponse({error: "Not enough coins."}); + // FIXME: does not work like expected here ... return; } let mintUrl = Object.keys(mcs)[0]; let ds = signDeposit(db, offer, mcs[mintUrl]); - return executePay(db, offer, ds, detail.merchantPageUrl, mintUrl); - }) - .then(() => { - sendResponse({ - success: true, - }); + return executePay(db, offer, ds, detail.merchantPageUrl, mintUrl) + .then(() => { + sendResponse({ + success: true, + }); + }); }); return true; } @@ -329,22 +321,20 @@ function confirmPay(db, detail: ConfirmPayRequest, sendResponse) { function doPayment(db, detail, sendResponse) { let H_contract = detail.H_contract; - let req = db.transaction(['transactions']) - .objectStore("transactions") - .get(H_contract); - console.log("executing contract", H_contract); - req.onsuccess = (e) => { - console.log("got db response for existing contract"); - if (!req.result) { - sendResponse({success: false, error: "contract not found"}); - return; - } - sendResponse({ - success: true, - payUrl: req.result.payUrl, - payReq: req.result.payReq - }); - }; + Query(db) + .get("transactions", H_contract) + .then((r) => { + if (!r) { + sendResponse({success: false, error: "contract not found"}); + return; + } + sendResponse({ + success: true, + payUrl: r.payUrl, + payReq: r.payReq + }); + }); + // async sendResponse return true; } @@ -359,7 +349,6 @@ function confirmReserve(db, detail, sendResponse) { form.append(detail.field_mint, detail.mint); // XXX: set bank-specified fields. let myRequest = new XMLHttpRequest(); - console.log("making request to " + detail.post_url); myRequest.open('post', detail.post_url); myRequest.send(form); let mintBaseUrl = canonicalizeBaseUrl(detail.mint); @@ -427,13 +416,6 @@ function rankDenom(denom1: any, denom2: any) { } -interface Reserve { - mint_base_url: string - reserve_priv: string; - reserve_pub: string; -} - - function withdrawPrepare(db: IDBDatabase, denom: Db.Denomination, reserve: Reserve): Promise { @@ -465,11 +447,7 @@ function withdrawPrepare(db: IDBDatabase, h_coin_envelope: ev.hash() }); - console.log("about to sign"); var sig = eddsaSign(withdrawRequest.toPurpose(), reservePriv); - console.log("signed"); - - console.log("crypto done, doing request"); let preCoin: Db.PreCoin = { reservePub: reservePub.toCrock(), @@ -483,104 +461,74 @@ function withdrawPrepare(db: IDBDatabase, coinValue: denom.value }; - console.log("storing precoin", JSON.stringify(preCoin)); - - let tx = db.transaction(['precoins'], 'readwrite'); - tx.objectStore('precoins').add(preCoin); - return new Promise((resolve, reject) => { - tx.oncomplete = (e) => { - resolve(preCoin); - } - }); -} - - -function dbGet(db, store: string, key: any): Promise { - let tx = db.transaction([store]); - let req = tx.objectStore(store).get(key); - return new Promise((resolve, reject) => { - req.onsuccess = (e) => resolve(req.result); - }); + return Query(db).put("precoins", preCoin).finish().then(() => preCoin); } function withdrawExecute(db, pc: Db.PreCoin): Promise { - return dbGet(db, 'reserves', pc.reservePub) - .then((r) => new Promise((resolve, reject) => { - console.log("loading precoin", JSON.stringify(pc)); + return Query(db) + .get("reserves", pc.reservePub) + .then((r) => { let wd: any = {}; wd.denom_pub = pc.denomPub; wd.reserve_pub = pc.reservePub; wd.reserve_sig = pc.withdrawSig; wd.coin_ev = pc.coinEv; let reqUrl = URI("reserve/withdraw").absoluteTo(r.mint_base_url); - let myRequest = new XMLHttpRequest(); - console.log("making request to " + reqUrl.href()); - myRequest.open('post', reqUrl.href()); - myRequest.setRequestHeader("Content-Type", - "application/json;charset=UTF-8"); - myRequest.send(JSON.stringify(wd)); - myRequest.addEventListener('readystatechange', (e) => { - if (myRequest.readyState == XMLHttpRequest.DONE) { - if (myRequest.status != 200) { - console.log("Withdrawal failed, status ", myRequest.status); - reject(); - return; - } - console.log("Withdrawal successful"); - console.log(myRequest.responseText); - let resp = JSON.parse(myRequest.responseText); - let denomSig = rsaUnblind(RsaSignature.fromCrock(resp.ev_sig), - RsaBlindingKey.fromCrock(pc.blindingKey), - RsaPublicKey.fromCrock(pc.denomPub)); - let coin: Db.Coin = { - coinPub: pc.coinPub, - coinPriv: pc.coinPriv, - denomPub: pc.denomPub, - denomSig: denomSig.encode().toCrock(), - currentAmount: pc.coinValue, - mintBaseUrl: pc.mintBaseUrl, - }; - console.log("unblinded coin"); - resolve(coin); - } else { - console.log("ready state change to", myRequest.status); - } - }); - })); + return httpPost(reqUrl, wd); + }) + .then(resp => { + if (resp.status != 200) { + throw new RequestException({ + hint: "Withdrawal failed", + status: resp.status + }); + } + let r = JSON.parse(resp.responseText); + let denomSig = rsaUnblind(RsaSignature.fromCrock(r.ev_sig), + RsaBlindingKey.fromCrock(pc.blindingKey), + RsaPublicKey.fromCrock(pc.denomPub)); + let coin: Db.Coin = { + coinPub: pc.coinPub, + coinPriv: pc.coinPriv, + denomPub: pc.denomPub, + denomSig: denomSig.encode().toCrock(), + currentAmount: pc.coinValue, + mintBaseUrl: pc.mintBaseUrl, + }; + return coin; + }); } function updateBadge(db) { - let tx = db.transaction(['coins'], 'readwrite'); - let req = tx.objectStore('coins').openCursor(); - let n = 0; - req.onsuccess = (e) => { - let cursor = req.result; - if (cursor) { - let c: Db.Coin = cursor.value; - if (c.currentAmount.fraction != 0 || c.currentAmount.value != 0) { - n++; - } - cursor.continue(); - } else { - chrome.browserAction.setBadgeText({text: "" + n}); - chrome.browserAction.setBadgeBackgroundColor({color: "#0F0"}); + function countNonEmpty(n, c) { + if (c.currentAmount.fraction != 0 || c.currentAmount.value != 0) { + return n + 1; } + return n; } + + function doBadge(n) { + chrome.browserAction.setBadgeText({text: "" + n}); + chrome.browserAction.setBadgeBackgroundColor({color: "#0F0"}); + } + + Query(db) + .iter("coins") + .reduce(countNonEmpty, 0) + .then(doBadge); } function storeCoin(db, coin: Db.Coin) { - let tx = db.transaction(['coins', 'precoins'], 'readwrite'); - tx.objectStore('precoins').delete(coin.coinPub); - tx.objectStore('coins').add(coin); - return new Promise((resolve, reject) => { - tx.oncomplete = (e) => { - resolve(); + Query(db) + .delete("precoins", coin.coinPub) + .add("coins", coin) + .finish() + .then(() => { updateBadge(db); - } - }); + }); } @@ -631,88 +579,102 @@ function depleteReserve(db, reserve, mint) { } -function updateReserve(db, reservePub: EddsaPublicKey, mint) { - let reserve; - return new Promise((resolve, reject) => { - let tx = db.transaction(['reserves']); - tx.objectStore('reserves').get(reservePub.toCrock()).onsuccess = (e) => { - let reserve = e.target.result; +function updateReserve(db: IDBDatabase, + reservePub: EddsaPublicKey, + mint): Promise { + let reservePubStr = reservePub.toCrock(); + return Query(db) + .get("reserves", reservePubStr) + .then((reserve) => { let reqUrl = URI("reserve/status").absoluteTo(mint.baseUrl); - reqUrl.query({'reserve_pub': reservePub.toCrock()}); - let myRequest = new XMLHttpRequest(); - console.log("making request to " + reqUrl.href()); - myRequest.open('get', reqUrl.href()); - myRequest.send(); - myRequest.addEventListener('readystatechange', (e) => { - if (myRequest.readyState == XMLHttpRequest.DONE) { - if (myRequest.status != 200) { - reject(); - return; - } - let reserveInfo = JSON.parse(myRequest.responseText); - console.log("got response " + JSON.stringify(reserveInfo)); - reserve.current_amount = reserveInfo.balance; - let tx = db.transaction(['reserves'], 'readwrite'); - console.log("putting updated reserve " + JSON.stringify(reserve)); - tx.objectStore('reserves').put(reserve); - tx.oncomplete = (e) => { - resolve(reserve); - }; + reqUrl.query({'reserve_pub': reservePubStr}); + return httpGet(reqUrl).then(resp => { + if (resp.status != 200) { + throw Error(); } + let reserveInfo = JSON.parse(resp.responseText); + if (!reserveInfo) { + throw Error(); + } + reserve.current_amount = reserveInfo.balance; + let q = Query(db); + return q.put("reserves", reserve).finish().then(() => reserve); }); - }; - }); - + }); } +interface HttpResponse { + status: number; + responseText: string; +} + + +function httpReq(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); + } + myRequest.addEventListener("readystatechange", (e) => { + if (myRequest.readyState == XMLHttpRequest.DONE) { + let resp = { + status: myRequest.status, + responseText: myRequest.responseText + }; + resolve(resp); + } + }); + }); +} + + +function httpGet(url: string|uri.URI) { + return httpReq("get", url); +} + + +function httpPost(url: string|uri.URI, body) { + return httpReq("put", url, {req: JSON.stringify(body)}); +} + + +class RequestException { + constructor(detail) { + + } +} + /** * Update or add mint DB entry by fetching the /keys information. * Optionally link the reserve entry to the new or existing * mint entry in then DB. */ function updateMintFromUrl(db, baseUrl) { - console.log("base url is " + baseUrl); let reqUrl = URI("keys").absoluteTo(baseUrl); - let myRequest = new XMLHttpRequest(); - myRequest.open('get', reqUrl.href()); - myRequest.send(); - return new Promise((resolve, reject) => { - myRequest.addEventListener('readystatechange', (e) => { - console.log("state change to " + myRequest.readyState); - if (myRequest.readyState == XMLHttpRequest.DONE) { - if (myRequest.status == 200) { - console.log("got /keys"); - let mintKeysJson = JSON.parse(myRequest.responseText); - if (!mintKeysJson) { - console.log("keys invalid"); - reject(); - } else { - let mint: Db.Mint = { - baseUrl: baseUrl, - keys: mintKeysJson - }; - let tx = db.transaction(['mints', 'denoms'], 'readwrite'); - tx.objectStore('mints').put(mint); - for (let d of mintKeysJson.denoms) { - // TODO: verify and complete - let di = { - denomPub: d.denom_pub, - value: d.value - }; - tx.objectStore('denoms').put(di); - } - tx.oncomplete = (e) => { - resolve(mint); - }; - } - } else { - console.log("/keys request failed with status " + myRequest.status); - // XXX: also write last error to DB to show in the UI - reject(); - } - } - }); + return httpGet(reqUrl).then((resp) => { + if (resp.status != 200) { + throw Error("/keys request failed"); + } + let mintKeysJson = JSON.parse(resp.responseText); + if (!mintKeysJson) { + throw new RequestException({url: reqUrl, hint: "keys invalid"}); + } + let mint: Db.Mint = { + baseUrl: baseUrl, + keys: mintKeysJson + }; + return Query(db).put("mints", mint).finish().then(() => mint); }); } @@ -723,7 +685,6 @@ function dumpDb(db, detail, sendResponse) { version: db.version, stores: {} }; - console.log("stores: " + JSON.stringify(db.objectStoreNames)); let tx = db.transaction(db.objectStoreNames); tx.addEventListener('complete', (e) => { sendResponse(dump); @@ -755,58 +716,55 @@ function reset(db, detail, sendResponse) { indexedDB.deleteDatabase(DB_NAME); chrome.browserAction.setBadgeText({text: ""}); console.log("reset done"); + // Response is synchronous return false; } -function balances(db, detail, sendResponse) { - let byCurrency = {}; - let tx = db.transaction(['coins', 'denoms']); - let req = tx.objectStore('coins').openCursor(); - req.onsuccess = (e) => { - let cursor = req.result; - if (cursor) { - let c: Db.Coin = cursor.value; - tx.objectStore('denoms').get(c.denomPub).onsuccess = (e2) => { - let d = e2.target.result; - let acc = byCurrency[d.value.currency]; - if (!acc) { - acc = Amount.getZero(c.currentAmount.currency); - } - let am = new Amount(c.currentAmount); - am.add(new Amount(acc)); - byCurrency[d.value.currency] = am.toJson(); - console.log("counting", byCurrency[d.value.currency]); - }; - cursor.continue(); - } else { - sendResponse(byCurrency); +function balances(db, detail, sendResponse): boolean { + function collectBalances(c: Db.Coin, byCurrency) { + let acc: AmountJson = byCurrency[c.currentAmount.currency]; + if (!acc) { + acc = Amount.getZero(c.currentAmount.currency).toJson(); } - }; + let am = new Amount(c.currentAmount); + am.add(new Amount(acc)); + byCurrency[c.currentAmount.currency] = am.toJson(); + } + + Query(db) + .iter("coins") + .reduce(collectBalances, {}) + .then(sendResponse); return true; } -chrome.browserAction.setBadgeText({text: ""}); -openTalerDb().then((db) => { - console.log("db loaded"); - updateBadge(db); - chrome.runtime.onMessage.addListener( - function(req, sender, onresponse) { - let dispatch = { - "confirm-reserve": confirmReserve, - "confirm-pay": confirmPay, - "dump-db": dumpDb, - "balances": balances, - "execute-payment": doPayment, - "reset": reset - }; - if (req.type in dispatch) { - return dispatch[req.type](db, req.detail, onresponse); - } - console.error(format("Request type {1} unknown, req {0}", - JSON.stringify(req), - req.type)); - return false; - }); -}); +function wxMain() { + chrome.browserAction.setBadgeText({text: ""}); + + openTalerDb().then((db) => { + updateBadge(db); + chrome.runtime.onMessage.addListener( + function(req, sender, onresponse) { + let dispatch = { + "confirm-reserve": confirmReserve, + "confirm-pay": confirmPay, + "dump-db": dumpDb, + "balances": balances, + "execute-payment": doPayment, + "reset": reset + }; + if (req.type in dispatch) { + return dispatch[req.type](db, req.detail, onresponse); + } + console.error(format("Request type {1} unknown, req {0}", + JSON.stringify(req), + req.type)); + return false; + }); + }); +} + + +wxMain(); \ No newline at end of file diff --git a/extension/manifest.json b/extension/manifest.json index 9a3b28b37..90745e577 100644 --- a/extension/manifest.json +++ b/extension/manifest.json @@ -46,6 +46,7 @@ "background/libwrapper.js", "background/emscriptif.js", "background/db.js", + "background/query.js", "background/wallet.js" ] }, diff --git a/extension/tsconfig.json b/extension/tsconfig.json index 669c264e6..43b814f9d 100644 --- a/extension/tsconfig.json +++ b/extension/tsconfig.json @@ -7,6 +7,7 @@ "background/wallet.ts", "background/emscriptif.ts", "background/db.ts", + "background/query.ts", "lib/util.ts", "lib/polyfill-react.ts", "content_scripts/notify.ts",