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); - } else { - document.location.href = 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 { + 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 { + return this.doRpc("hashString", 1, str); + } + hashRsaPub(rsaPub: string): Promise { 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), @@ -395,6 +396,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 { + 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 { 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 { 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 { + 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); }