aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--content_scripts/notify.ts135
-rw-r--r--lib/wallet/wallet.ts59
-rw-r--r--manifest.json4
-rw-r--r--popup/popup.css13
-rw-r--r--popup/popup.tsx69
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<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,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<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 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<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;
}
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) {