diff options
Diffstat (limited to 'extension')
-rw-r--r-- | extension/background/checkable.ts | 31 | ||||
-rw-r--r-- | extension/background/db.js | 1 | ||||
-rw-r--r-- | extension/background/db.ts | 6 | ||||
-rw-r--r-- | extension/background/messaging.ts | 101 | ||||
-rw-r--r-- | extension/background/wallet.js | 803 | ||||
-rw-r--r-- | extension/background/wallet.ts | 947 | ||||
-rw-r--r-- | extension/content_scripts/notify.js | 2 | ||||
-rw-r--r-- | extension/content_scripts/notify.ts | 2 | ||||
-rw-r--r-- | extension/manifest.json | 2 | ||||
-rw-r--r-- | extension/popup/balance-overview.html | 2 | ||||
-rw-r--r-- | extension/popup/balance-overview.js | 17 | ||||
-rw-r--r-- | extension/popup/balance-overview.tsx | 26 | ||||
-rw-r--r-- | extension/popup/history.html | 32 | ||||
-rw-r--r-- | extension/popup/history.tsx | 22 | ||||
-rw-r--r-- | extension/popup/reserve-create-sepa.html | 2 | ||||
-rw-r--r-- | extension/popup/reserve-create.html | 2 | ||||
-rw-r--r-- | extension/popup/reserves.html | 2 | ||||
-rw-r--r-- | extension/popup/transactions.html | 62 | ||||
-rw-r--r-- | extension/popup/transactions.js | 39 | ||||
-rw-r--r-- | extension/tsconfig.json | 1 |
20 files changed, 1081 insertions, 1021 deletions
diff --git a/extension/background/checkable.ts b/extension/background/checkable.ts index f7e99df92..7cf50318a 100644 --- a/extension/background/checkable.ts +++ b/extension/background/checkable.ts @@ -41,6 +41,13 @@ namespace Checkable { return target; } + function checkAnyObject(target, prop): any { + if (typeof target !== "object") { + throw Error("object expected for " + prop.propertyKey); + } + return target; + } + function checkValue(target, prop): any { let type = prop.type; if (!type) { @@ -84,11 +91,7 @@ namespace Checkable { export function Value(type) { function deco(target: Object, propertyKey: string | symbol): void { - let chk = target[chkSym]; - if (!chk) { - chk = {props: []}; - target[chkSym] = chk; - } + let chk = mkChk(target); chk.props.push({ propertyKey: propertyKey, checker: checkValue, @@ -108,20 +111,26 @@ namespace Checkable { } export function Number(target: Object, propertyKey: string | symbol): void { - let chk = target[chkSym]; - if (!chk) { - chk = {props: []}; - target[chkSym] = chk; - } + let chk = mkChk(target); chk.props.push({propertyKey: propertyKey, checker: checkNumber}); } + export function AnyObject(target: Object, propertyKey: string | symbol): void { + let chk = mkChk(target); + chk.props.push({propertyKey: propertyKey, checker: checkAnyObject}); + } + export function String(target: Object, propertyKey: string | symbol): void { + let chk = mkChk(target); + chk.props.push({propertyKey: propertyKey, checker: checkString}); + } + + function mkChk(target) { let chk = target[chkSym]; if (!chk) { chk = {props: []}; target[chkSym] = chk; } - chk.props.push({propertyKey: propertyKey, checker: checkString}); + return chk; } } diff --git a/extension/background/db.js b/extension/background/db.js index 4b81555da..8abf56b48 100644 --- a/extension/background/db.js +++ b/extension/background/db.js @@ -42,6 +42,7 @@ function openTalerDb() { coins.createIndex("mintBaseUrl", "mintBaseUrl"); db.createObjectStore("transactions", { keyPath: "contractHash" }); db.createObjectStore("precoins", { keyPath: "coinPub", autoIncrement: true }); + db.createObjectStore("history", { keyPath: "id", autoIncrement: true }); break; } }; diff --git a/extension/background/db.ts b/extension/background/db.ts index d3c6e9182..2807eb185 100644 --- a/extension/background/db.ts +++ b/extension/background/db.ts @@ -67,11 +67,8 @@ namespace Db { currentAmount: AmountJson_interface; mintBaseUrl: string; } - - } - const DB_NAME = "taler"; const DB_VERSION = 1; @@ -102,6 +99,7 @@ function openTalerDb(): Promise<IDBDatabase> { db.createObjectStore("transactions", {keyPath: "contractHash"}); db.createObjectStore("precoins", {keyPath: "coinPub", autoIncrement: true}); + db.createObjectStore("history", {keyPath: "id", autoIncrement: true}); break; } }; @@ -137,4 +135,4 @@ function exportDb(db): Promise<any> { }); } }); -}
\ No newline at end of file +} diff --git a/extension/background/messaging.ts b/extension/background/messaging.ts index 6d444f95d..8cde06262 100644 --- a/extension/background/messaging.ts +++ b/extension/background/messaging.ts @@ -25,45 +25,82 @@ "use strict"; -// FIXME: none of these handlers should pass on the sendResponse. - -let handlers = { - ["balances"]: function(db, detail, sendResponse) { - getBalances(db).then(sendResponse); - return true; - }, - ["dump-db"]: function(db, detail, sendResponse) { - exportDb(db).then(sendResponse); - return true; - }, - ["reset"]: function(db, detail, sendResponse) { - let tx = db.transaction(db.objectStoreNames, 'readwrite'); - for (let i = 0; i < db.objectStoreNames.length; i++) { - tx.objectStore(db.objectStoreNames[i]).clear(); +function makeHandlers(wallet) { + return { + ["balances"]: function(db, detail, sendResponse) { + wallet.getBalances().then(sendResponse); + return true; + }, + ["dump-db"]: function(db, detail, sendResponse) { + exportDb(db).then(sendResponse); + return true; + }, + ["reset"]: function(db, detail, sendResponse) { + let tx = db.transaction(db.objectStoreNames, 'readwrite'); + for (let i = 0; i < db.objectStoreNames.length; i++) { + tx.objectStore(db.objectStoreNames[i]).clear(); + } + indexedDB.deleteDatabase(DB_NAME); + chrome.browserAction.setBadgeText({text: ""}); + console.log("reset done"); + // Response is synchronous + return false; + }, + ["confirm-reserve"]: function(db, detail, sendResponse) { + // TODO: make it a checkable + let req: ConfirmReserveRequest = { + field_amount: detail.field_amount, + field_mint: detail.field_mint, + field_reserve_pub: detail.field_reserve_pub, + post_url: detail.post_url, + mint: detail.mint, + amount_str: detail.amount_str + }; + wallet.confirmReserve(req) + .then((resp) => { + if (resp.success) { + resp.backlink = chrome.extension.getURL("pages/reserve-success.html"); + } + sendResponse(resp); + }); + return true; + }, + ["confirm-pay"]: function(db, detail, sendResponse) { + wallet.confirmPay(detail.offer, detail.merchantPageUrl) + .then(() => { + sendResponse({success: true}) + }) + .catch((e) => { + sendResponse({error: e.message}); + }); + return true; + }, + ["execute-payment"]: function(db, detail, sendResponse) { + wallet.doPayment(detail.H_contract) + .then((r) => { + sendResponse({ + success: true, + payUrl: r.payUrl, + payReq: r.payReq + }); + }) + .catch((e) => { + sendResponse({success: false, error: e.message}); + }); + // async sendResponse + return true; } - indexedDB.deleteDatabase(DB_NAME); - chrome.browserAction.setBadgeText({text: ""}); - console.log("reset done"); - // Response is synchronous - return false; - }, - ["confirm-reserve"]: function(db, detail, sendResponse) { - return confirmReserveHandler(db, detail, sendResponse); - }, - ["confirm-pay"]: function(db, detail, sendResponse) { - return confirmPayHandler(db, detail, sendResponse); - }, - ["execute-payment"]: function(db, detail, sendResponse) { - return doPaymentHandler(db, detail, sendResponse); - } -}; + }; +} function wxMain() { chrome.browserAction.setBadgeText({text: ""}); openTalerDb().then((db) => { - updateBadge(db); + let wallet = new Wallet(db, undefined, undefined); + let handlers = makeHandlers(wallet); + wallet.updateBadge(); chrome.runtime.onMessage.addListener( function(req, sender, onresponse) { if (req.type in handlers) { diff --git a/extension/background/wallet.js b/extension/background/wallet.js index 0962961d6..f0337818c 100644 --- a/extension/background/wallet.js +++ b/extension/background/wallet.js @@ -75,438 +75,429 @@ function canonicalizeBaseUrl(url) { x.query(); return x.href(); } -function signDeposit(db, offer, cds) { - let ret = []; - let amountSpent = Amount.getZero(cds[0].coin.currentAmount.currency); - let amountRemaining = new Amount(offer.contract.amount); - cds = copy(cds); - for (let cd of cds) { - let coinSpend; - if (amountRemaining.value == 0 && amountRemaining.fraction == 0) { - break; - } - if (amountRemaining.cmp(new Amount(cd.coin.currentAmount)) < 0) { - coinSpend = new Amount(amountRemaining.toJson()); - } - else { - coinSpend = new Amount(cd.coin.currentAmount); - } - amountSpent.add(coinSpend); - amountRemaining.sub(coinSpend); - let newAmount = new Amount(cd.coin.currentAmount); - newAmount.sub(coinSpend); - cd.coin.currentAmount = newAmount.toJson(); - let args = { - h_contract: HashCode.fromCrock(offer.H_contract), - h_wire: HashCode.fromCrock(offer.contract.H_wire), - amount_with_fee: coinSpend.toNbo(), - coin_pub: EddsaPublicKey.fromCrock(cd.coin.coinPub), - deposit_fee: new Amount(cd.denom.fee_deposit).toNbo(), - merchant: EddsaPublicKey.fromCrock(offer.contract.merchant_pub), - refund_deadline: AbsoluteTimeNbo.fromTalerString(offer.contract.refund_deadline), - timestamp: AbsoluteTimeNbo.fromTalerString(offer.contract.timestamp), - transaction_id: UInt64.fromNumber(offer.contract.transaction_id), - }; - let d = new DepositRequestPS(args); - let coinSig = eddsaSign(d.toPurpose(), EddsaPrivateKey.fromCrock(cd.coin.coinPriv)) - .toCrock(); - let s = { - 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; +function copy(o) { + return JSON.parse(JSON.stringify(o)); } -/** - * Get mints and associated coins that are still spendable, - * but only if the sum the coins' remaining value exceeds the payment amount. - * @param db - * @param paymentAmount - * @param depositFeeLimit - * @param allowedMints - */ -function getPossibleMintCoins(db, paymentAmount, depositFeeLimit, allowedMints) { - let m = {}; - function storeMintCoin(mc) { - let mint = mc[0]; - let coin = mc[1]; - let cd = { - coin: coin, - denom: mint.keys.denoms.find((e) => e.denom_pub === coin.denomPub) - }; - if (!cd.denom) { - throw Error("denom not found (database inconsistent)"); - } - let x = m[mint.baseUrl]; - if (!x) { - m[mint.baseUrl] = [cd]; - } - else { - x.push(cd); - } +function rankDenom(denom1, denom2) { + // Slow ... we should find a better way than to convert it evert time. + let v1 = new Amount(denom1.value); + let v2 = new Amount(denom2.value); + return (-1) * v1.cmp(v2); +} +class Wallet { + constructor(db, http, badge) { + this.db = db; + this.http = http; + this.badge = badge; } - let ps = allowedMints.map((info) => { - return Query(db) - .iterIndex("mints", "pubKey", info.master_pub) - .indexJoin("coins", "mintBaseUrl", (mint) => mint.baseUrl) - .reduce(storeMintCoin); - }); - return Promise.all(ps).then(() => { - let ret = {}; - nextMint: for (let key in m) { - let coins = m[key].map((x) => ({ - a: new Amount(x.denom.fee_deposit), - c: x - })); - // Sort by ascending deposit fee - coins.sort((o1, o2) => o1.a.cmp(o2.a)); - let maxFee = new Amount(depositFeeLimit); - let minAmount = new Amount(paymentAmount); - let accFee = new Amount(coins[0].c.denom.fee_deposit); - let accAmount = Amount.getZero(coins[0].c.coin.currentAmount.currency); - let usableCoins = []; - 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; - } + static signDeposit(offer, cds) { + let ret = []; + let amountSpent = Amount.getZero(cds[0].coin.currentAmount.currency); + let amountRemaining = new Amount(offer.contract.amount); + cds = copy(cds); + for (let cd of cds) { + let coinSpend; + if (amountRemaining.value == 0 && amountRemaining.fraction == 0) { + break; } + if (amountRemaining.cmp(new Amount(cd.coin.currentAmount)) < 0) { + coinSpend = new Amount(amountRemaining.toJson()); + } + else { + coinSpend = new Amount(cd.coin.currentAmount); + } + amountSpent.add(coinSpend); + amountRemaining.sub(coinSpend); + let newAmount = new Amount(cd.coin.currentAmount); + newAmount.sub(coinSpend); + cd.coin.currentAmount = newAmount.toJson(); + let args = { + h_contract: HashCode.fromCrock(offer.H_contract), + h_wire: HashCode.fromCrock(offer.contract.H_wire), + amount_with_fee: coinSpend.toNbo(), + coin_pub: EddsaPublicKey.fromCrock(cd.coin.coinPub), + deposit_fee: new Amount(cd.denom.fee_deposit).toNbo(), + merchant: EddsaPublicKey.fromCrock(offer.contract.merchant_pub), + refund_deadline: AbsoluteTimeNbo.fromTalerString(offer.contract.refund_deadline), + timestamp: AbsoluteTimeNbo.fromTalerString(offer.contract.timestamp), + transaction_id: UInt64.fromNumber(offer.contract.transaction_id), + }; + let d = new DepositRequestPS(args); + let coinSig = eddsaSign(d.toPurpose(), EddsaPrivateKey.fromCrock(cd.coin.coinPriv)) + .toCrock(); + let s = { + 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; - }); -} -function executePay(db, offer, payCoinInfo, merchantBaseUrl, chosenMint) { - 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 confirmPayHandler(db, detail, sendResponse) { - let offer = detail.offer; - getPossibleMintCoins(db, offer.contract.amount, offer.contract.max_fee, offer.contract.mints) - .then((mcs) => { - if (Object.keys(mcs).length == 0) { - sendResponse({ error: "Not enough coins." }); - return; + } + /** + * Get mints and associated coins that are still spendable, + * but only if the sum the coins' remaining value exceeds the payment amount. + * @param paymentAmount + * @param depositFeeLimit + * @param allowedMints + */ + getPossibleMintCoins(paymentAmount, depositFeeLimit, allowedMints) { + let m = {}; + function storeMintCoin(mc) { + let mint = mc[0]; + let coin = mc[1]; + let cd = { + coin: coin, + denom: mint.keys.denoms.find((e) => e.denom_pub === coin.denomPub) + }; + if (!cd.denom) { + throw Error("denom not found (database inconsistent)"); + } + let x = m[mint.baseUrl]; + if (!x) { + m[mint.baseUrl] = [cd]; + } + else { + x.push(cd); + } } - let mintUrl = Object.keys(mcs)[0]; - let ds = signDeposit(db, offer, mcs[mintUrl]); - return executePay(db, offer, ds, detail.merchantPageUrl, mintUrl) - .then(() => { - sendResponse({ - success: true, - }); + let ps = allowedMints.map((info) => { + return Query(this.db) + .iterIndex("mints", "pubKey", info.master_pub) + .indexJoin("coins", "mintBaseUrl", (mint) => mint.baseUrl) + .reduce(storeMintCoin); }); - }); - return true; -} -function doPaymentHandler(db, detail, sendResponse) { - let H_contract = detail.H_contract; - 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 + return Promise.all(ps).then(() => { + let ret = {}; + nextMint: for (let key in m) { + let coins = m[key].map((x) => ({ + a: new Amount(x.denom.fee_deposit), + c: x + })); + // Sort by ascending deposit fee + coins.sort((o1, o2) => o1.a.cmp(o2.a)); + let maxFee = new Amount(depositFeeLimit); + let minAmount = new Amount(paymentAmount); + let accFee = new Amount(coins[0].c.denom.fee_deposit); + let accAmount = Amount.getZero(coins[0].c.coin.currentAmount.currency); + let usableCoins = []; + nextCoin: for (let i = 0; i < coins.length; i++) { + let coinAmount = new Amount(coins[i].c.coin.currentAmount); + let coinFee = coins[i].a; + if (coinAmount.cmp(coinFee) <= 0) { + continue nextCoin; + } + accFee.add(coinFee); + accAmount.add(coinAmount); + if (accFee.cmp(maxFee) >= 0) { + console.log("too much fees"); + continue nextMint; + } + usableCoins.push(coins[i].c); + if (accAmount.cmp(minAmount) >= 0) { + ret[key] = usableCoins; + continue nextMint; + } + } + } + return ret; }); - }); - // async sendResponse - return true; -} -function confirmReserveHandler(db, detail, sendResponse) { - let reservePriv = EddsaPrivateKey.create(); - let reservePub = reservePriv.getPublicKey(); - let form = new FormData(); - let now = (new Date()).toString(); - form.append(detail.field_amount, detail.amount_str); - form.append(detail.field_reserve_pub, reservePub.toCrock()); - form.append(detail.field_mint, detail.mint); - // XXX: set bank-specified fields. - let mintBaseUrl = canonicalizeBaseUrl(detail.mint); - httpPostForm(detail.post_url, form) - .then((hresp) => { - // TODO: extract as interface - let resp = { - status: hresp.status, - text: hresp.responseText, - success: undefined, - backlink: undefined - }; - let reserveRecord = { - reserve_pub: reservePub.toCrock(), - reserve_priv: reservePriv.toCrock(), - mint_base_url: mintBaseUrl, - created: now, - last_query: null, - current_amount: null, - // XXX: set to actual amount - initial_amount: null + } + executePay(offer, payCoinInfo, merchantBaseUrl, chosenMint) { + 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 }; - if (hresp.status != 200) { - resp.success = false; - return resp; - } - resp.success = true; - // We can't show the page directly, so - // we show some generic page from the wallet. - // TODO: this should not be webextensions-specific - resp.backlink = chrome.extension.getURL("pages/reserve-success.html"); - return Query(db) - .put("reserves", reserveRecord) - .finish() - .then(() => { - // Do this in the background - updateMintFromUrl(db, reserveRecord.mint_base_url) - .then((mint) => updateReserve(db, reservePub, mint) - .then((reserve) => depleteReserve(db, reserve, mint))); - return resp; + return Query(this.db) + .put("transactions", t) + .putAll("coins", payCoinInfo.map((pci) => pci.updatedCoin)) + .finish(); + } + confirmPay(offer, merchantPageUrl) { + return Promise.resolve().then(() => { + return this.getPossibleMintCoins(offer.contract.amount, offer.contract.max_fee, offer.contract.mints); + }).then((mcs) => { + if (Object.keys(mcs).length == 0) { + throw Error("Not enough coins."); + } + let mintUrl = Object.keys(mcs)[0]; + let ds = Wallet.signDeposit(offer, mcs[mintUrl]); + return this.executePay(offer, ds, merchantPageUrl, mintUrl); }); - }) - .then((resp) => { - sendResponse(resp); - }); - // Allow async response - return true; -} -function copy(o) { - return JSON.parse(JSON.stringify(o)); -} -function rankDenom(denom1, denom2) { - // Slow ... we should find a better way than to convert it evert time. - let v1 = new Amount(denom1.value); - let v2 = new Amount(denom2.value); - return (-1) * v1.cmp(v2); -} -function withdrawPrepare(db, denom, reserve) { - let reservePriv = new EddsaPrivateKey(); - reservePriv.loadCrock(reserve.reserve_priv); - let reservePub = new EddsaPublicKey(); - reservePub.loadCrock(reserve.reserve_pub); - let denomPub = RsaPublicKey.fromCrock(denom.denom_pub); - let coinPriv = EddsaPrivateKey.create(); - let coinPub = coinPriv.getPublicKey(); - let blindingFactor = RsaBlindingKey.create(1024); - let pubHash = coinPub.hash(); - let ev = rsaBlind(pubHash, blindingFactor, denomPub); - if (!denom.fee_withdraw) { - throw Error("Field fee_withdraw missing"); } - let amountWithFee = new Amount(denom.value); - amountWithFee.add(new Amount(denom.fee_withdraw)); - let withdrawFee = new Amount(denom.fee_withdraw); - // Signature - let withdrawRequest = new WithdrawRequestPS({ - reserve_pub: reservePub, - amount_with_fee: amountWithFee.toNbo(), - withdraw_fee: withdrawFee.toNbo(), - h_denomination_pub: denomPub.encode().hash(), - h_coin_envelope: ev.hash() - }); - var sig = eddsaSign(withdrawRequest.toPurpose(), reservePriv); - let preCoin = { - reservePub: reservePub.toCrock(), - blindingKey: blindingFactor.toCrock(), - coinPub: coinPub.toCrock(), - coinPriv: coinPriv.toCrock(), - denomPub: denomPub.encode().toCrock(), - mintBaseUrl: reserve.mint_base_url, - withdrawSig: sig.toCrock(), - coinEv: ev.toCrock(), - coinValue: denom.value - }; - return Query(db).put("precoins", preCoin).finish().then(() => preCoin); -} -function withdrawExecute(db, 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); - return httpPostJson(reqUrl, wd); - }) - .then(resp => { - if (resp.status != 200) { - throw new RequestException({ - hint: "Withdrawal failed", - status: resp.status + doPayment(H_contract) { + return Promise.resolve().then(() => { + return Query(this.db) + .get("transactions", H_contract) + .then((t) => { + if (!t) { + throw Error("contract not found"); + } + let resp = { + payUrl: t.payUrl, + payReq: t.payReq + }; + return resp; }); + }); + } + confirmReserve(req) { + let reservePriv = EddsaPrivateKey.create(); + let reservePub = reservePriv.getPublicKey(); + let form = new FormData(); + let now = (new Date()).toString(); + form.append(req.field_amount, req.amount_str); + form.append(req.field_reserve_pub, reservePub.toCrock()); + form.append(req.field_mint, req.mint); + // TODO: set bank-specified fields. + let mintBaseUrl = canonicalizeBaseUrl(req.mint); + return httpPostForm(req.post_url, form) + .then((hresp) => { + let resp = { + status: hresp.status, + text: hresp.responseText, + success: undefined, + backlink: undefined + }; + let reserveRecord = { + reserve_pub: reservePub.toCrock(), + reserve_priv: reservePriv.toCrock(), + mint_base_url: mintBaseUrl, + created: now, + last_query: null, + current_amount: null, + // XXX: set to actual amount + initial_amount: null + }; + if (hresp.status != 200) { + resp.success = false; + return resp; + } + resp.success = true; + // We can't show the page directly, so + // we show some generic page from the wallet. + resp.backlink = null; + return Query(this.db) + .put("reserves", reserveRecord) + .finish() + .then(() => { + // Do this in the background + this.updateMintFromUrl(reserveRecord.mint_base_url) + .then((mint) => this.updateReserve(reservePub, mint) + .then((reserve) => this.depleteReserve(reserve, mint))); + return resp; + }); + }); + } + withdrawPrepare(denom, reserve) { + let reservePriv = new EddsaPrivateKey(); + reservePriv.loadCrock(reserve.reserve_priv); + let reservePub = new EddsaPublicKey(); + reservePub.loadCrock(reserve.reserve_pub); + let denomPub = RsaPublicKey.fromCrock(denom.denom_pub); + let coinPriv = EddsaPrivateKey.create(); + let coinPub = coinPriv.getPublicKey(); + let blindingFactor = RsaBlindingKey.create(1024); + let pubHash = coinPub.hash(); + let ev = rsaBlind(pubHash, blindingFactor, denomPub); + if (!denom.fee_withdraw) { + throw Error("Field fee_withdraw missing"); } - 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, + let amountWithFee = new Amount(denom.value); + amountWithFee.add(new Amount(denom.fee_withdraw)); + let withdrawFee = new Amount(denom.fee_withdraw); + // Signature + let withdrawRequest = new WithdrawRequestPS({ + reserve_pub: reservePub, + amount_with_fee: amountWithFee.toNbo(), + withdraw_fee: withdrawFee.toNbo(), + h_denomination_pub: denomPub.encode().hash(), + h_coin_envelope: ev.hash() + }); + var sig = eddsaSign(withdrawRequest.toPurpose(), reservePriv); + let preCoin = { + reservePub: reservePub.toCrock(), + blindingKey: blindingFactor.toCrock(), + coinPub: coinPub.toCrock(), + coinPriv: coinPriv.toCrock(), + denomPub: denomPub.encode().toCrock(), + mintBaseUrl: reserve.mint_base_url, + withdrawSig: sig.toCrock(), + coinEv: ev.toCrock(), + coinValue: denom.value }; - return coin; - }); -} -function updateBadge(db) { - function countNonEmpty(c, n) { - if (c.currentAmount.fraction != 0 || c.currentAmount.value != 0) { - return n + 1; - } - return n; + return Query(this.db).put("precoins", preCoin).finish().then(() => preCoin); } - function doBadge(n) { - chrome.browserAction.setBadgeText({ text: "" + n }); - chrome.browserAction.setBadgeBackgroundColor({ color: "#0F0" }); + withdrawExecute(pc) { + return Query(this.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); + return httpPostJson(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; + }); } - Query(db) - .iter("coins") - .reduce(countNonEmpty, 0) - .then(doBadge); -} -function storeCoin(db, coin) { - Query(db) - .delete("precoins", coin.coinPub) - .add("coins", coin) - .finish() - .then(() => { - updateBadge(db); - }); -} -function withdraw(db, denom, reserve) { - return withdrawPrepare(db, denom, reserve) - .then((pc) => withdrawExecute(db, pc)) - .then((c) => storeCoin(db, c)); -} -/** - * Withdraw coins from a reserve until it is empty. - */ -function depleteReserve(db, reserve, mint) { - let denoms = copy(mint.keys.denoms); - let remaining = new Amount(reserve.current_amount); - denoms.sort(rankDenom); - let workList = []; - for (let i = 0; i < 1000; i++) { - let found = false; - for (let d of denoms) { - let cost = new Amount(d.value); - cost.add(new Amount(d.fee_withdraw)); - if (remaining.cmp(cost) < 0) { - continue; + updateBadge() { + function countNonEmpty(c, n) { + if (c.currentAmount.fraction != 0 || c.currentAmount.value != 0) { + return n + 1; } - found = true; - remaining.sub(cost); - workList.push(d); + return n; } - if (!found) { - console.log("did not find coins for remaining ", remaining.toJson()); - break; + function doBadge(n) { + chrome.browserAction.setBadgeText({ text: "" + n }); + chrome.browserAction.setBadgeBackgroundColor({ color: "#0F0" }); } + Query(this.db) + .iter("coins") + .reduce(countNonEmpty, 0) + .then(doBadge); + } + storeCoin(coin) { + Query(this.db) + .delete("precoins", coin.coinPub) + .add("coins", coin) + .finish() + .then(() => { + this.updateBadge(); + }); + } + withdraw(denom, reserve) { + return this.withdrawPrepare(denom, reserve) + .then((pc) => this.withdrawExecute(pc)) + .then((c) => this.storeCoin(c)); } - // Do the request one by one. - function next() { - if (workList.length == 0) { - return; + /** + * Withdraw coins from a reserve until it is empty. + */ + depleteReserve(reserve, mint) { + let denoms = copy(mint.keys.denoms); + let remaining = new Amount(reserve.current_amount); + denoms.sort(rankDenom); + let workList = []; + for (let i = 0; i < 1000; i++) { + let found = false; + for (let d of denoms) { + let cost = new Amount(d.value); + cost.add(new Amount(d.fee_withdraw)); + if (remaining.cmp(cost) < 0) { + continue; + } + found = true; + remaining.sub(cost); + workList.push(d); + } + if (!found) { + console.log("did not find coins for remaining ", remaining.toJson()); + break; + } } - let d = workList.pop(); - withdraw(db, d, reserve) - .then(() => next()); + // Do the request one by one. + let next = () => { + if (workList.length == 0) { + return; + } + let d = workList.pop(); + this.withdraw(d, reserve) + .then(() => next()); + }; + // Asynchronous recursion + next(); } - next(); -} -function updateReserve(db, reservePub, mint) { - 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 => { + updateReserve(reservePub, mint) { + let reservePubStr = reservePub.toCrock(); + return Query(this.db) + .get("reserves", reservePubStr) + .then((reserve) => { + let reqUrl = URI("reserve/status").absoluteTo(mint.baseUrl); + reqUrl.query({ 'reserve_pub': reservePubStr }); + return 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; + return Query(this.db) + .put("reserves", reserve) + .finish() + .then(() => reserve); + }); + }); + } + /** + * Update or add mint DB entry by fetching the /keys information. + * Optionally link the reserve entry to the new or existing + * mint entry in then DB. + */ + updateMintFromUrl(baseUrl) { + let reqUrl = URI("keys").absoluteTo(baseUrl); + return httpGet(reqUrl).then((resp) => { if (resp.status != 200) { - throw Error(); + throw Error("/keys request failed"); } - let reserveInfo = JSON.parse(resp.responseText); - if (!reserveInfo) { - throw Error(); + let mintKeysJson = JSON.parse(resp.responseText); + if (!mintKeysJson) { + throw new RequestException({ url: reqUrl, hint: "keys invalid" }); } - reserve.current_amount = reserveInfo.balance; - return Query(db) - .put("reserves", reserve) - .finish() - .then(() => reserve); + let mint = { + baseUrl: baseUrl, + keys: mintKeysJson + }; + return Query(this.db).put("mints", mint).finish().then(() => mint); }); - }); -} -/** - * 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) { - let reqUrl = URI("keys").absoluteTo(baseUrl); - 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 getBalances(db) { - function collectBalances(c, byCurrency) { - let acc = byCurrency[c.currentAmount.currency]; - if (!acc) { - acc = Amount.getZero(c.currentAmount.currency).toJson(); + } + getBalances() { + function collectBalances(c, byCurrency) { + let acc = byCurrency[c.currentAmount.currency]; + if (!acc) { + acc = Amount.getZero(c.currentAmount.currency).toJson(); + } + let am = new Amount(c.currentAmount); + am.add(new Amount(acc)); + byCurrency[c.currentAmount.currency] = am.toJson(); + return byCurrency; } - let am = new Amount(c.currentAmount); - am.add(new Amount(acc)); - byCurrency[c.currentAmount.currency] = am.toJson(); - return byCurrency; + return Query(this.db) + .iter("coins") + .reduce(collectBalances, {}); } - return Query(db) - .iter("coins") - .reduce(collectBalances, {}); } diff --git a/extension/background/wallet.ts b/extension/background/wallet.ts index d9187f14a..6479b961a 100644 --- a/extension/background/wallet.ts +++ b/extension/background/wallet.ts @@ -131,6 +131,54 @@ interface Reserve { } +interface PaymentResponse { + payUrl: string; + payReq: any; +} + + +interface ConfirmReserveRequest { + /** + * Name of the form field for the amount. + */ + field_amount; + + /** + * Name of the form field for the reserve public key. + */ + field_reserve_pub; + + /** + * Name of the form field for the reserve public key. + */ + field_mint; + + /** + * The actual amount in string form. + * TODO: where is this format specified? + */ + amount_str; + + /** + * Target URL for the reserve creation request. + */ + post_url; + + /** + * Mint URL where the bank should create the reserve. + */ + mint; +} + + +interface ConfirmReserveResponse { + backlink: string; + success: boolean; + status: number; + text: string; +} + + type PayCoinInfo = Array<{ updatedCoin: Db.Coin, sig: CoinPaySig_interface }>; @@ -148,521 +196,512 @@ function canonicalizeBaseUrl(url) { return x.href() } +interface HttpRequestLibrary { -function signDeposit(db: IDBDatabase, - offer: Offer, - cds: Db.CoinWithDenom[]): PayCoinInfo { - let ret = []; - let amountSpent = Amount.getZero(cds[0].coin.currentAmount.currency); - let amountRemaining = new Amount(offer.contract.amount); - cds = copy(cds); - for (let cd of cds) { - let coinSpend; - - if (amountRemaining.value == 0 && amountRemaining.fraction == 0) { - break; - } - - if (amountRemaining.cmp(new Amount(cd.coin.currentAmount)) < 0) { - coinSpend = new Amount(amountRemaining.toJson()); - } else { - coinSpend = new Amount(cd.coin.currentAmount); - } +} - amountSpent.add(coinSpend); - amountRemaining.sub(coinSpend); - - let newAmount = new Amount(cd.coin.currentAmount); - newAmount.sub(coinSpend); - cd.coin.currentAmount = newAmount.toJson(); - - let args: DepositRequestPS_Args = { - h_contract: HashCode.fromCrock(offer.H_contract), - h_wire: HashCode.fromCrock(offer.contract.H_wire), - amount_with_fee: coinSpend.toNbo(), - coin_pub: EddsaPublicKey.fromCrock(cd.coin.coinPub), - deposit_fee: new Amount(cd.denom.fee_deposit).toNbo(), - merchant: EddsaPublicKey.fromCrock(offer.contract.merchant_pub), - refund_deadline: AbsoluteTimeNbo.fromTalerString(offer.contract.refund_deadline), - timestamp: AbsoluteTimeNbo.fromTalerString(offer.contract.timestamp), - transaction_id: UInt64.fromNumber(offer.contract.transaction_id), - }; +interface Badge { - let d = new DepositRequestPS(args); +} - let coinSig = eddsaSign(d.toPurpose(), - EddsaPrivateKey.fromCrock(cd.coin.coinPriv)) - .toCrock(); - let s: CoinPaySig_interface = { - coin_sig: coinSig, - coin_pub: cd.coin.coinPub, - ub_sig: cd.coin.denomSig, - denom_pub: cd.coin.denomPub, - f: coinSpend.toJson(), - }; - ret.push({sig: s, updatedCoin: cd.coin}); - } - return ret; +function copy(o) { + return JSON.parse(JSON.stringify(o)); } -/** - * Get mints and associated coins that are still spendable, - * but only if the sum the coins' remaining value exceeds the payment amount. - * @param db - * @param paymentAmount - * @param depositFeeLimit - * @param allowedMints - */ -function getPossibleMintCoins(db: IDBDatabase, - paymentAmount: AmountJson_interface, - depositFeeLimit: AmountJson_interface, - allowedMints: MintInfo[]): Promise<MintCoins> { +function rankDenom(denom1: any, denom2: any) { + // Slow ... we should find a better way than to convert it evert time. + let v1 = new Amount(denom1.value); + let v2 = new Amount(denom2.value); + return (-1) * v1.cmp(v2); +} - let m: MintCoins = {}; +class Wallet { + private db: IDBDatabase; + private http: HttpRequestLibrary; + private badge: Badge; - function storeMintCoin(mc) { - let mint = mc[0]; - let coin = mc[1]; - let cd = { - coin: coin, - denom: mint.keys.denoms.find((e) => e.denom_pub === coin.denomPub) - }; - if (!cd.denom) { - throw Error("denom not found (database inconsistent)"); - } - let x = m[mint.baseUrl]; - if (!x) { - m[mint.baseUrl] = [cd]; - } else { - x.push(cd); - } + constructor(db: IDBDatabase, http: HttpRequestLibrary, badge: Badge) { + this.db = db; + this.http = http; + this.badge = badge; } - let ps = allowedMints.map((info) => { - return Query(db) - .iterIndex("mints", "pubKey", info.master_pub) - .indexJoin("coins", "mintBaseUrl", (mint) => mint.baseUrl) - .reduce(storeMintCoin); - }); - - return Promise.all(ps).then(() => { - let ret: MintCoins = {}; - - nextMint: - for (let key in m) { - let coins = m[key].map((x) => ({ - a: new Amount(x.denom.fee_deposit), - c: x - })); - // Sort by ascending deposit fee - coins.sort((o1, o2) => o1.a.cmp(o2.a)); - let maxFee = new Amount(depositFeeLimit); - let minAmount = new Amount(paymentAmount); - let accFee = new Amount(coins[0].c.denom.fee_deposit); - let accAmount = Amount.getZero(coins[0].c.coin.currentAmount.currency); - let usableCoins: 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; - } - 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; - } - } + static signDeposit(offer: Offer, + cds: Db.CoinWithDenom[]): PayCoinInfo { + let ret = []; + let amountSpent = Amount.getZero(cds[0].coin.currentAmount.currency); + let amountRemaining = new Amount(offer.contract.amount); + cds = copy(cds); + for (let cd of cds) { + let coinSpend; + + if (amountRemaining.value == 0 && amountRemaining.fraction == 0) { + break; } - return ret; - }); -} - -function executePay(db, - offer: Offer, - payCoinInfo: PayCoinInfo, - merchantBaseUrl: string, - chosenMint: string): Promise<void> { - let payReq = {}; - payReq["H_wire"] = offer.contract.H_wire; - payReq["H_contract"] = offer.H_contract; - payReq["transaction_id"] = offer.contract.transaction_id; - payReq["refund_deadline"] = offer.contract.refund_deadline; - payReq["mint"] = URI(chosenMint).href(); - payReq["coins"] = payCoinInfo.map((x) => x.sig); - payReq["timestamp"] = offer.contract.timestamp; - let payUrl = URI(offer.pay_url).absoluteTo(merchantBaseUrl); - let t: Transaction = { - contractHash: offer.H_contract, - contract: offer.contract, - payUrl: payUrl.href(), - payReq: payReq - }; - - return Query(db) - .put("transactions", t) - .putAll("coins", payCoinInfo.map((pci) => pci.updatedCoin)) - .finish(); -} - - -function confirmPayHandler(db, detail: ConfirmPayRequest, sendResponse) { - let offer: Offer = detail.offer; - getPossibleMintCoins(db, - offer.contract.amount, - offer.contract.max_fee, - offer.contract.mints) - .then((mcs) => { - if (Object.keys(mcs).length == 0) { - sendResponse({error: "Not enough coins."}); - return; + if (amountRemaining.cmp(new Amount(cd.coin.currentAmount)) < 0) { + coinSpend = new Amount(amountRemaining.toJson()); + } else { + coinSpend = new Amount(cd.coin.currentAmount); } - 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 true; -} + amountSpent.add(coinSpend); + amountRemaining.sub(coinSpend); + + let newAmount = new Amount(cd.coin.currentAmount); + newAmount.sub(coinSpend); + cd.coin.currentAmount = newAmount.toJson(); + + let args: DepositRequestPS_Args = { + h_contract: HashCode.fromCrock(offer.H_contract), + h_wire: HashCode.fromCrock(offer.contract.H_wire), + amount_with_fee: coinSpend.toNbo(), + coin_pub: EddsaPublicKey.fromCrock(cd.coin.coinPub), + deposit_fee: new Amount(cd.denom.fee_deposit).toNbo(), + merchant: EddsaPublicKey.fromCrock(offer.contract.merchant_pub), + refund_deadline: AbsoluteTimeNbo.fromTalerString(offer.contract.refund_deadline), + timestamp: AbsoluteTimeNbo.fromTalerString(offer.contract.timestamp), + transaction_id: UInt64.fromNumber(offer.contract.transaction_id), + }; -function doPaymentHandler(db, detail, sendResponse) { - let H_contract = detail.H_contract; - 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; -} + let d = new DepositRequestPS(args); + let coinSig = eddsaSign(d.toPurpose(), + EddsaPrivateKey.fromCrock(cd.coin.coinPriv)) + .toCrock(); -function confirmReserveHandler(db, detail, sendResponse) { - let reservePriv = EddsaPrivateKey.create(); - let reservePub = reservePriv.getPublicKey(); - let form = new FormData(); - let now = (new Date()).toString(); - form.append(detail.field_amount, detail.amount_str); - form.append(detail.field_reserve_pub, reservePub.toCrock()); - form.append(detail.field_mint, detail.mint); - // XXX: set bank-specified fields. - let mintBaseUrl = canonicalizeBaseUrl(detail.mint); - httpPostForm(detail.post_url, form) - .then((hresp) => { - // TODO: extract as interface - let resp = { - status: hresp.status, - text: hresp.responseText, - success: undefined, - backlink: undefined - }; - let reserveRecord = { - reserve_pub: reservePub.toCrock(), - reserve_priv: reservePriv.toCrock(), - mint_base_url: mintBaseUrl, - created: now, - last_query: null, - current_amount: null, - // XXX: set to actual amount - initial_amount: null + let s: CoinPaySig_interface = { + coin_sig: coinSig, + coin_pub: cd.coin.coinPub, + ub_sig: cd.coin.denomSig, + denom_pub: cd.coin.denomPub, + f: coinSpend.toJson(), }; + ret.push({sig: s, updatedCoin: cd.coin}); + } + return ret; + } - if (hresp.status != 200) { - resp.success = false; - return resp; - } - resp.success = true; - // We can't show the page directly, so - // we show some generic page from the wallet. - // TODO: this should not be webextensions-specific - resp.backlink = chrome.extension.getURL("pages/reserve-success.html"); - return Query(db) - .put("reserves", reserveRecord) - .finish() - .then(() => { - // Do this in the background - updateMintFromUrl(db, reserveRecord.mint_base_url) - .then((mint) => - updateReserve(db, reservePub, mint) - .then((reserve) => depleteReserve(db, reserve, mint)) - ); - return resp; - }); - }) - .then((resp) => { - sendResponse(resp); - }); + /** + * Get mints and associated coins that are still spendable, + * but only if the sum the coins' remaining value exceeds the payment amount. + * @param paymentAmount + * @param depositFeeLimit + * @param allowedMints + */ + getPossibleMintCoins(paymentAmount: AmountJson_interface, + depositFeeLimit: AmountJson_interface, + allowedMints: MintInfo[]): Promise<MintCoins> { - // Allow async response - return true; -} + let m: MintCoins = {}; -function copy(o) { - return JSON.parse(JSON.stringify(o)); -} + function storeMintCoin(mc) { + let mint = mc[0]; + let coin = mc[1]; + let cd = { + coin: coin, + denom: mint.keys.denoms.find((e) => e.denom_pub === coin.denomPub) + }; + if (!cd.denom) { + throw Error("denom not found (database inconsistent)"); + } + let x = m[mint.baseUrl]; + if (!x) { + m[mint.baseUrl] = [cd]; + } else { + x.push(cd); + } + } + let ps = allowedMints.map((info) => { + return Query(this.db) + .iterIndex("mints", "pubKey", info.master_pub) + .indexJoin("coins", "mintBaseUrl", (mint) => mint.baseUrl) + .reduce(storeMintCoin); + }); -function rankDenom(denom1: any, denom2: any) { - // Slow ... we should find a better way than to convert it evert time. - let v1 = new Amount(denom1.value); - let v2 = new Amount(denom2.value); - return (-1) * v1.cmp(v2); -} + return Promise.all(ps).then(() => { + let ret: MintCoins = {}; + + nextMint: + for (let key in m) { + let coins = m[key].map((x) => ({ + a: new Amount(x.denom.fee_deposit), + c: x + })); + // Sort by ascending deposit fee + coins.sort((o1, o2) => o1.a.cmp(o2.a)); + let maxFee = new Amount(depositFeeLimit); + let minAmount = new Amount(paymentAmount); + let accFee = new Amount(coins[0].c.denom.fee_deposit); + let accAmount = Amount.getZero(coins[0].c.coin.currentAmount.currency); + let usableCoins: 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; + } + accFee.add(coinFee); + accAmount.add(coinAmount); + if (accFee.cmp(maxFee) >= 0) { + console.log("too much fees"); + continue nextMint; + } + usableCoins.push(coins[i].c); + if (accAmount.cmp(minAmount) >= 0) { + ret[key] = usableCoins; + continue nextMint; + } + } + } + return ret; + }); + } -function withdrawPrepare(db: IDBDatabase, - denom: Db.Denomination, - reserve: Reserve): Promise<Db.PreCoin> { - let reservePriv = new EddsaPrivateKey(); - reservePriv.loadCrock(reserve.reserve_priv); - let reservePub = new EddsaPublicKey(); - reservePub.loadCrock(reserve.reserve_pub); - let denomPub = RsaPublicKey.fromCrock(denom.denom_pub); - let coinPriv = EddsaPrivateKey.create(); - let coinPub = coinPriv.getPublicKey(); - let blindingFactor = RsaBlindingKey.create(1024); - let pubHash: HashCode = coinPub.hash(); - let ev: ByteArray = rsaBlind(pubHash, blindingFactor, denomPub); - - if (!denom.fee_withdraw) { - throw Error("Field fee_withdraw missing"); - } + executePay(offer: Offer, + payCoinInfo: PayCoinInfo, + merchantBaseUrl: string, + chosenMint: string): Promise<void> { + let payReq = {}; + payReq["H_wire"] = offer.contract.H_wire; + payReq["H_contract"] = offer.H_contract; + payReq["transaction_id"] = offer.contract.transaction_id; + payReq["refund_deadline"] = offer.contract.refund_deadline; + payReq["mint"] = URI(chosenMint).href(); + payReq["coins"] = payCoinInfo.map((x) => x.sig); + payReq["timestamp"] = offer.contract.timestamp; + let payUrl = URI(offer.pay_url).absoluteTo(merchantBaseUrl); + let t: Transaction = { + contractHash: offer.H_contract, + contract: offer.contract, + payUrl: payUrl.href(), + payReq: payReq + }; - let amountWithFee = new Amount(denom.value); - amountWithFee.add(new Amount(denom.fee_withdraw)); - let withdrawFee = new Amount(denom.fee_withdraw); - - // Signature - let withdrawRequest = new WithdrawRequestPS({ - reserve_pub: reservePub, - amount_with_fee: amountWithFee.toNbo(), - withdraw_fee: withdrawFee.toNbo(), - h_denomination_pub: denomPub.encode().hash(), - h_coin_envelope: ev.hash() - }); - - var sig = eddsaSign(withdrawRequest.toPurpose(), reservePriv); - - let preCoin: Db.PreCoin = { - reservePub: reservePub.toCrock(), - blindingKey: blindingFactor.toCrock(), - coinPub: coinPub.toCrock(), - coinPriv: coinPriv.toCrock(), - denomPub: denomPub.encode().toCrock(), - mintBaseUrl: reserve.mint_base_url, - withdrawSig: sig.toCrock(), - coinEv: ev.toCrock(), - coinValue: denom.value - }; - - return Query(db).put("precoins", preCoin).finish().then(() => preCoin); -} + return Query(this.db) + .put("transactions", t) + .putAll("coins", payCoinInfo.map((pci) => pci.updatedCoin)) + .finish(); + } + confirmPay(offer: Offer, merchantPageUrl: string): Promise<any> { + return Promise.resolve().then(() => { + return this.getPossibleMintCoins(offer.contract.amount, + offer.contract.max_fee, + offer.contract.mints) + }).then((mcs) => { + if (Object.keys(mcs).length == 0) { + throw Error("Not enough coins."); + } + let mintUrl = Object.keys(mcs)[0]; + let ds = Wallet.signDeposit(offer, mcs[mintUrl]); + return this.executePay(offer, ds, merchantPageUrl, mintUrl); + }); + } -function withdrawExecute(db, pc: Db.PreCoin): Promise<Db.Coin> { - 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); - return httpPostJson(reqUrl, wd); - }) - .then(resp => { - if (resp.status != 200) { - throw new RequestException({ - hint: "Withdrawal failed", - status: resp.status + doPayment(H_contract): Promise<PaymentResponse> { + return Promise.resolve().then(() => { + return Query(this.db) + .get("transactions", H_contract) + .then((t) => { + if (!t) { + throw Error("contract not found"); + } + let resp: PaymentResponse = { + payUrl: t.payUrl, + payReq: t.payReq + }; + return resp; }); - } - 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; }); -} + } + confirmReserve(req: ConfirmReserveRequest): Promise<ConfirmReserveResponse> { + let reservePriv = EddsaPrivateKey.create(); + let reservePub = reservePriv.getPublicKey(); + let form = new FormData(); + let now = (new Date()).toString(); + form.append(req.field_amount, req.amount_str); + form.append(req.field_reserve_pub, reservePub.toCrock()); + form.append(req.field_mint, req.mint); + // TODO: set bank-specified fields. + let mintBaseUrl = canonicalizeBaseUrl(req.mint); + + return httpPostForm(req.post_url, form) + .then((hresp) => { + let resp: ConfirmReserveResponse = { + status: hresp.status, + text: hresp.responseText, + success: undefined, + backlink: undefined + }; + let reserveRecord = { + reserve_pub: reservePub.toCrock(), + reserve_priv: reservePriv.toCrock(), + mint_base_url: mintBaseUrl, + created: now, + last_query: null, + current_amount: null, + // XXX: set to actual amount + initial_amount: null + }; + + if (hresp.status != 200) { + resp.success = false; + return resp; + } -function updateBadge(db) { - function countNonEmpty(c, n) { - if (c.currentAmount.fraction != 0 || c.currentAmount.value != 0) { - return n + 1; - } - return n; + resp.success = true; + // We can't show the page directly, so + // we show some generic page from the wallet. + resp.backlink = null; + return Query(this.db) + .put("reserves", reserveRecord) + .finish() + .then(() => { + // Do this in the background + this.updateMintFromUrl(reserveRecord.mint_base_url) + .then((mint) => + this.updateReserve(reservePub, mint) + .then((reserve) => this.depleteReserve(reserve, + mint)) + ); + return resp; + }); + }); } - function doBadge(n) { - chrome.browserAction.setBadgeText({text: "" + n}); - chrome.browserAction.setBadgeBackgroundColor({color: "#0F0"}); - } + withdrawPrepare(denom: Db.Denomination, + reserve: Reserve): Promise<Db.PreCoin> { + let reservePriv = new EddsaPrivateKey(); + reservePriv.loadCrock(reserve.reserve_priv); + let reservePub = new EddsaPublicKey(); + reservePub.loadCrock(reserve.reserve_pub); + let denomPub = RsaPublicKey.fromCrock(denom.denom_pub); + let coinPriv = EddsaPrivateKey.create(); + let coinPub = coinPriv.getPublicKey(); + let blindingFactor = RsaBlindingKey.create(1024); + let pubHash: HashCode = coinPub.hash(); + let ev: ByteArray = rsaBlind(pubHash, blindingFactor, denomPub); + + if (!denom.fee_withdraw) { + throw Error("Field fee_withdraw missing"); + } - Query(db) - .iter("coins") - .reduce(countNonEmpty, 0) - .then(doBadge); -} + let amountWithFee = new Amount(denom.value); + amountWithFee.add(new Amount(denom.fee_withdraw)); + let withdrawFee = new Amount(denom.fee_withdraw); + + // Signature + let withdrawRequest = new WithdrawRequestPS({ + reserve_pub: reservePub, + amount_with_fee: amountWithFee.toNbo(), + withdraw_fee: withdrawFee.toNbo(), + h_denomination_pub: denomPub.encode().hash(), + h_coin_envelope: ev.hash() + }); + var sig = eddsaSign(withdrawRequest.toPurpose(), reservePriv); + + let preCoin: Db.PreCoin = { + reservePub: reservePub.toCrock(), + blindingKey: blindingFactor.toCrock(), + coinPub: coinPub.toCrock(), + coinPriv: coinPriv.toCrock(), + denomPub: denomPub.encode().toCrock(), + mintBaseUrl: reserve.mint_base_url, + withdrawSig: sig.toCrock(), + coinEv: ev.toCrock(), + coinValue: denom.value + }; -function storeCoin(db, coin: Db.Coin) { - Query(db) - .delete("precoins", coin.coinPub) - .add("coins", coin) - .finish() - .then(() => { - updateBadge(db); - }); -} + return Query(this.db).put("precoins", preCoin).finish().then(() => preCoin); + } -function withdraw(db, denom, reserve): Promise<void> { - return withdrawPrepare(db, denom, reserve) - .then((pc) => withdrawExecute(db, pc)) - .then((c) => storeCoin(db, c)); -} + withdrawExecute(pc: Db.PreCoin): Promise<Db.Coin> { + return Query(this.db) + .get("reserves", pc.reservePub) + .then((r) => { + let wd: any = {}; + wd.denom_pub = pc.denomPub; + wd.reserve_pub = pc.reservePub; + wd.reserve_sig = pc.withdrawSig; + wd.coin_ev = pc.coinEv; + let reqUrl = URI("reserve/withdraw").absoluteTo(r.mint_base_url); + return httpPostJson(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; + }); + } -/** - * Withdraw coins from a reserve until it is empty. - */ -function depleteReserve(db, reserve, mint): void { - let denoms = copy(mint.keys.denoms); - let remaining = new Amount(reserve.current_amount); - denoms.sort(rankDenom); - let workList = []; - for (let i = 0; i < 1000; i++) { - let found = false; - for (let d of denoms) { - let cost = new Amount(d.value); - cost.add(new Amount(d.fee_withdraw)); - if (remaining.cmp(cost) < 0) { - continue; + updateBadge() { + function countNonEmpty(c, n) { + if (c.currentAmount.fraction != 0 || c.currentAmount.value != 0) { + return n + 1; } - found = true; - remaining.sub(cost); - workList.push(d); + return n; } - if (!found) { - console.log("did not find coins for remaining ", remaining.toJson()); - break; + + function doBadge(n) { + chrome.browserAction.setBadgeText({text: "" + n}); + chrome.browserAction.setBadgeBackgroundColor({color: "#0F0"}); } + + Query(this.db) + .iter("coins") + .reduce(countNonEmpty, 0) + .then(doBadge); } - // Do the request one by one. - function next(): void { - if (workList.length == 0) { - return; - } - let d = workList.pop(); - withdraw(db, d, reserve) - .then(() => next()); + storeCoin(coin: Db.Coin) { + Query(this.db) + .delete("precoins", coin.coinPub) + .add("coins", coin) + .finish() + .then(() => { + this.updateBadge(); + }); } - next(); -} + withdraw(denom, reserve): Promise<void> { + return this.withdrawPrepare(denom, reserve) + .then((pc) => this.withdrawExecute(pc)) + .then((c) => this.storeCoin(c)); + } -function updateReserve(db: IDBDatabase, - reservePub: EddsaPublicKey, - mint): Promise<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(); + /** + * Withdraw coins from a reserve until it is empty. + */ + depleteReserve(reserve, mint): void { + let denoms = copy(mint.keys.denoms); + let remaining = new Amount(reserve.current_amount); + denoms.sort(rankDenom); + let workList = []; + for (let i = 0; i < 1000; i++) { + let found = false; + for (let d of denoms) { + let cost = new Amount(d.value); + cost.add(new Amount(d.fee_withdraw)); + if (remaining.cmp(cost) < 0) { + continue; } - reserve.current_amount = reserveInfo.balance; - return Query(db) - .put("reserves", reserve) - .finish() - .then(() => reserve); + found = true; + remaining.sub(cost); + workList.push(d); + } + if (!found) { + console.log("did not find coins for remaining ", remaining.toJson()); + break; + } + } + + // Do the request one by one. + let next = () => { + if (workList.length == 0) { + return; + } + let d = workList.pop(); + this.withdraw(d, reserve) + .then(() => next()); + }; + + // Asynchronous recursion + next(); + } + + updateReserve(reservePub: EddsaPublicKey, + mint): Promise<Reserve> { + let reservePubStr = reservePub.toCrock(); + return Query(this.db) + .get("reserves", reservePubStr) + .then((reserve) => { + let reqUrl = URI("reserve/status").absoluteTo(mint.baseUrl); + reqUrl.query({'reserve_pub': reservePubStr}); + return 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; + return Query(this.db) + .put("reserves", reserve) + .finish() + .then(() => reserve); + }); }); + } + + /** + * Update or add mint DB entry by fetching the /keys information. + * Optionally link the reserve entry to the new or existing + * mint entry in then DB. + */ + updateMintFromUrl(baseUrl) { + let reqUrl = URI("keys").absoluteTo(baseUrl); + return 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(this.db).put("mints", mint).finish().then(() => mint); }); -} + } -/** - * 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) { - let reqUrl = URI("keys").absoluteTo(baseUrl); - 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"}); + getBalances(): Promise<any> { + function collectBalances(c: Db.Coin, byCurrency) { + let acc: AmountJson_interface = byCurrency[c.currentAmount.currency]; + if (!acc) { + acc = Amount.getZero(c.currentAmount.currency).toJson(); + } + let am = new Amount(c.currentAmount); + am.add(new Amount(acc)); + byCurrency[c.currentAmount.currency] = am.toJson(); + return byCurrency; } - let mint: Db.Mint = { - baseUrl: baseUrl, - keys: mintKeysJson - }; - return Query(db).put("mints", mint).finish().then(() => mint); - }); -} - -function getBalances(db): Promise<any> { - function collectBalances(c: Db.Coin, byCurrency) { - let acc: AmountJson_interface = byCurrency[c.currentAmount.currency]; - if (!acc) { - acc = Amount.getZero(c.currentAmount.currency).toJson(); - } - let am = new Amount(c.currentAmount); - am.add(new Amount(acc)); - byCurrency[c.currentAmount.currency] = am.toJson(); - return byCurrency; + return Query(this.db) + .iter("coins") + .reduce(collectBalances, {}); } - - return Query(db) - .iter("coins") - .reduce(collectBalances, {}); } diff --git a/extension/content_scripts/notify.js b/extension/content_scripts/notify.js index 57d32135d..47c839799 100644 --- a/extension/content_scripts/notify.js +++ b/extension/content_scripts/notify.js @@ -50,7 +50,7 @@ document.addEventListener("taler-create-reserve", function (e) { let uri = URI(chrome.extension.getURL("pages/confirm-create-reserve.html")); document.location.href = uri.query(params).href(); }); -document.addEventListener('taler-contract', function (e) { +document.addEventListener("taler-contract", function (e) { // XXX: the merchant should just give us the parsed data ... let offer = JSON.parse(e.detail); let uri = URI(chrome.extension.getURL("pages/confirm-contract.html")); diff --git a/extension/content_scripts/notify.ts b/extension/content_scripts/notify.ts index a75ab09cf..e2ffaefa9 100644 --- a/extension/content_scripts/notify.ts +++ b/extension/content_scripts/notify.ts @@ -57,7 +57,7 @@ document.addEventListener("taler-create-reserve", function(e: CustomEvent) { document.location.href = uri.query(params).href(); }); -document.addEventListener('taler-contract', function(e: CustomEvent) { +document.addEventListener("taler-contract", function(e: CustomEvent) { // XXX: the merchant should just give us the parsed data ... let offer = JSON.parse(e.detail); let uri = URI(chrome.extension.getURL("pages/confirm-contract.html")); diff --git a/extension/manifest.json b/extension/manifest.json index 7c0f295b4..c913eafed 100644 --- a/extension/manifest.json +++ b/extension/manifest.json @@ -43,12 +43,12 @@ "scripts": [ "lib/util.js", "lib/URI.js", + "background/checkable.js", "background/libwrapper.js", "background/emscriptif.js", "background/db.js", "background/query.js", "background/messaging.js", - "background/checkable.js", "background/http.js", "background/wallet.js" ] diff --git a/extension/popup/balance-overview.html b/extension/popup/balance-overview.html index 1bc80d97e..f6adc521d 100644 --- a/extension/popup/balance-overview.html +++ b/extension/popup/balance-overview.html @@ -22,7 +22,7 @@ <body> <div id="header" class="nav"> <a href="balance-overview.html" class="active">Wallet</a> - <a href="transactions.html">Transactions</a> + <a href="history.html">History</a> <a href="reserves.html">Reserves</a> <button id="debug">Debug!</button> <button id="reset">Reset!</button> diff --git a/extension/popup/balance-overview.js b/extension/popup/balance-overview.js index 6b6aaf794..08ad465f4 100644 --- a/extension/popup/balance-overview.js +++ b/extension/popup/balance-overview.js @@ -1,5 +1,20 @@ +/* + This file is part of TALER + (C) 2016 GNUnet e.V. + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, If not, see <http://www.gnu.org/licenses/> + */ "use strict"; -document.addEventListener('DOMContentLoaded', (e) => { +document.addEventListener("DOMContentLoaded", (e) => { console.log("content loaded"); chrome.runtime.sendMessage({ type: "balances" }, function (wallet) { let context = document.getElementById("balance-template").innerHTML; diff --git a/extension/popup/balance-overview.tsx b/extension/popup/balance-overview.tsx index a78111bb1..8a278d6a1 100644 --- a/extension/popup/balance-overview.tsx +++ b/extension/popup/balance-overview.tsx @@ -1,6 +1,22 @@ +/* + This file is part of TALER + (C) 2016 GNUnet e.V. + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, If not, see <http://www.gnu.org/licenses/> + */ + "use strict"; -document.addEventListener('DOMContentLoaded', (e) => { +document.addEventListener("DOMContentLoaded", (e) => { console.log("content loaded"); chrome.runtime.sendMessage({type: "balances"}, function(wallet) { let context = document.getElementById("balance-template").innerHTML; @@ -12,16 +28,16 @@ document.addEventListener('DOMContentLoaded', (e) => { el.onclick = (e) => { let target: any = e.target; chrome.tabs.create({ - "url": target.href - }); + "url": target.href + }); }; } }); document.getElementById("debug").addEventListener("click", (e) => { chrome.tabs.create({ - "url": chrome.extension.getURL("pages/debug.html") - }); + "url": chrome.extension.getURL("pages/debug.html") + }); }); document.getElementById("reset").addEventListener("click", (e) => { chrome.runtime.sendMessage({type: "reset"}); diff --git a/extension/popup/history.html b/extension/popup/history.html new file mode 100644 index 000000000..dccc84605 --- /dev/null +++ b/extension/popup/history.html @@ -0,0 +1,32 @@ +<!DOCTYPE html> + +<html> +<head> + <meta charset="utf-8"> + <link rel="stylesheet" href="popup.css" type="text/css"> + <script src="../lib/util.js" type="text/javascript"></script> + <script src="history.js" type="text/javascript"></script> + + <script id="balance-template" type="text/x-handlebars-template"> + {{#each transactions}} + <p>bla</p> + {{else}} + There's nothing here. Go to + our <a href="http://demo.taler.net">demo site</a> to try GNU Taler. + {{/each}} + </script> +</head> + +<body> +<div id="header" class="nav"> + <a href="balance-overview.html">Wallet</a> + <a href="history.html" class="active">Transactions</a> + <a href="reserves.html">Reserves</a> +</div> + +<div id="content"> + (Loading...) +</div> + +</body> +</html> diff --git a/extension/popup/history.tsx b/extension/popup/history.tsx new file mode 100644 index 000000000..387f19c80 --- /dev/null +++ b/extension/popup/history.tsx @@ -0,0 +1,22 @@ +/* + This file is part of TALER + (C) 2016 GNUnet e.V. + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, If not, see <http://www.gnu.org/licenses/> + */ + + +"use strict"; + +document.addEventListener("DOMContentLoaded", (e) => { + +}); diff --git a/extension/popup/reserve-create-sepa.html b/extension/popup/reserve-create-sepa.html index d360f05ed..12d02b832 100644 --- a/extension/popup/reserve-create-sepa.html +++ b/extension/popup/reserve-create-sepa.html @@ -9,7 +9,7 @@ <body> <div id="header" class="nav"> <a href="balance-overview.html">Wallet</a> - <a href="transactions.html">Transactions</a> + <a href="history.html">Transactions</a> <a href="reserves.html" class="active">Reserves</a> </div> diff --git a/extension/popup/reserve-create.html b/extension/popup/reserve-create.html index 423e519c1..165496ab6 100644 --- a/extension/popup/reserve-create.html +++ b/extension/popup/reserve-create.html @@ -9,7 +9,7 @@ <body> <div id="header" class="nav"> <a href="balance-overview.html">Wallet</a> - <a href="transactions.html">Transactions</a> + <a href="history.html">Transactions</a> <a href="reserves.html" class="active">Reserves</a> </div> diff --git a/extension/popup/reserves.html b/extension/popup/reserves.html index 0785cb5ca..424f59d3c 100644 --- a/extension/popup/reserves.html +++ b/extension/popup/reserves.html @@ -9,7 +9,7 @@ <body> <div id="header" class="nav"> <a href="balance-overview.html">Wallet</a> - <a href="transactions.html">Transactions</a> + <a href="history.html">Transactions</a> <a href="reserves.html" class="active">Reserves</a> </div> diff --git a/extension/popup/transactions.html b/extension/popup/transactions.html deleted file mode 100644 index e575f0227..000000000 --- a/extension/popup/transactions.html +++ /dev/null @@ -1,62 +0,0 @@ -<!DOCTYPE html> - -<html> -<head> - <meta charset="utf-8"> - <link rel="stylesheet" href="popup.css" type="text/css"> - <script src="../lib/util.js" type="text/javascript"></script> - <script src="transactions.js" type="text/javascript"></script> - - <script id="balance-template" type="text/x-handlebars-template"> - {{#each transactions}} - bla - {{else}} - Looks like you didn't make any transactions. Get some - coins and <a>donate</a> something. - {{/each}} - </script> - -</head> - -<body> -<div id="header" class="nav"> - <a href="balance-overview.html">Wallet</a> - <a href="transactions.html" class="active">Transactions</a> - <a href="reserves.html">Reserves</a> -</div> - -<div id="content"> - <table id="transactions-table" class="hidden"> - <thead> - <tr> - <th>Date</th> - <th>Amount</th> - <th>Status</th> - <th></th> - </tr> - </thead> - <tbody> - <!-- - <tr> - <td class="date">2015-12-21 13:37</td> - <td class="amount">42 EUR</td> - <td class="status">Completed</td> - <td class="contract"><button>Contract</button></td> - </tr> - <tr> - <td class="date">2015-12-22 10:01</td> - <td class="amount">23 USD</td> - <td class="status">Pending</td> - <td class="contract"><button>Contract</button></td> - </tr> - --> - </tbody> - </table> - - <p id="no-transactions"> - There are no transactions to show. - </p> -</div> - -</body> -</html> diff --git a/extension/popup/transactions.js b/extension/popup/transactions.js deleted file mode 100644 index fbd578114..000000000 --- a/extension/popup/transactions.js +++ /dev/null @@ -1,39 +0,0 @@ -'use strict'; - -function add_transaction (date, currency, amount, status, contract) -{ - let table = document.getElementById('transactions-table'); - table.className = table.className.replace(/\bhidden\b/, ''); - let tr = document.createElement('tr'); - table.appendChild(tr); - - let td_date = document.createElement('td'); - td_date.className = 'date'; - let text_date = document.createTextNode(date_format (date)); - tr.appendChild(td_date).appendChild(text_date); - - let td_amount = document.createElement('td'); - td_amount.className = 'amount'; - let text_amount = document.createTextNode(amount +' '+ currency); - tr.appendChild(td_amount).appendChild(text_amount); - - let td_status = document.createElement('td'); - td_status.className = 'status'; - let text_status = document.createTextNode(status); - tr.appendChild(td_status).appendChild(text_status); - - let td_contract = document.createElement('td'); - td_contract.className = 'contract'; - let btn_contract = document.createElement('button'); - btn_contract.appendChild(document.createTextNode('Contract')); - tr.appendChild(td_contract).appendChild(btn_contract); -} - -document.addEventListener('DOMContentLoaded', function () { - let no = document.getElementById('no-transactions'); - - // FIXME - no.className += ' hidden'; - add_transaction (new Date('2015-12-21 13:37'), 'EUR', 42, 'Completed', {}); - add_transaction (new Date('2015-12-22 10:01'), 'USD', 23, 'Pending', {}); -}); diff --git a/extension/tsconfig.json b/extension/tsconfig.json index 5586d78bc..68b437671 100644 --- a/extension/tsconfig.json +++ b/extension/tsconfig.json @@ -16,6 +16,7 @@ "lib/polyfill-react.ts", "content_scripts/notify.ts", "popup/balance-overview.tsx", + "popup/history.tsx", "pages/confirm-contract.tsx", "pages/confirm-create-reserve.tsx" ] |