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 {
type: string;
listener: (e: CustomEvent) => void;
listener: (e: CustomEvent) => void|Promise<void>;
}
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() {
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,30 +217,20 @@ 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 (resp.hash != offer.H_contract) {
if (contractHash != offer.H_contract) {
console.error("merchant-supplied contract hash is wrong");
return;
}
const walletMsg = {
type: "check-repurchase",
detail: {
contract: offer.contract
},
};
let resp = await checkRepurchase(offer.contract);
chrome.runtime.sendMessage(walletMsg, (resp: any) => {
if (resp.error) {
console.error("wallet backend error", resp);
return;
}
if (resp.isRepurchase) {
console.log("doing repurchase");
console.assert(resp.existingFulfillmentUrl);
@ -205,6 +239,25 @@ namespace TalerNotify {
resp.existingContractHash);
} else {
let merchantName = "(unknown)";
try {
merchantName = offer.contract.merchant.name;
} catch (e) {
// bad contract / name not included
}
let historyEntry = {
timestamp: (new Date).getTime(),
subjectId: `contract-${contractHash}`,
type: "offer-contract",
detail: {
contractHash,
merchantName,
}
};
await putHistory(historyEntry);
const uri = URI(chrome.extension.getURL(
"pages/confirm-contract.html"));
const params = {
@ -219,8 +272,6 @@ namespace TalerNotify {
}
}
});
});
});
addHandler("taler-payment-failed", (msg: any, sendResponse: any) => {
const walletMsg = {

View File

@ -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<void> {
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 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<void> {
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<void> {
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<void> {
exchange: IExchangeInfo): Promise<number> {
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;
}

View File

@ -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": {

View File

@ -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;
}

View File

@ -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,7 +137,11 @@ namespace WalletBalance {
return listing;
}
let helpLink = m("a",
{config: openInExtension, href: chrome.extension.getURL("pages/help/empty-wallet.html")},
{
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});
return m("p",
[
i18n`Payment for ${formatAmount(d.amount)} to merchant ${d.merchantName}. `,
m(`a`,
let merchantElem = m("em", abbrev(d.merchantName, 15));
let fulfillmentLinkElem = m(`a`,
{href: url, onclick: openTab(url)},
"Retry")
]);
"view product");
return m("p",
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) {