history aggregation

This commit is contained in:
Florian Dold 2016-09-29 01:40:29 +02:00
parent cc34929da6
commit 693e7c92e0
5 changed files with 199 additions and 83 deletions

View File

@ -45,10 +45,54 @@ namespace TalerNotify {
interface Handler { interface Handler {
type: string; type: string;
listener: (e: CustomEvent) => void; listener: (e: CustomEvent) => void|Promise<void>;
} }
const handlers: Handler[] = []; const handlers: Handler[] = [];
function hashContract(contract: string): Promise<string> {
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<any> {
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<void> {
const walletMsg = {
type: "put-history-entry",
detail: {
historyEntry,
},
};
return new Promise((resolve, reject) => {
chrome.runtime.sendMessage(walletMsg, (resp: any) => {
resolve();
});
});
}
function init() { function init() {
chrome.runtime.sendMessage({type: "ping"}, (resp) => { chrome.runtime.sendMessage({type: "ping"}, (resp) => {
if (chrome.runtime.lastError) { 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) { if (!msg.contract_wrapper) {
console.error("contract wrapper missing"); console.error("contract wrapper missing");
return; return;
@ -173,53 +217,60 @@ namespace TalerNotify {
detail: {contract: offer.contract} detail: {contract: offer.contract}
}; };
chrome.runtime.sendMessage(walletHashContractMsg, (resp: any) => { let contractHash = await hashContract(offer.contract);
if (!resp.hash) { if (contractHash != offer.H_contract) {
console.log("error", resp); console.error("merchant-supplied contract hash is wrong");
throw Error("hashing failed"); 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
} }
if (resp.hash != offer.H_contract) { let historyEntry = {
console.error("merchant-supplied contract hash is wrong"); timestamp: (new Date).getTime(),
return; subjectId: `contract-${contractHash}`,
} type: "offer-contract",
const walletMsg = {
type: "check-repurchase",
detail: { detail: {
contract: offer.contract contractHash,
}, merchantName,
}
}; };
await putHistory(historyEntry);
chrome.runtime.sendMessage(walletMsg, (resp: any) => { const uri = URI(chrome.extension.getURL(
if (resp.error) { "pages/confirm-contract.html"));
console.error("wallet backend error", resp); const params = {
return; offer: JSON.stringify(offer),
} merchantPageUrl: document.location.href,
if (resp.isRepurchase) { };
console.log("doing repurchase"); const target = uri.query(params).href();
console.assert(resp.existingFulfillmentUrl); if (msg.replace_navigation === true) {
console.assert(resp.existingContractHash); document.location.replace(target);
window.location.href = subst(resp.existingFulfillmentUrl, } else {
resp.existingContractHash); document.location.href = target;
}
} 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;
}
}
});
});
}); });
addHandler("taler-payment-failed", (msg: any, sendResponse: any) => { addHandler("taler-payment-failed", (msg: any, sendResponse: any) => {

View File

@ -56,8 +56,20 @@ interface ReserveRecord {
exchange_base_url: string, exchange_base_url: string,
created: number, created: number,
last_query: number|null, 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, requested_amount: AmountJson,
/**
* Amount we've already withdrawn from the reserve.
*/
withdrawn_amount: AmountJson;
confirmed: boolean, confirmed: boolean,
} }
@ -139,6 +151,7 @@ export interface HistoryRecord {
timestamp: number; timestamp: number;
subjectId?: string; subjectId?: string;
detail: any; detail: any;
level: HistoryLevel;
} }
@ -154,6 +167,13 @@ interface Transaction {
merchantSig: string; merchantSig: string;
} }
export enum HistoryLevel {
Trace = 1,
Developer = 2,
Expert = 3,
User = 4,
}
export interface Badge { export interface Badge {
setText(s: string): void; setText(s: string): void;
@ -531,6 +551,7 @@ export class Wallet {
async putHistory(historyEntry: HistoryRecord): Promise<void> { async putHistory(historyEntry: HistoryRecord): Promise<void> {
await Query(this.db).put("history", historyEntry).finish(); 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 exchange = await this.updateExchangeFromUrl(reserveRecord.exchange_base_url);
let reserve = await this.updateReserve(reserveRecord.reserve_pub, let reserve = await this.updateReserve(reserveRecord.reserve_pub,
exchange); exchange);
await this.depleteReserve(reserve, exchange); let n = await this.depleteReserve(reserve, exchange);
let depleted = {
type: "depleted-reserve", if (n != 0) {
subjectId: `reserve-progress-${reserveRecord.reserve_pub}`, let depleted = {
timestamp: (new Date).getTime(), type: "depleted-reserve",
detail: { subjectId: `reserve-progress-${reserveRecord.reserve_pub}`,
reservePub: reserveRecord.reserve_pub, timestamp: (new Date).getTime(),
currentAmount: reserveRecord.current_amount, detail: {
} reservePub: reserveRecord.reserve_pub,
}; requestedAmount: reserveRecord.requested_amount,
await Query(this.db).put("history", depleted).finish(); currentAmount: reserveRecord.current_amount,
}
};
await Query(this.db).put("history", depleted).finish();
}
} catch (e) { } catch (e) {
// random, exponential backoff truncated at 3 minutes // random, exponential backoff truncated at 3 minutes
let nextDelay = Math.min(2 * retryDelayMs + retryDelayMs * Math.random(), 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<void> { retryDelayMs = 100): Promise<void> {
try { try {
const coin = await this.withdrawExecute(preCoin); const coin = await this.withdrawExecute(preCoin);
@ -690,6 +715,7 @@ export class Wallet {
current_amount: null, current_amount: null,
requested_amount: req.amount, requested_amount: req.amount,
confirmed: false, confirmed: false,
withdrawn_amount: Amounts.getZero(req.amount.currency)
}; };
const historyEntry = { const historyEntry = {
@ -787,9 +813,11 @@ export class Wallet {
async storeCoin(coin: Coin): Promise<void> { async storeCoin(coin: Coin): Promise<void> {
console.log("storing coin", new Date()); console.log("storing coin", new Date());
let historyEntry = {
let historyEntry: HistoryRecord = {
type: "withdraw", type: "withdraw",
timestamp: (new Date).getTime(), timestamp: (new Date).getTime(),
level: HistoryLevel.Expert,
detail: { detail: {
coinPub: coin.coinPub, coinPub: coin.coinPub,
} }
@ -821,13 +849,14 @@ export class Wallet {
* Withdraw coins from a reserve until it is empty. * Withdraw coins from a reserve until it is empty.
*/ */
private async depleteReserve(reserve: any, private async depleteReserve(reserve: any,
exchange: IExchangeInfo): Promise<void> { exchange: IExchangeInfo): Promise<number> {
let denomsAvailable: Denomination[] = copy(exchange.active_denoms); let denomsAvailable: Denomination[] = copy(exchange.active_denoms);
let denomsForWithdraw = getWithdrawDenomList(reserve.current_amount, let denomsForWithdraw = getWithdrawDenomList(reserve.current_amount,
denomsAvailable); denomsAvailable);
let ps = denomsForWithdraw.map((denom) => this.withdraw(denom, reserve)); let ps = denomsForWithdraw.map((denom) => this.withdraw(denom, reserve));
await Promise.all(ps); await Promise.all(ps);
return ps.length;
} }

View File

@ -2,8 +2,8 @@
"description": "Privacy preserving and transparent payments", "description": "Privacy preserving and transparent payments",
"manifest_version": 2, "manifest_version": 2,
"name": "GNU Taler Wallet (git)", "name": "GNU Taler Wallet (git)",
"version": "0.6.6", "version": "0.6.7",
"version_name": "0.0.1-pre2", "version_name": "0.0.1-pre3",
"applications": { "applications": {
"gecko": { "gecko": {

View File

@ -69,3 +69,16 @@ body {
#reserve-create table .input input[type="text"] { #reserve-create table .input input[type="text"] {
width: 100%; 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;
}

View File

@ -30,7 +30,7 @@
import {substituteFulfillmentUrl} from "../lib/wallet/helpers"; import {substituteFulfillmentUrl} from "../lib/wallet/helpers";
import BrowserClickedEvent = chrome.browserAction.BrowserClickedEvent; import BrowserClickedEvent = chrome.browserAction.BrowserClickedEvent;
import {HistoryRecord} from "../lib/wallet/wallet"; import {HistoryRecord, HistoryLevel} from "../lib/wallet/wallet";
import {AmountJson} from "../lib/wallet/types"; import {AmountJson} from "../lib/wallet/types";
declare var m: any; declare var m: any;
@ -92,7 +92,6 @@ function openInExtension(element: HTMLAnchorElement, isInitialized: boolean) {
} }
namespace WalletBalance { namespace WalletBalance {
export function controller() { export function controller() {
return new Controller(); return new Controller();
@ -138,8 +137,12 @@ namespace WalletBalance {
return listing; return listing;
} }
let helpLink = m("a", 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?`; 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) { function abbrev(s: string, n: number = 5) {
return m("span.abbrev", {title: s}, (s.slice(0, 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) { switch (historyItem.type) {
case "create-reserve": case "create-reserve":
return m("p", return m("p",
i18n.parts`Created reserve (${abbrevKey(d.reservePub)}) of ${formatAmount( i18n.parts`Bank requested reserve (${abbrev(d.reservePub)}) for ${formatAmount(
d.requestedAmount)} at ${formatTimestamp( d.requestedAmount)}.`);
t)}`);
case "confirm-reserve": case "confirm-reserve":
return m("p", return m("p",
i18n.parts`Bank confirmed reserve (${abbrevKey(d.reservePub)}) at ${formatTimestamp( i18n.parts`Started to withdraw from reserve (${abbrev(d.reservePub)}) of ${formatAmount(
t)}`); d.requestedAmount)}.`);
case "withdraw": case "withdraw":
return m("p", return m("p",
i18n`Withdraw at ${formatTimestamp(t)}`); 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": case "depleted-reserve":
return m("p", return m("p",
i18n.parts`Wallet depleted reserve (${abbrevKey(d.reservePub)}) at ${formatTimestamp(t)}`); i18n.parts`Withdraw from reserve (${abbrev(d.reservePub)}) of ${formatAmount(
case "pay": d.requestedAmount)} completed.`);
case "pay": {
let url = substituteFulfillmentUrl(d.fulfillmentUrl, let url = substituteFulfillmentUrl(d.fulfillmentUrl,
{H_contract: d.contractHash}); {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", return m("p",
[ i18n.parts`Confirmed payment of ${formatAmount(d.amount)} to merchant ${merchantElem}. (${fulfillmentLinkElem})`);
i18n`Payment for ${formatAmount(d.amount)} to merchant ${d.merchantName}. `, }
m(`a`,
{href: url, onclick: openTab(url)},
"Retry")
]);
default: default:
return m("p", i18n`Unknown event (${historyItem.type})`); return m("p", i18n`Unknown event (${historyItem.type})`);
} }
@ -252,11 +266,20 @@ namespace WalletHistory {
let subjectMemo: {[s: string]: boolean} = {}; let subjectMemo: {[s: string]: boolean} = {};
let listing: any[] = []; let listing: any[] = [];
for (let record of history.reverse()) { for (let record of history.reverse()) {
//if (record.subjectId && subjectMemo[record.subjectId]) { if (record.subjectId && subjectMemo[record.subjectId]) {
// return; continue;
//} }
if (record.level != undefined && record.level < HistoryLevel.User) {
continue;
}
subjectMemo[record.subjectId as string] = true; 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) { if (listing.length > 0) {