history aggregation
This commit is contained in:
parent
cc34929da6
commit
693e7c92e0
@ -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) => {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -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": {
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
@ -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) {
|
||||||
|
Loading…
Reference in New Issue
Block a user