aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--content_scripts/notify.ts80
-rw-r--r--lib/emscripten/emsc.d.ts2
-rw-r--r--lib/wallet/cryptoApi.ts4
-rw-r--r--lib/wallet/cryptoLib.ts5
-rw-r--r--lib/wallet/emscriptif.ts46
-rw-r--r--lib/wallet/wallet.ts78
-rw-r--r--lib/wallet/wxMessaging.ts14
-rw-r--r--popup/popup.tsx20
8 files changed, 198 insertions, 51 deletions
diff --git a/content_scripts/notify.ts b/content_scripts/notify.ts
index abc72616c..e50c93c4d 100644
--- a/content_scripts/notify.ts
+++ b/content_scripts/notify.ts
@@ -76,8 +76,8 @@ namespace TalerNotify {
console.log("it's execute");
document.documentElement.style.visibility = "hidden";
taler.internalExecutePayment(resp.contractHash,
- resp.payUrl,
- resp.offerUrl);
+ resp.payUrl,
+ resp.offerUrl);
}
});
}
@@ -163,38 +163,62 @@ namespace TalerNotify {
return;
}
- const walletMsg = {
- type: "check-repurchase",
- detail: {
- contract: offer.contract
- },
+ if (!offer.H_contract) {
+ console.error("H_contract field missing");
+ return;
+ }
+
+ let walletHashContractMsg = {
+ type: "hash-contract",
+ detail: {contract: offer.contract}
};
- chrome.runtime.sendMessage(walletMsg, (resp: any) => {
- if (resp.error) {
- console.error("wallet backend error", resp);
+ chrome.runtime.sendMessage(walletHashContractMsg, (resp: any) => {
+
+ if (!resp.hash) {
+ console.log("error", resp);
+ throw Error("hashing failed");
+ }
+
+ if (resp.hash != offer.H_contract) {
+ console.error("merchant-supplied contract hash is wrong");
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 {
- 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);
+
+ const walletMsg = {
+ type: "check-repurchase",
+ detail: {
+ contract: 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);
+ console.assert(resp.existingContractHash);
+ window.location.href = subst(resp.existingFulfillmentUrl,
+ resp.existingContractHash);
+
} 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;
+ }
}
- }
+ });
});
});
diff --git a/lib/emscripten/emsc.d.ts b/lib/emscripten/emsc.d.ts
index 19a6990e5..b9690433f 100644
--- a/lib/emscripten/emsc.d.ts
+++ b/lib/emscripten/emsc.d.ts
@@ -33,6 +33,8 @@ export interface EmscFunGen {
export declare namespace Module {
var cwrap: EmscFunGen;
+ function stringToUTF8(s: string, addr: number, maxLength: number): void
+
function _free(ptr: number): void;
function _malloc(n: number): number;
diff --git a/lib/wallet/cryptoApi.ts b/lib/wallet/cryptoApi.ts
index 585aa39e7..db29592fc 100644
--- a/lib/wallet/cryptoApi.ts
+++ b/lib/wallet/cryptoApi.ts
@@ -176,6 +176,10 @@ export class CryptoApi {
return this.doRpc("createPreCoin", 1, denom, reserve);
}
+ hashString(str: string): Promise<string> {
+ return this.doRpc("hashString", 1, str);
+ }
+
hashRsaPub(rsaPub: string): Promise<string> {
return this.doRpc("hashRsaPub", 2, rsaPub);
}
diff --git a/lib/wallet/cryptoLib.ts b/lib/wallet/cryptoLib.ts
index 58a3d5004..9a77b3d74 100644
--- a/lib/wallet/cryptoLib.ts
+++ b/lib/wallet/cryptoLib.ts
@@ -139,6 +139,11 @@ namespace RpcFunctions {
}
+ export function hashString(str: string): string {
+ const b = native.ByteArray.fromString(str);
+ return b.hash().toCrock();
+ }
+
export function hashRsaPub(rsaPub: string): string {
return native.RsaPublicKey.fromCrock(rsaPub)
diff --git a/lib/wallet/emscriptif.ts b/lib/wallet/emscriptif.ts
index 1e5fb0283..9a1d902c0 100644
--- a/lib/wallet/emscriptif.ts
+++ b/lib/wallet/emscriptif.ts
@@ -36,8 +36,9 @@ const GNUNET_SYSERR = -1;
let Module = EmscWrapper.Module;
-let getEmsc: EmscWrapper.EmscFunGen = (...args: any[]) => Module.cwrap.apply(null,
- args);
+let getEmsc: EmscWrapper.EmscFunGen = (...args: any[]) => Module.cwrap.apply(
+ null,
+ args);
var emsc = {
free: (ptr: number) => Module._free(ptr),
@@ -396,6 +397,30 @@ export class Amount extends ArenaObject {
/**
+ * Count the UTF-8 characters in a JavaScript string.
+ */
+function countBytes(str: string): number {
+ var s = str.length;
+ // JavaScript strings are UTF-16 arrays
+ for (let i = str.length - 1; i >= 0; i--) {
+ var code = str.charCodeAt(i);
+ if (code > 0x7f && code <= 0x7ff) {
+ // We need an extra byte in utf-8 here
+ s++;
+ } else if (code > 0x7ff && code <= 0xffff) {
+ // We need two extra bytes in utf-8 here
+ s += 2;
+ }
+ // Skip over the other surrogate
+ if (code >= 0xDC00 && code <= 0xDFFF) {
+ i--;
+ }
+ }
+ return s;
+}
+
+
+/**
* Managed reference to a contiguous block of memory in the Emscripten heap.
* Should contain only data, not pointers.
*/
@@ -632,17 +657,20 @@ export class ByteArray extends PackedArenaObject {
}
static fromString(s: string, a?: Arena): ByteArray {
- let hstr = emscAlloc.malloc(s.length + 1);
- Module.writeStringToMemory(s, hstr);
- return new ByteArray(s.length, hstr, a);
+ // UTF-8 bytes, including 0-terminator
+ let terminatedByteLength = countBytes(s) + 1;
+ let hstr = emscAlloc.malloc(terminatedByteLength);
+ Module.stringToUTF8(s, hstr, terminatedByteLength);
+ return new ByteArray(terminatedByteLength, hstr, a);
}
static fromCrock(s: string, a?: Arena): ByteArray {
- let hstr = emscAlloc.malloc(s.length + 1);
- Module.writeStringToMemory(s, hstr);
- let decodedLen = Math.floor((s.length * 5) / 8);
+ let byteLength = countBytes(s) + 1;
+ let hstr = emscAlloc.malloc(byteLength);
+ Module.stringToUTF8(s, hstr, byteLength);
+ let decodedLen = Math.floor((byteLength * 5) / 8);
let ba = new ByteArray(decodedLen, undefined, a);
- let res = emsc.string_to_data(hstr, s.length, ba.nativePtr, decodedLen);
+ let res = emsc.string_to_data(hstr, byteLength, ba.nativePtr, decodedLen);
emsc.free(hstr);
if (res != GNUNET_OK) {
throw Error("decoding failed");
diff --git a/lib/wallet/wallet.ts b/lib/wallet/wallet.ts
index 35f86399a..0a2c07673 100644
--- a/lib/wallet/wallet.ts
+++ b/lib/wallet/wallet.ts
@@ -45,12 +45,22 @@ import {ExchangeHandle} from "./types";
"use strict";
-
export interface CoinWithDenom {
coin: Coin;
denom: Denomination;
}
+interface ReserveRecord {
+ reserve_pub: string;
+ reserve_priv: string,
+ exchange_base_url: string,
+ created: number,
+ last_query: number|null,
+ current_amount: null,
+ requested_amount: AmountJson,
+ confirmed: boolean,
+}
+
@Checkable.Class
export class KeysJson {
@@ -124,6 +134,13 @@ export class Offer {
static checked: (obj: any) => Offer;
}
+export interface HistoryRecord {
+ type: string;
+ timestamp: number;
+ subjectId?: string;
+ detail: any;
+}
+
interface ExchangeCoins {
[exchangeUrl: string]: CoinWithDenom[];
@@ -145,6 +162,32 @@ export interface Badge {
stopBusy(): void;
}
+export function canonicalJson(obj: any): string {
+ // Check for cycles, etc.
+ JSON.stringify(obj);
+ if (typeof obj == "string" || typeof obj == "number" || obj === null) {
+ return JSON.stringify(obj)
+ }
+ if (Array.isArray(obj)) {
+ let objs: string[] = obj.map((e) => canonicalJson(e));
+ return `[${objs.join(',')}]`;
+ }
+ let keys: string[] = [];
+ for (let key in obj) {
+ keys.push(key);
+ }
+ keys.sort();
+ let s = "{";
+ for (let i = 0; i < keys.length; i++) {
+ let key = keys[i];
+ s += JSON.stringify(key) + ":" + canonicalJson(obj[key]);
+ if (i != keys.length - 1) {
+ s += ",";
+ }
+ }
+ return s + "}";
+}
+
function deepEquals(x: any, y: any): boolean {
if (x === y) {
@@ -467,6 +510,7 @@ export class Wallet {
let historyEntry = {
type: "pay",
timestamp: (new Date).getTime(),
+ subjectId: `contract-${offer.H_contract}`,
detail: {
merchantName: offer.contract.merchant.name,
amount: offer.contract.amount,
@@ -485,6 +529,11 @@ export class Wallet {
}
+ async putHistory(historyEntry: HistoryRecord): Promise<void> {
+ await Query(this.db).put("history", historyEntry).finish();
+ }
+
+
/**
* Add a contract to the wallet and sign coins,
* but do not send them yet.
@@ -574,7 +623,7 @@ export class Wallet {
* First fetch information requred to withdraw from the reserve,
* then deplete the reserve, withdrawing coins until it is empty.
*/
- private async processReserve(reserveRecord: any,
+ private async processReserve(reserveRecord: ReserveRecord,
retryDelayMs: number = 250): Promise<void> {
const opId = "reserve-" + reserveRecord.reserve_pub;
this.startOperation(opId);
@@ -586,9 +635,11 @@ export class Wallet {
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();
@@ -630,7 +681,7 @@ export class Wallet {
const now = (new Date).getTime();
const canonExchange = canonicalizeBaseUrl(req.exchange);
- const reserveRecord = {
+ const reserveRecord: ReserveRecord = {
reserve_pub: keypair.pub,
reserve_priv: keypair.priv,
exchange_base_url: canonExchange,
@@ -644,6 +695,7 @@ export class Wallet {
const historyEntry = {
type: "create-reserve",
timestamp: now,
+ subjectId: `reserve-progress-${reserveRecord.reserve_pub}`,
detail: {
requestedAmount: req.amount,
reservePub: reserveRecord.reserve_pub,
@@ -674,26 +726,28 @@ export class Wallet {
*/
async confirmReserve(req: ConfirmReserveRequest): Promise<void> {
const now = (new Date).getTime();
+ let reserve: ReserveRecord = await Query(this.db)
+ .get("reserves", req.reservePub);
const historyEntry = {
type: "confirm-reserve",
timestamp: now,
+ subjectId: `reserve-progress-${reserve.reserve_pub}`,
detail: {
reservePub: req.reservePub,
+ requestedAmount: reserve.requested_amount,
}
};
- let r = await Query(this.db)
- .get("reserves", req.reservePub);
- if (!r) {
+ if (!reserve) {
console.error("Unable to confirm reserve, not found in DB");
return;
}
- r.confirmed = true;
+ reserve.confirmed = true;
await Query(this.db)
- .put("reserves", r)
+ .put("reserves", reserve)
.put("history", historyEntry)
.finish();
- this.processReserve(r);
+ this.processReserve(reserve);
}
@@ -801,8 +855,10 @@ export class Wallet {
let historyEntry = {
type: "reserve-update",
timestamp: (new Date).getTime(),
+ subjectId: `reserve-progress-${reserve.reserve_pub}`,
detail: {
reservePub,
+ requestedAmount: reserve.requested_amount,
oldAmount,
newAmount
}
@@ -1040,6 +1096,10 @@ export class Wallet {
return {history};
}
+ async hashContract(contract: any): Promise<string> {
+ return this.cryptoApi.hashString(canonicalJson(contract));
+ }
+
/**
* Check if there's an equivalent contract we've already purchased.
*/
diff --git a/lib/wallet/wxMessaging.ts b/lib/wallet/wxMessaging.ts
index be0e09de7..5c97248c4 100644
--- a/lib/wallet/wxMessaging.ts
+++ b/lib/wallet/wxMessaging.ts
@@ -151,6 +151,20 @@ function makeHandlers(db: IDBDatabase,
}
return wallet.updateExchangeFromUrl(detail.baseUrl);
},
+ ["hash-contract"]: function(detail) {
+ if (!detail.contract) {
+ return Promise.resolve({error: "contract missing"});
+ }
+ return wallet.hashContract(detail.contract).then((hash) => {
+ return {hash};
+ });
+ },
+ ["put-history-entry"]: function(detail: any) {
+ if (!detail.historyEntry) {
+ return Promise.resolve({error: "historyEntry missing"});
+ }
+ return wallet.putHistory(detail.historyEntry);
+ },
["reserve-creation-info"]: function(detail, sender) {
if (!detail.baseUrl || typeof detail.baseUrl !== "string") {
return Promise.resolve({error: "bad url"});
diff --git a/popup/popup.tsx b/popup/popup.tsx
index 5a381c987..3797d81dc 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 {Wallet} from "../lib/wallet/wallet";
+import {HistoryRecord} from "../lib/wallet/wallet";
import {AmountJson} from "../lib/wallet/types";
declare var m: any;
@@ -173,7 +173,7 @@ function retryPayment(url: string, contractHash: string) {
}
-function formatHistoryItem(historyItem: any) {
+function formatHistoryItem(historyItem: HistoryRecord) {
const d = historyItem.detail;
const t = historyItem.timestamp;
console.log("hist item", historyItem);
@@ -215,7 +215,7 @@ namespace WalletHistory {
}
class Controller {
- myHistory: any;
+ myHistory: any[];
gotError = false;
constructor() {
@@ -241,14 +241,24 @@ namespace WalletHistory {
}
export function view(ctrl: Controller) {
- let history = ctrl.myHistory;
+ let history: HistoryRecord[] = ctrl.myHistory;
if (ctrl.gotError) {
return i18n`Error: could not retrieve event history`;
}
if (!history) {
throw Error("Could not retrieve history");
}
- let listing = _.map(history, formatHistoryItem);
+
+ let subjectMemo: {[s: string]: boolean} = {};
+ let listing: any[] = [];
+ for (let record of history.reverse()) {
+ //if (record.subjectId && subjectMemo[record.subjectId]) {
+ // return;
+ //}
+ subjectMemo[record.subjectId as string] = true;
+ listing.push(formatHistoryItem(record));
+ }
+
if (listing.length > 0) {
return m("div.container", listing);
}