From 693e7c92e0dd23ee0577dea7d5f5a44eee1fa5be Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Thu, 29 Sep 2016 01:40:29 +0200 Subject: history aggregation --- content_scripts/notify.ts | 135 +++++++++++++++++++++++++++++++--------------- lib/wallet/wallet.ts | 59 ++++++++++++++------ manifest.json | 4 +- popup/popup.css | 13 +++++ popup/popup.tsx | 69 ++++++++++++++++-------- 5 files changed, 198 insertions(+), 82 deletions(-) diff --git a/content_scripts/notify.ts b/content_scripts/notify.ts index e50c93c4d..ed704aaf0 100644 --- a/content_scripts/notify.ts +++ b/content_scripts/notify.ts @@ -45,10 +45,54 @@ namespace TalerNotify { interface Handler { type: string; - listener: (e: CustomEvent) => void; + listener: (e: CustomEvent) => void|Promise; } const handlers: Handler[] = []; + function hashContract(contract: string): Promise { + let walletHashContractMsg = { + type: "hash-contract", + detail: {contract} + }; + return new Promise((resolve, reject) => { + chrome.runtime.sendMessage(walletHashContractMsg, (resp: any) => { + if (!resp.hash) { + console.log("error", resp); + reject(Error("hashing failed")); + } + resolve(resp.hash); + }); + }); + } + + function checkRepurchase(contract: string): Promise { + const walletMsg = { + type: "check-repurchase", + detail: { + contract: contract + }, + }; + return new Promise((resolve, reject) => { + chrome.runtime.sendMessage(walletMsg, (resp: any) => { + resolve(resp); + }); + }); + } + + function putHistory(historyEntry: any): Promise { + const walletMsg = { + type: "put-history-entry", + detail: { + historyEntry, + }, + }; + return new Promise((resolve, reject) => { + chrome.runtime.sendMessage(walletMsg, (resp: any) => { + resolve(); + }); + }); + } + function init() { chrome.runtime.sendMessage({type: "ping"}, (resp) => { if (chrome.runtime.lastError) { @@ -150,7 +194,7 @@ namespace TalerNotify { }); - addHandler("taler-confirm-contract", (msg: any) => { + addHandler("taler-confirm-contract", async(msg: any) => { if (!msg.contract_wrapper) { console.error("contract wrapper missing"); return; @@ -173,53 +217,60 @@ namespace TalerNotify { detail: {contract: offer.contract} }; - chrome.runtime.sendMessage(walletHashContractMsg, (resp: any) => { + let contractHash = await hashContract(offer.contract); - if (!resp.hash) { - console.log("error", resp); - throw Error("hashing failed"); - } + if (contractHash != offer.H_contract) { + console.error("merchant-supplied contract hash is wrong"); + return; + } - if (resp.hash != offer.H_contract) { - console.error("merchant-supplied contract hash is wrong"); - return; + let resp = await checkRepurchase(offer.contract); + + if (resp.error) { + console.error("wallet backend error", resp); + return; + } + + if (resp.isRepurchase) { + console.log("doing repurchase"); + console.assert(resp.existingFulfillmentUrl); + console.assert(resp.existingContractHash); + window.location.href = subst(resp.existingFulfillmentUrl, + resp.existingContractHash); + + } else { + + let merchantName = "(unknown)"; + try { + merchantName = offer.contract.merchant.name; + } catch (e) { + // bad contract / name not included } - const walletMsg = { - type: "check-repurchase", + let historyEntry = { + timestamp: (new Date).getTime(), + subjectId: `contract-${contractHash}`, + type: "offer-contract", detail: { - contract: offer.contract - }, - }; - - chrome.runtime.sendMessage(walletMsg, (resp: any) => { - if (resp.error) { - console.error("wallet backend error", resp); - return; + contractHash, + merchantName, } - if (resp.isRepurchase) { - console.log("doing repurchase"); - console.assert(resp.existingFulfillmentUrl); - console.assert(resp.existingContractHash); - window.location.href = subst(resp.existingFulfillmentUrl, - resp.existingContractHash); + }; + await putHistory(historyEntry); - } else { - const uri = URI(chrome.extension.getURL( - "pages/confirm-contract.html")); - const params = { - offer: JSON.stringify(offer), - merchantPageUrl: document.location.href, - }; - const target = uri.query(params).href(); - if (msg.replace_navigation === true) { - document.location.replace(target); - } else { - document.location.href = target; - } - } - }); - }); + const uri = URI(chrome.extension.getURL( + "pages/confirm-contract.html")); + const params = { + offer: JSON.stringify(offer), + merchantPageUrl: document.location.href, + }; + const target = uri.query(params).href(); + if (msg.replace_navigation === true) { + document.location.replace(target); + } else { + document.location.href = target; + } + } }); addHandler("taler-payment-failed", (msg: any, sendResponse: any) => { diff --git a/lib/wallet/wallet.ts b/lib/wallet/wallet.ts index 0a2c07673..45d083570 100644 --- a/lib/wallet/wallet.ts +++ b/lib/wallet/wallet.ts @@ -56,8 +56,20 @@ interface ReserveRecord { exchange_base_url: string, created: number, last_query: number|null, - current_amount: null, + /** + * Current amount left in the reserve + */ + current_amount: AmountJson|null, + /** + * Amount requested when the reserve was created. + * When a reserve is re-used (rare!) the current_amount can + * be higher than the requested_amount + */ requested_amount: AmountJson, + /** + * Amount we've already withdrawn from the reserve. + */ + withdrawn_amount: AmountJson; confirmed: boolean, } @@ -139,6 +151,7 @@ export interface HistoryRecord { timestamp: number; subjectId?: string; detail: any; + level: HistoryLevel; } @@ -154,6 +167,13 @@ interface Transaction { merchantSig: string; } +export enum HistoryLevel { + Trace = 1, + Developer = 2, + Expert = 3, + User = 4, +} + export interface Badge { setText(s: string): void; @@ -531,6 +551,7 @@ export class Wallet { async putHistory(historyEntry: HistoryRecord): Promise { await Query(this.db).put("history", historyEntry).finish(); + this.notifier.notify(); } @@ -632,17 +653,21 @@ export class Wallet { let exchange = await this.updateExchangeFromUrl(reserveRecord.exchange_base_url); let reserve = await this.updateReserve(reserveRecord.reserve_pub, exchange); - await this.depleteReserve(reserve, exchange); - let depleted = { - type: "depleted-reserve", - subjectId: `reserve-progress-${reserveRecord.reserve_pub}`, - timestamp: (new Date).getTime(), - detail: { - reservePub: reserveRecord.reserve_pub, - currentAmount: reserveRecord.current_amount, - } - }; - await Query(this.db).put("history", depleted).finish(); + let n = await this.depleteReserve(reserve, exchange); + + if (n != 0) { + let depleted = { + type: "depleted-reserve", + subjectId: `reserve-progress-${reserveRecord.reserve_pub}`, + timestamp: (new Date).getTime(), + detail: { + reservePub: reserveRecord.reserve_pub, + requestedAmount: reserveRecord.requested_amount, + currentAmount: reserveRecord.current_amount, + } + }; + await Query(this.db).put("history", depleted).finish(); + } } catch (e) { // random, exponential backoff truncated at 3 minutes let nextDelay = Math.min(2 * retryDelayMs + retryDelayMs * Math.random(), @@ -656,7 +681,7 @@ export class Wallet { } - private async processPreCoin(preCoin: any, + private async processPreCoin(preCoin: PreCoin, retryDelayMs = 100): Promise { try { const coin = await this.withdrawExecute(preCoin); @@ -690,6 +715,7 @@ export class Wallet { current_amount: null, requested_amount: req.amount, confirmed: false, + withdrawn_amount: Amounts.getZero(req.amount.currency) }; const historyEntry = { @@ -787,9 +813,11 @@ export class Wallet { async storeCoin(coin: Coin): Promise { console.log("storing coin", new Date()); - let historyEntry = { + + let historyEntry: HistoryRecord = { type: "withdraw", timestamp: (new Date).getTime(), + level: HistoryLevel.Expert, detail: { coinPub: coin.coinPub, } @@ -821,13 +849,14 @@ export class Wallet { * Withdraw coins from a reserve until it is empty. */ private async depleteReserve(reserve: any, - exchange: IExchangeInfo): Promise { + exchange: IExchangeInfo): Promise { let denomsAvailable: Denomination[] = copy(exchange.active_denoms); let denomsForWithdraw = getWithdrawDenomList(reserve.current_amount, denomsAvailable); let ps = denomsForWithdraw.map((denom) => this.withdraw(denom, reserve)); await Promise.all(ps); + return ps.length; } diff --git a/manifest.json b/manifest.json index d5c553460..947fe2b99 100644 --- a/manifest.json +++ b/manifest.json @@ -2,8 +2,8 @@ "description": "Privacy preserving and transparent payments", "manifest_version": 2, "name": "GNU Taler Wallet (git)", - "version": "0.6.6", - "version_name": "0.0.1-pre2", + "version": "0.6.7", + "version_name": "0.0.1-pre3", "applications": { "gecko": { diff --git a/popup/popup.css b/popup/popup.css index 7218b7baf..675412c11 100644 --- a/popup/popup.css +++ b/popup/popup.css @@ -69,3 +69,16 @@ body { #reserve-create table .input input[type="text"] { width: 100%; } + +.historyItem { + border: 1px solid black; + border-radius: 10px; + padding-left: 0.5em; + margin: 0.5em; +} + +.historyDate { + font-size: 90%; + margin: 0.3em; + color: slategray; +} diff --git a/popup/popup.tsx b/popup/popup.tsx index 3797d81dc..946b148a6 100644 --- a/popup/popup.tsx +++ b/popup/popup.tsx @@ -30,7 +30,7 @@ import {substituteFulfillmentUrl} from "../lib/wallet/helpers"; import BrowserClickedEvent = chrome.browserAction.BrowserClickedEvent; -import {HistoryRecord} from "../lib/wallet/wallet"; +import {HistoryRecord, HistoryLevel} from "../lib/wallet/wallet"; import {AmountJson} from "../lib/wallet/types"; declare var m: any; @@ -92,7 +92,6 @@ function openInExtension(element: HTMLAnchorElement, isInitialized: boolean) { } - namespace WalletBalance { export function controller() { return new Controller(); @@ -138,8 +137,12 @@ namespace WalletBalance { return listing; } let helpLink = m("a", - {config: openInExtension, href: chrome.extension.getURL("pages/help/empty-wallet.html")}, - i18n`help`); + { + config: openInExtension, + href: chrome.extension.getURL( + "pages/help/empty-wallet.html") + }, + i18n`help`); return i18n.parts`You have no balance to show. Need some ${helpLink} getting started?`; } @@ -158,8 +161,12 @@ function formatAmount(amount: AmountJson) { } -function abbrevKey(s: string) { - return m("span.abbrev", {title: s}, (s.slice(0, 5) + "..")) +function abbrev(s: string, n: number = 5) { + let sAbbrev = s; + if (s.length > n) { + sAbbrev = s.slice(0, n) + ".."; + } + return m("span.abbrev", {title: s}, sAbbrev); } @@ -180,29 +187,36 @@ function formatHistoryItem(historyItem: HistoryRecord) { switch (historyItem.type) { case "create-reserve": return m("p", - i18n.parts`Created reserve (${abbrevKey(d.reservePub)}) of ${formatAmount( - d.requestedAmount)} at ${formatTimestamp( - t)}`); + i18n.parts`Bank requested reserve (${abbrev(d.reservePub)}) for ${formatAmount( + d.requestedAmount)}.`); case "confirm-reserve": return m("p", - i18n.parts`Bank confirmed reserve (${abbrevKey(d.reservePub)}) at ${formatTimestamp( - t)}`); + i18n.parts`Started to withdraw from reserve (${abbrev(d.reservePub)}) of ${formatAmount( + d.requestedAmount)}.`); case "withdraw": return m("p", i18n`Withdraw at ${formatTimestamp(t)}`); + case "offer-contract": { + let link = chrome.extension.getURL("view-contract.html"); + let linkElem = m("a", {href: link}, abbrev(d.contractHash)); + let merchantElem = m("em", abbrev(d.merchantName, 15)); + return m("p", + i18n.parts`Merchant ${merchantElem} offered contract ${linkElem}.`); + } case "depleted-reserve": return m("p", - i18n.parts`Wallet depleted reserve (${abbrevKey(d.reservePub)}) at ${formatTimestamp(t)}`); - case "pay": + i18n.parts`Withdraw from reserve (${abbrev(d.reservePub)}) of ${formatAmount( + d.requestedAmount)} completed.`); + case "pay": { let url = substituteFulfillmentUrl(d.fulfillmentUrl, {H_contract: d.contractHash}); + let merchantElem = m("em", abbrev(d.merchantName, 15)); + let fulfillmentLinkElem = m(`a`, + {href: url, onclick: openTab(url)}, + "view product"); return m("p", - [ - i18n`Payment for ${formatAmount(d.amount)} to merchant ${d.merchantName}. `, - m(`a`, - {href: url, onclick: openTab(url)}, - "Retry") - ]); + i18n.parts`Confirmed payment of ${formatAmount(d.amount)} to merchant ${merchantElem}. (${fulfillmentLinkElem})`); + } default: return m("p", i18n`Unknown event (${historyItem.type})`); } @@ -252,11 +266,20 @@ namespace WalletHistory { let subjectMemo: {[s: string]: boolean} = {}; let listing: any[] = []; for (let record of history.reverse()) { - //if (record.subjectId && subjectMemo[record.subjectId]) { - // return; - //} + if (record.subjectId && subjectMemo[record.subjectId]) { + continue; + } + if (record.level != undefined && record.level < HistoryLevel.User) { + continue; + } subjectMemo[record.subjectId as string] = true; - listing.push(formatHistoryItem(record)); + + let item = m("div.historyItem", {}, [ + m("div.historyDate", {}, (new Date(record.timestamp * 1000)).toString()), + formatHistoryItem(record) + ]); + + listing.push(item); } if (listing.length > 0) { -- cgit v1.2.3