From ded02bf11f31c7b9d9cf8b2c863485a491b4c0ae Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Tue, 15 Nov 2016 15:07:17 +0100 Subject: renaming / dce --- src/background/background.ts | 2 +- src/cryptoApi.ts | 14 +- src/cryptoLib.ts | 26 +- src/db.ts | 117 --------- src/pages/confirm-contract.tsx | 14 +- src/pages/tree.tsx | 20 +- src/types.ts | 18 +- src/wallet.ts | 146 ++++++------ src/wxApi.ts | 14 +- src/wxBackend.ts | 527 +++++++++++++++++++++++++++++++++++++++++ src/wxMessaging.ts | 439 ---------------------------------- tsconfig.json | 9 +- 12 files changed, 656 insertions(+), 690 deletions(-) delete mode 100644 src/db.ts create mode 100644 src/wxBackend.ts delete mode 100644 src/wxMessaging.ts diff --git a/src/background/background.ts b/src/background/background.ts index 57335e023..ba9be16e2 100644 --- a/src/background/background.ts +++ b/src/background/background.ts @@ -30,7 +30,7 @@ window.addEventListener("load", () => { defaultJSExtensions: true, }); - System.import("../wxMessaging") + System.import("../wxBackend") .then((wxMessaging: any) => { // Export as global for debugger (window as any).wxMessaging = wxMessaging; diff --git a/src/cryptoApi.ts b/src/cryptoApi.ts index 41f6c9593..1a17f6dca 100644 --- a/src/cryptoApi.ts +++ b/src/cryptoApi.ts @@ -21,12 +21,12 @@ */ -import {PreCoin, Coin, ReserveRecord, AmountJson} from "./types"; +import {PreCoinRecord, CoinRecord, ReserveRecord, AmountJson} from "./types"; import {Denomination} from "./types"; -import {Offer} from "./wallet"; +import {OfferRecord} from "./wallet"; import {CoinWithDenom} from "./wallet"; import {PayCoinInfo} from "./types"; -import {RefreshSession} from "./types"; +import {RefreshSessionRecord} from "./types"; interface WorkerState { @@ -214,7 +214,7 @@ export class CryptoApi { } - createPreCoin(denom: Denomination, reserve: ReserveRecord): Promise { + createPreCoin(denom: Denomination, reserve: ReserveRecord): Promise { return this.doRpc("createPreCoin", 1, denom, reserve); } @@ -227,7 +227,7 @@ export class CryptoApi { return this.doRpc("isValidDenom", 2, denom, masterPub); } - signDeposit(offer: Offer, + signDeposit(offer: OfferRecord, cds: CoinWithDenom[]): Promise { return this.doRpc("signDeposit", 3, offer, cds); } @@ -242,9 +242,9 @@ export class CryptoApi { createRefreshSession(exchangeBaseUrl: string, kappa: number, - meltCoin: Coin, + meltCoin: CoinRecord, newCoinDenoms: Denomination[], - meltFee: AmountJson): Promise { + meltFee: AmountJson): Promise { return this.doRpc("createRefreshSession", 4, exchangeBaseUrl, diff --git a/src/cryptoLib.ts b/src/cryptoLib.ts index 1db686756..8dc5c0914 100644 --- a/src/cryptoLib.ts +++ b/src/cryptoLib.ts @@ -23,13 +23,13 @@ import * as native from "./emscriptif"; import { - PreCoin, PayCoinInfo, AmountJson, - RefreshSession, RefreshPreCoin, ReserveRecord + PreCoinRecord, PayCoinInfo, AmountJson, + RefreshSessionRecord, RefreshPreCoinRecord, ReserveRecord } from "./types"; import create = chrome.alarms.create; -import {Offer} from "./wallet"; +import {OfferRecord} from "./wallet"; import {CoinWithDenom} from "./wallet"; -import {CoinPaySig, Coin} from "./types"; +import {CoinPaySig, CoinRecord} from "./types"; import {Denomination, Amounts} from "./types"; import {Amount} from "./emscriptif"; import {HashContext} from "./emscriptif"; @@ -68,7 +68,7 @@ namespace RpcFunctions { * reserve. */ export function createPreCoin(denom: Denomination, - reserve: ReserveRecord): PreCoin { + reserve: ReserveRecord): PreCoinRecord { let reservePriv = new native.EddsaPrivateKey(); reservePriv.loadCrock(reserve.reserve_priv); let reservePub = new native.EddsaPublicKey(); @@ -105,7 +105,7 @@ namespace RpcFunctions { var sig = native.eddsaSign(withdrawRequest.toPurpose(), reservePriv); - let preCoin: PreCoin = { + let preCoin: PreCoinRecord = { reservePub: reservePub.toCrock(), blindingKey: blindingFactor.toCrock(), coinPub: coinPub.toCrock(), @@ -170,7 +170,7 @@ namespace RpcFunctions { * Generate updated coins (to store in the database) * and deposit permissions for each given coin. */ - export function signDeposit(offer: Offer, + export function signDeposit(offer: OfferRecord, cds: CoinWithDenom[]): PayCoinInfo { let ret: PayCoinInfo = []; let amountSpent = native.Amount.getZero(cds[0].coin.currentAmount.currency); @@ -228,9 +228,9 @@ namespace RpcFunctions { export function createRefreshSession(exchangeBaseUrl: string, kappa: number, - meltCoin: Coin, + meltCoin: CoinRecord, newCoinDenoms: Denomination[], - meltFee: AmountJson): RefreshSession { + meltFee: AmountJson): RefreshSessionRecord { let valueWithFee = Amounts.getZero(newCoinDenoms[0].value.currency); @@ -248,7 +248,7 @@ namespace RpcFunctions { let transferPubs: string[] = []; let transferPrivs: string[] = []; - let preCoinsForGammas: RefreshPreCoin[][] = []; + let preCoinsForGammas: RefreshPreCoinRecord[][] = []; for (let i = 0; i < kappa; i++) { let t = native.EcdhePrivateKey.create(); @@ -267,7 +267,7 @@ namespace RpcFunctions { sessionHc.read((new native.Amount(valueWithFee)).toNbo()); for (let i = 0; i < kappa; i++) { - let preCoins: RefreshPreCoin[] = []; + let preCoins: RefreshPreCoinRecord[] = []; for (let j = 0; j < newCoinDenoms.length; j++) { let transferPriv = native.EcdhePrivateKey.fromCrock(transferPrivs[i]); @@ -287,7 +287,7 @@ namespace RpcFunctions { if (!ev) { throw Error("couldn't blind (malicious exchange key?)"); } - let preCoin: RefreshPreCoin = { + let preCoin: RefreshPreCoinRecord = { blindingKey: blindingFactor.toCrock(), coinEv: ev.toCrock(), publicKey: coinPub.toCrock(), @@ -320,7 +320,7 @@ namespace RpcFunctions { valueOutput = Amounts.add(valueOutput, denom.value).amount; } - let refreshSession: RefreshSession = { + let refreshSession: RefreshSessionRecord = { meltCoinPub: meltCoin.coinPub, newDenoms: newCoinDenoms.map((d) => d.denom_pub), confirmSig, diff --git a/src/db.ts b/src/db.ts deleted file mode 100644 index 9cffc164c..000000000 --- a/src/db.ts +++ /dev/null @@ -1,117 +0,0 @@ -/* - This file is part of TALER - (C) 2016 GNUnet e.V. - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see - */ - -"use strict"; -import {IExchangeInfo} from "./types"; - -/** - * Declarations and helpers for - * things that are stored in the wallet's - * database. - * @module Db - * @author Florian Dold - */ - -const DB_NAME = "taler"; -const DB_VERSION = 11; - -import {Stores} from "./wallet"; -import {Store, Index} from "./query"; - - - - - -/** - * Return a promise that resolves - * to the taler wallet db. - */ -export function openTalerDb(): Promise { - return new Promise((resolve, reject) => { - const req = indexedDB.open(DB_NAME, DB_VERSION); - req.onerror = (e) => { - reject(e); - }; - req.onsuccess = (e) => { - resolve(req.result); - }; - req.onupgradeneeded = (e) => { - const db = req.result; - console.log("DB: upgrade needed: oldVersion = " + e.oldVersion); - switch (e.oldVersion) { - case 0: // DB does not exist yet - - for (let n in Stores) { - if ((Stores as any)[n] instanceof Store) { - let si: Store = (Stores as any)[n]; - const s = db.createObjectStore(si.name, si.storeParams); - for (let indexName in (si as any)) { - if ((si as any)[indexName] instanceof Index) { - let ii: Index = (si as any)[indexName]; - s.createIndex(ii.indexName, ii.keyPath); - } - } - } - } - break; - default: - if (e.oldVersion != DB_VERSION) { - window.alert("Incompatible wallet dababase version, please reset" + - " db."); - chrome.browserAction.setBadgeText({text: "err"}); - chrome.browserAction.setBadgeBackgroundColor({color: "#F00"}); - throw Error("incompatible DB"); - } - break; - } - }; - }); -} - - -export function exportDb(db: IDBDatabase): Promise { - let dump = { - name: db.name, - version: db.version, - stores: {} as {[s: string]: any}, - }; - - return new Promise((resolve, reject) => { - - let tx = db.transaction(Array.from(db.objectStoreNames)); - tx.addEventListener("complete", () => { - resolve(dump); - }); - for (let i = 0; i < db.objectStoreNames.length; i++) { - let name = db.objectStoreNames[i]; - let storeDump = {} as {[s: string]: any}; - dump.stores[name] = storeDump; - let store = tx.objectStore(name) - .openCursor() - .addEventListener("success", (e: Event) => { - let cursor = (e.target as any).result; - if (cursor) { - storeDump[cursor.key] = cursor.value; - cursor.continue(); - } - }); - } - }); -} - -export function deleteDb() { - indexedDB.deleteDatabase(DB_NAME); -} diff --git a/src/pages/confirm-contract.tsx b/src/pages/confirm-contract.tsx index 2beac08f6..238ae2fb5 100644 --- a/src/pages/confirm-contract.tsx +++ b/src/pages/confirm-contract.tsx @@ -24,8 +24,8 @@ "use strict"; import {substituteFulfillmentUrl} from "src/helpers"; -import {Contract, AmountJson, IExchangeInfo} from "src/types"; -import {Offer} from "src/wallet"; +import {Contract, AmountJson, ExchangeRecord} from "src/types"; +import {OfferRecord} from "src/wallet"; import {renderContract, prettyAmount} from "src/renderHtml"; import {getExchanges} from "src/wxApi"; @@ -37,7 +37,7 @@ interface DetailState { interface DetailProps { contract: Contract collapsed: boolean - exchanges: null|IExchangeInfo[]; + exchanges: null|ExchangeRecord[]; } @@ -78,7 +78,7 @@ class Details extends React.Component { Exchanges in the wallet:
    {(this.props.exchanges || []).map( - (e: IExchangeInfo) => + (e: ExchangeRecord) =>
  • {`${e.baseUrl}: ${e.masterPublicKey}`}
  • )}
@@ -92,10 +92,10 @@ interface ContractPromptProps { } interface ContractPromptState { - offer: Offer|null; + offer: OfferRecord|null; error: string|null; payDisabled: boolean; - exchanges: null|IExchangeInfo[]; + exchanges: null|ExchangeRecord[]; } class ContractPrompt extends React.Component { @@ -125,7 +125,7 @@ class ContractPrompt extends React.Component { + getOffer(): Promise { return new Promise((resolve, reject) => { let msg = { type: 'get-offer', diff --git a/src/pages/tree.tsx b/src/pages/tree.tsx index e368ffe9b..daabdaf49 100644 --- a/src/pages/tree.tsx +++ b/src/pages/tree.tsx @@ -21,8 +21,8 @@ */ -import { IExchangeInfo } from "src/types"; -import { ReserveRecord, Coin, PreCoin, Denomination } from "src/types"; +import { ExchangeRecord } from "src/types"; +import { ReserveRecord, CoinRecord, PreCoinRecord, Denomination } from "src/types"; import { ImplicitStateComponent, StateHolder } from "src/components"; import { getReserves, getExchanges, getCoins, getPreCoins, @@ -87,11 +87,11 @@ class Toggle extends ImplicitStateComponent { interface CoinViewProps { - coin: Coin; + coin: CoinRecord; } interface RefreshDialogProps { - coin: Coin; + coin: CoinRecord; } class RefreshDialog extends ImplicitStateComponent { @@ -134,7 +134,7 @@ class CoinView extends React.Component { interface PreCoinViewProps { - precoin: PreCoin; + precoin: PreCoinRecord; } class PreCoinView extends React.Component { @@ -155,7 +155,7 @@ interface CoinListProps { } class CoinList extends ImplicitStateComponent { - coins = this.makeState(null); + coins = this.makeState(null); expanded = this.makeState(false); constructor(props: CoinListProps) { @@ -194,7 +194,7 @@ interface PreCoinListProps { } class PreCoinList extends ImplicitStateComponent { - precoins = this.makeState(null); + precoins = this.makeState(null); expanded = this.makeState(false); constructor(props: PreCoinListProps) { @@ -224,7 +224,7 @@ class PreCoinList extends ImplicitStateComponent { } interface DenominationListProps { - exchange: IExchangeInfo; + exchange: ExchangeRecord; } interface ExpanderTextProps { @@ -336,7 +336,7 @@ class ReserveList extends ImplicitStateComponent { } interface ExchangeProps { - exchange: IExchangeInfo; + exchange: ExchangeRecord; } class ExchangeView extends React.Component { @@ -358,7 +358,7 @@ class ExchangeView extends React.Component { } interface ExchangesListState { - exchanges?: IExchangeInfo[]; + exchanges?: ExchangeRecord[]; } class ExchangesList extends React.Component { diff --git a/src/types.ts b/src/types.ts index f6458955b..1a9d45871 100644 --- a/src/types.ts +++ b/src/types.ts @@ -132,7 +132,7 @@ export class Denomination { } -export interface IExchangeInfo { +export interface ExchangeRecord { baseUrl: string; masterPublicKey: string; @@ -159,7 +159,7 @@ export interface WireInfo { } export interface ReserveCreationInfo { - exchangeInfo: IExchangeInfo; + exchangeInfo: ExchangeRecord; wireInfo: WireInfo; selectedDenoms: Denomination[]; withdrawFee: AmountJson; @@ -170,7 +170,7 @@ export interface ReserveCreationInfo { /** * A coin that isn't yet signed by an exchange. */ -export interface PreCoin { +export interface PreCoinRecord { coinPub: string; coinPriv: string; reservePub: string; @@ -182,7 +182,7 @@ export interface PreCoin { coinValue: AmountJson; } -export interface RefreshPreCoin { +export interface RefreshPreCoinRecord { publicKey: string; privateKey: string; coinEv: string; @@ -193,7 +193,7 @@ export interface RefreshPreCoin { /** * Ongoing refresh */ -export interface RefreshSession { +export interface RefreshSessionRecord { /** * Public key that's being melted in this session. */ @@ -222,7 +222,7 @@ export interface RefreshSession { newDenoms: string[]; - preCoinsForGammas: RefreshPreCoin[][]; + preCoinsForGammas: RefreshPreCoinRecord[][]; /** @@ -257,10 +257,10 @@ export interface CoinPaySig { } /** - * Coin as stored in the "coins" data store + * CoinRecord as stored in the "coins" data store * of the wallet database. */ -export interface Coin { +export interface CoinRecord { /** * Public key of the coin. */ @@ -436,7 +436,7 @@ export class Contract { } -export type PayCoinInfo = Array<{ updatedCoin: Coin, sig: CoinPaySig }>; +export type PayCoinInfo = Array<{ updatedCoin: CoinRecord, sig: CoinPaySig }>; export namespace Amounts { diff --git a/src/wallet.ts b/src/wallet.ts index 49b0f1135..4388c61c0 100644 --- a/src/wallet.ts +++ b/src/wallet.ts @@ -25,17 +25,17 @@ import { AmountJson, Amounts, CheckRepurchaseResult, - Coin, + CoinRecord, CoinPaySig, Contract, CreateReserveResponse, Denomination, ExchangeHandle, - IExchangeInfo, + ExchangeRecord, Notifier, PayCoinInfo, - PreCoin, - RefreshSession, + PreCoinRecord, + RefreshSessionRecord, ReserveCreationInfo, ReserveRecord, WalletBalance, @@ -68,7 +68,7 @@ import {CryptoApi} from "./cryptoApi"; "use strict"; export interface CoinWithDenom { - coin: Coin; + coin: CoinRecord; denom: Denomination; } @@ -132,7 +132,7 @@ export class ConfirmReserveRequest { @Checkable.Class -export class Offer { +export class OfferRecord { @Checkable.Value(Contract) contract: Contract; @@ -151,7 +151,7 @@ export class Offer { @Checkable.Optional(Checkable.Number) id?: number; - static checked: (obj: any) => Offer; + static checked: (obj: any) => OfferRecord; } export interface HistoryRecord { @@ -163,10 +163,6 @@ export interface HistoryRecord { } -interface ExchangeCoins { - [exchangeUrl: string]: CoinWithDenom[]; -} - interface PayReq { amount: AmountJson; coins: CoinPaySig[]; @@ -185,7 +181,7 @@ interface PayReq { instance?: string; } -interface Transaction { +interface TransactionRecord { contractHash: string; contract: Contract; payReq: PayReq; @@ -303,20 +299,20 @@ function getWithdrawDenomList(amountAvailable: AmountJson, export namespace Stores { - class ExchangeStore extends Store { + class ExchangeStore extends Store { constructor() { super("exchanges", {keyPath: "baseUrl"}); } - pubKeyIndex = new Index(this, "pubKey", "masterPublicKey"); + pubKeyIndex = new Index(this, "pubKey", "masterPublicKey"); } - class CoinsStore extends Store { + class CoinsStore extends Store { constructor() { super("coins", {keyPath: "coinPub"}); } - exchangeBaseUrlIndex = new Index(this, "exchangeBaseUrl", "exchangeBaseUrl"); + exchangeBaseUrlIndex = new Index(this, "exchangeBaseUrl", "exchangeBaseUrl"); } class HistoryStore extends Store { @@ -330,7 +326,7 @@ export namespace Stores { timestampIndex = new Index(this, "timestamp", "timestamp"); } - class OffersStore extends Store { + class OffersStore extends Store { constructor() { super("offers", { keyPath: "id", @@ -339,12 +335,12 @@ export namespace Stores { } } - class TransactionsStore extends Store { + class TransactionsStore extends Store { constructor() { super("transactions", {keyPath: "contractHash"}); } - repurchaseIndex = new Index<[string,string],Transaction>(this, "repurchase", [ + repurchaseIndex = new Index<[string,string],TransactionRecord>(this, "repurchase", [ "contract.merchant_pub", "contract.repurchase_correlation_id" ]); @@ -354,10 +350,10 @@ export namespace Stores { export let transactions: TransactionsStore = new TransactionsStore(); export let reserves: Store = new Store("reserves", {keyPath: "reserve_pub"}); export let coins: CoinsStore = new CoinsStore(); - export let refresh: Store = new Store("refresh", {keyPath: "meltCoinPub"}); + export let refresh: Store = new Store("refresh", {keyPath: "meltCoinPub"}); export let history: HistoryStore = new HistoryStore(); export let offers: OffersStore = new OffersStore(); - export let precoins: Store = new Store("precoins", {keyPath: "coinPub"}); + export let precoins: Store = new Store("precoins", {keyPath: "coinPub"}); } @@ -442,14 +438,14 @@ export class Wallet { this.q() .iter(Stores.refresh) - .reduce((r: RefreshSession) => { + .reduce((r: RefreshSessionRecord) => { this.continueRefreshSession(r); }); // FIXME: optimize via index this.q() .iter(Stores.coins) - .reduce((c: Coin) => { + .reduce((c: CoinRecord) => { if (c.dirty && !c.transactionPending) { this.refresh(c.coinPub); } @@ -471,7 +467,7 @@ export class Wallet { console.error("db inconsistent"); continue; } - let coins: Coin[] = await this.q().iterIndex(Stores.coins.exchangeBaseUrlIndex, exchangeHandle.url).toArray(); + let coins: CoinRecord[] = await this.q().iterIndex(Stores.coins.exchangeBaseUrlIndex, exchangeHandle.url).toArray(); if (!coins || coins.length == 0) { continue; } @@ -515,7 +511,7 @@ export class Wallet { * Record all information that is necessary to * pay for a contract in the wallet's database. */ - private async recordConfirmPay(offer: Offer, + private async recordConfirmPay(offer: OfferRecord, payCoinInfo: PayCoinInfo, chosenExchange: string): Promise { let payReq: PayReq = { @@ -531,7 +527,7 @@ export class Wallet { transaction_id: offer.contract.transaction_id, instance: offer.contract.merchant.instance }; - let t: Transaction = { + let t: TransactionRecord = { contractHash: offer.H_contract, contract: offer.contract, payReq: payReq, @@ -568,7 +564,7 @@ export class Wallet { } - async saveOffer(offer: Offer): Promise { + async saveOffer(offer: OfferRecord): Promise { console.log(`saving offer in wallet.ts`); let id = await this.q().putWithResult(Stores.offers, offer); this.notifier.notify(); @@ -584,7 +580,7 @@ export class Wallet { * Add a contract to the wallet and sign coins, * but do not send them yet. */ - async confirmPay(offer: Offer): Promise { + async confirmPay(offer: OfferRecord): Promise { console.log("executing confirmPay"); let transaction = await this.q().get(Stores.transactions, offer.H_contract); @@ -618,7 +614,7 @@ export class Wallet { * Add a contract to the wallet and sign coins, * but do not send them yet. */ - async checkPay(offer: Offer): Promise { + async checkPay(offer: OfferRecord): Promise { // First check if we already payed for it. let transaction = await this.q().get(Stores.transactions, offer.H_contract); if (transaction) { @@ -645,7 +641,7 @@ export class Wallet { * with the given hash. */ async executePayment(H_contract: string): Promise { - let t = await this.q().get(Stores.transactions, H_contract); + let t = await this.q().get(Stores.transactions, H_contract); if (!t) { return { success: false, @@ -704,7 +700,7 @@ export class Wallet { } - private async processPreCoin(preCoin: PreCoin, + private async processPreCoin(preCoin: PreCoinRecord, retryDelayMs = 100): Promise { let exchange = await this.q().get(Stores.exchanges, @@ -852,7 +848,7 @@ export class Wallet { } - private async withdrawExecute(pc: PreCoin): Promise { + private async withdrawExecute(pc: PreCoinRecord): Promise { let reserve = await this.q().get(Stores.reserves, pc.reservePub); @@ -879,7 +875,7 @@ export class Wallet { let denomSig = await this.cryptoApi.rsaUnblind(r.ev_sig, pc.blindingKey, pc.denomPub); - let coin: Coin = { + let coin: CoinRecord = { coinPub: pc.coinPub, coinPriv: pc.coinPriv, denomPub: pc.denomPub, @@ -897,7 +893,7 @@ export class Wallet { * Withdraw coins from a reserve until it is empty. */ private async depleteReserve(reserve: ReserveRecord, - exchange: IExchangeInfo): Promise { + exchange: ExchangeRecord): Promise { if (!reserve.current_amount) { throw Error("can't withdraw when amount is unknown"); } @@ -947,7 +943,7 @@ export class Wallet { * by quering the reserve's exchange. */ private async updateReserve(reservePub: string, - exchange: IExchangeInfo): Promise { + exchange: ExchangeRecord): Promise { let reserve = await this.q() .get(Stores.reserves, reservePub); if (!reserve) { @@ -1037,7 +1033,7 @@ export class Wallet { * Optionally link the reserve entry to the new or existing * exchange entry in then DB. */ - async updateExchangeFromUrl(baseUrl: string): Promise { + async updateExchangeFromUrl(baseUrl: string): Promise { baseUrl = canonicalizeBaseUrl(baseUrl); let reqUrl = URI("keys").absoluteTo(baseUrl); let resp = await this.http.get(reqUrl); @@ -1048,11 +1044,11 @@ export class Wallet { return this.updateExchangeFromJson(baseUrl, exchangeKeysJson); } - private async suspendCoins(exchangeInfo: IExchangeInfo): Promise { + private async suspendCoins(exchangeInfo: ExchangeRecord): Promise { let suspendedCoins = await ( this.q() .iterIndex(Stores.coins.exchangeBaseUrlIndex, exchangeInfo.baseUrl) - .reduce((coin: Coin, suspendedCoins: Coin[]) => { + .reduce((coin: CoinRecord, suspendedCoins: CoinRecord[]) => { if (!exchangeInfo.active_denoms.find((c) => c.denom_pub == coin.denomPub)) { return Array.prototype.concat(suspendedCoins, [coin]); } @@ -1070,15 +1066,15 @@ export class Wallet { private async updateExchangeFromJson(baseUrl: string, - exchangeKeysJson: KeysJson): Promise { + exchangeKeysJson: KeysJson): Promise { const updateTimeSec = getTalerStampSec(exchangeKeysJson.list_issue_date); if (updateTimeSec === null) { throw Error("invalid update time"); } - let r = await this.q().get(Stores.exchanges, baseUrl); + let r = await this.q().get(Stores.exchanges, baseUrl); - let exchangeInfo: IExchangeInfo; + let exchangeInfo: ExchangeRecord; if (!r) { exchangeInfo = { @@ -1110,8 +1106,8 @@ export class Wallet { } - private async updateExchangeInfo(exchangeInfo: IExchangeInfo, - newKeys: KeysJson): Promise { + private async updateExchangeInfo(exchangeInfo: ExchangeRecord, + newKeys: KeysJson): Promise { if (exchangeInfo.masterPublicKey != newKeys.master_public_key) { throw Error("public keys do not match"); } @@ -1186,7 +1182,7 @@ export class Wallet { return entry; } - function collectBalances(c: Coin, balance: WalletBalance) { + function collectBalances(c: CoinRecord, balance: WalletBalance) { if (c.suspended) { return balance; } @@ -1213,7 +1209,7 @@ export class Wallet { return balance; } - function collectPendingRefresh(r: RefreshSession, balance: WalletBalance) { + function collectPendingRefresh(r: RefreshSessionRecord, balance: WalletBalance) { if (!r.finished) { return balance; } @@ -1224,7 +1220,7 @@ export class Wallet { return balance; } - function collectPayments(t: Transaction, balance: WalletBalance) { + function collectPayments(t: TransactionRecord, balance: WalletBalance) { if (t.finished) { return balance; } @@ -1235,7 +1231,7 @@ export class Wallet { return balance; } - function collectSmallestWithdraw(e: IExchangeInfo, sw: any) { + function collectSmallestWithdraw(e: ExchangeRecord, sw: any) { let min: AmountJson|undefined; for (let d of e.active_denoms) { let v = Amounts.add(d.value, d.fee_withdraw).amount; @@ -1277,8 +1273,8 @@ export class Wallet { } - async createRefreshSession(oldCoinPub: string): Promise { - let coin = await this.q().get(Stores.coins, oldCoinPub); + async createRefreshSession(oldCoinPub: string): Promise { + let coin = await this.q().get(Stores.coins, oldCoinPub); if (!coin) { throw Error("coin not found"); @@ -1312,14 +1308,14 @@ export class Wallet { } - let refreshSession: RefreshSession = await ( + let refreshSession: RefreshSessionRecord = await ( this.cryptoApi.createRefreshSession(exchange.baseUrl, 3, coin, newCoinDenoms, oldDenom.fee_refresh)); - function mutateCoin(c: Coin): Coin { + function mutateCoin(c: CoinRecord): CoinRecord { let r = Amounts.sub(c.currentAmount, refreshSession.valueWithFee); if (r.saturated) { @@ -1340,7 +1336,7 @@ export class Wallet { async refresh(oldCoinPub: string): Promise { - let refreshSession: RefreshSession|undefined; + let refreshSession: RefreshSessionRecord|undefined; let oldSession = await this.q().get(Stores.refresh, oldCoinPub); if (oldSession) { refreshSession = oldSession; @@ -1354,14 +1350,14 @@ export class Wallet { this.continueRefreshSession(refreshSession); } - async continueRefreshSession(refreshSession: RefreshSession) { + async continueRefreshSession(refreshSession: RefreshSessionRecord) { if (refreshSession.finished) { return; } if (typeof refreshSession.norevealIndex !== "number") { let coinPub = refreshSession.meltCoinPub; await this.refreshMelt(refreshSession); - let r = await this.q().get(Stores.refresh, coinPub); + let r = await this.q().get(Stores.refresh, coinPub); if (!r) { throw Error("refresh session does not exist anymore"); } @@ -1372,14 +1368,14 @@ export class Wallet { } - async refreshMelt(refreshSession: RefreshSession): Promise { + async refreshMelt(refreshSession: RefreshSessionRecord): Promise { if (refreshSession.norevealIndex != undefined) { console.error("won't melt again"); return; } - let coin = await this.q().get(Stores.coins, - refreshSession.meltCoinPub); + let coin = await this.q().get(Stores.coins, + refreshSession.meltCoinPub); if (!coin) { console.error("can't melt coin, it does not exist"); return; @@ -1429,7 +1425,7 @@ export class Wallet { } - async refreshReveal(refreshSession: RefreshSession): Promise { + async refreshReveal(refreshSession: RefreshSessionRecord): Promise { let norevealIndex = refreshSession.norevealIndex; if (norevealIndex == undefined) { throw Error("can't reveal without melting first"); @@ -1461,14 +1457,14 @@ export class Wallet { console.log("/refresh/reveal did not contain ev_sigs"); } - let exchange = await this.q().get(Stores.exchanges, - refreshSession.exchangeBaseUrl); + let exchange = await this.q().get(Stores.exchanges, + refreshSession.exchangeBaseUrl); if (!exchange) { console.error(`exchange ${refreshSession.exchangeBaseUrl} not found`); return; } - let coins: Coin[] = []; + let coins: CoinRecord[] = []; for (let i = 0; i < respJson.ev_sigs.length; i++) { let denom = exchange.all_denoms.find((d) => d.denom_pub == refreshSession.newDenoms[i]); @@ -1480,7 +1476,7 @@ export class Wallet { let denomSig = await this.cryptoApi.rsaUnblind(respJson.ev_sigs[i].ev_sig, pc.blindingKey, denom.denom_pub); - let coin: Coin = { + let coin: CoinRecord = { coinPub: pc.publicKey, coinPriv: pc.privateKey, denomPub: denom.denom_pub, @@ -1526,9 +1522,9 @@ export class Wallet { return offer; } - async getExchanges(): Promise { + async getExchanges(): Promise { return this.q() - .iter(Stores.exchanges) + .iter(Stores.exchanges) .flatMap((e) => [e]) .toArray(); } @@ -1540,17 +1536,17 @@ export class Wallet { .toArray(); } - async getCoins(exchangeBaseUrl: string): Promise { + async getCoins(exchangeBaseUrl: string): Promise { return this.q() - .iter(Stores.coins) - .filter((c: Coin) => c.exchangeBaseUrl === exchangeBaseUrl) + .iter(Stores.coins) + .filter((c: CoinRecord) => c.exchangeBaseUrl === exchangeBaseUrl) .toArray(); } - async getPreCoins(exchangeBaseUrl: string): Promise { + async getPreCoins(exchangeBaseUrl: string): Promise { return this.q() - .iter(Stores.precoins) - .filter((c: PreCoin) => c.exchangeBaseUrl === exchangeBaseUrl) + .iter(Stores.precoins) + .filter((c: PreCoinRecord) => c.exchangeBaseUrl === exchangeBaseUrl) .toArray(); } @@ -1566,7 +1562,7 @@ export class Wallet { console.log("no repurchase: no correlation id"); return {isRepurchase: false}; } - let result: Transaction|undefined = await ( + let result: TransactionRecord|undefined = await ( this.q() .getIndexed(Stores.transactions.repurchaseIndex, [ @@ -1589,16 +1585,16 @@ export class Wallet { async paymentSucceeded(contractHash: string): Promise { const doPaymentSucceeded = async() => { - let t = await this.q().get(Stores.transactions, - contractHash); + let t = await this.q().get(Stores.transactions, + contractHash); if (!t) { console.error("contract not found"); return; } t.finished = true; - let modifiedCoins: Coin[] = []; + let modifiedCoins: CoinRecord[] = []; for (let pc of t.payReq.coins) { - let c = await this.q().get(Stores.coins, pc.coin_pub); + let c = await this.q().get(Stores.coins, pc.coin_pub); if (!c) { console.error("coin not found"); return; diff --git a/src/wxApi.ts b/src/wxApi.ts index a85b56c28..110ba442f 100644 --- a/src/wxApi.ts +++ b/src/wxApi.ts @@ -16,10 +16,10 @@ import { AmountJson, - Coin, - PreCoin, + CoinRecord, + PreCoinRecord, ReserveCreationInfo, - IExchangeInfo, + ExchangeRecord, ReserveRecord } from "./types"; @@ -47,14 +47,14 @@ export function getReserveCreationInfo(baseUrl: string, } export async function callBackend(type: string, detail?: any): Promise { - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { chrome.runtime.sendMessage({ type, detail }, (resp) => { resolve(resp); }); }); } -export async function getExchanges(): Promise { +export async function getExchanges(): Promise { return await callBackend("get-exchanges"); } @@ -62,11 +62,11 @@ export async function getReserves(exchangeBaseUrl: string): Promise { +export async function getCoins(exchangeBaseUrl: string): Promise { return await callBackend("get-coins", { exchangeBaseUrl }); } -export async function getPreCoins(exchangeBaseUrl: string): Promise { +export async function getPreCoins(exchangeBaseUrl: string): Promise { return await callBackend("get-precoins", { exchangeBaseUrl }); } diff --git a/src/wxBackend.ts b/src/wxBackend.ts new file mode 100644 index 000000000..f556d0de4 --- /dev/null +++ b/src/wxBackend.ts @@ -0,0 +1,527 @@ +/* + This file is part of TALER + (C) 2016 GNUnet e.V. + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see + */ + + +import { + Wallet, + OfferRecord, + Badge, + ConfirmReserveRequest, + CreateReserveRequest +} from "./wallet"; +import { BrowserHttpLib } from "./http"; +import { Checkable } from "./checkable"; +import { AmountJson } from "./types"; +import Port = chrome.runtime.Port; +import { Notifier } from "./types"; +import { Contract } from "./types"; +import MessageSender = chrome.runtime.MessageSender; +import { ChromeBadge } from "./chromeBadge"; + +"use strict"; + +const DB_NAME = "taler"; +const DB_VERSION = 11; + +import {Stores} from "./wallet"; +import {Store, Index} from "./query"; + +/** + * Messaging for the WebExtensions wallet. Should contain + * parts that are specific for WebExtensions, but as little business + * logic as possible. + * + * @author Florian Dold + */ + + +type Handler = (detail: any, sender: MessageSender) => Promise; + +function makeHandlers(db: IDBDatabase, + wallet: Wallet): { [msg: string]: Handler } { + return { + ["balances"]: function (detail, sender) { + return wallet.getBalances(); + }, + ["dump-db"]: function (detail, sender) { + return exportDb(db); + }, + ["get-tab-cookie"]: function (detail, sender) { + if (!sender || !sender.tab || !sender.tab.id) { + return Promise.resolve(); + } + let id: number = sender.tab.id; + let info: any = paymentRequestCookies[id]; + delete paymentRequestCookies[id]; + return Promise.resolve(info); + }, + ["ping"]: function (detail, sender) { + return Promise.resolve(); + }, + ["reset"]: function (detail, sender) { + if (db) { + let tx = db.transaction(Array.from(db.objectStoreNames), 'readwrite'); + for (let i = 0; i < db.objectStoreNames.length; i++) { + tx.objectStore(db.objectStoreNames[i]).clear(); + } + } + deleteDb(); + + chrome.browserAction.setBadgeText({ text: "" }); + console.log("reset done"); + // Response is synchronous + return Promise.resolve({}); + }, + ["create-reserve"]: function (detail, sender) { + const d = { + exchange: detail.exchange, + amount: detail.amount, + }; + const req = CreateReserveRequest.checked(d); + return wallet.createReserve(req); + }, + ["confirm-reserve"]: function (detail, sender) { + // TODO: make it a checkable + const d = { + reservePub: detail.reservePub + }; + const req = ConfirmReserveRequest.checked(d); + return wallet.confirmReserve(req); + }, + ["confirm-pay"]: function (detail, sender) { + let offer: OfferRecord; + try { + offer = OfferRecord.checked(detail.offer); + } catch (e) { + if (e instanceof Checkable.SchemaError) { + console.error("schema error:", e.message); + return Promise.resolve({ + error: "invalid contract", + hint: e.message, + detail: detail + }); + } else { + throw e; + } + } + + return wallet.confirmPay(offer); + }, + ["check-pay"]: function (detail, sender) { + let offer: OfferRecord; + try { + offer = OfferRecord.checked(detail.offer); + } catch (e) { + if (e instanceof Checkable.SchemaError) { + console.error("schema error:", e.message); + return Promise.resolve({ + error: "invalid contract", + hint: e.message, + detail: detail + }); + } else { + throw e; + } + } + return wallet.checkPay(offer); + }, + ["execute-payment"]: function (detail: any, sender: MessageSender) { + if (sender.tab && sender.tab.id) { + rateLimitCache[sender.tab.id]++; + if (rateLimitCache[sender.tab.id] > 10) { + console.warn("rate limit for execute payment exceeded"); + let msg = { + error: "rate limit exceeded for execute-payment", + rateLimitExceeded: true, + hint: "Check for redirect loops", + }; + return Promise.resolve(msg); + } + } + return wallet.executePayment(detail.H_contract); + }, + ["exchange-info"]: function (detail) { + if (!detail.baseUrl) { + return Promise.resolve({ error: "bad url" }); + } + 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); + }, + ["save-offer"]: function (detail: any) { + let offer = detail.offer; + if (!offer) { + return Promise.resolve({ error: "offer missing" }); + } + console.log("handling safe-offer"); + return wallet.saveOffer(offer); + }, + ["reserve-creation-info"]: function (detail, sender) { + if (!detail.baseUrl || typeof detail.baseUrl !== "string") { + return Promise.resolve({ error: "bad url" }); + } + let amount = AmountJson.checked(detail.amount); + return wallet.getReserveCreationInfo(detail.baseUrl, amount); + }, + ["check-repurchase"]: function (detail, sender) { + let contract = Contract.checked(detail.contract); + return wallet.checkRepurchase(contract); + }, + ["get-history"]: function (detail, sender) { + // TODO: limit history length + return wallet.getHistory(); + }, + ["get-offer"]: function (detail, sender) { + return wallet.getOffer(detail.offerId); + }, + ["get-exchanges"]: function (detail, sender) { + return wallet.getExchanges(); + }, + ["get-reserves"]: function (detail, sender) { + if (typeof detail.exchangeBaseUrl !== "string") { + return Promise.reject(Error("exchangeBaseUrl missing")); + } + return wallet.getReserves(detail.exchangeBaseUrl); + }, + ["get-coins"]: function (detail, sender) { + if (typeof detail.exchangeBaseUrl !== "string") { + return Promise.reject(Error("exchangBaseUrl missing")); + } + return wallet.getCoins(detail.exchangeBaseUrl); + }, + ["get-precoins"]: function (detail, sender) { + if (typeof detail.exchangeBaseUrl !== "string") { + return Promise.reject(Error("exchangBaseUrl missing")); + } + return wallet.getPreCoins(detail.exchangeBaseUrl); + }, + ["refresh-coin"]: function (detail, sender) { + if (typeof detail.coinPub !== "string") { + return Promise.reject(Error("coinPub missing")); + } + return wallet.refresh(detail.coinPub); + }, + ["payment-failed"]: function (detail, sender) { + // For now we just update exchanges (maybe the exchange did something + // wrong and the keys were messed up). + // FIXME: in the future we should look at what actually went wrong. + console.error("payment reported as failed"); + wallet.updateExchanges(); + return Promise.resolve(); + }, + ["payment-succeeded"]: function (detail, sender) { + let contractHash = detail.contractHash; + if (!contractHash) { + return Promise.reject(Error("contractHash missing")); + } + return wallet.paymentSucceeded(contractHash); + }, + }; +} + + +function dispatch(handlers: any, req: any, sender: any, sendResponse: any) { + if (req.type in handlers) { + Promise + .resolve() + .then(() => { + const p = handlers[req.type](req.detail, sender); + + return p.then((r: any) => { + try { + sendResponse(r); + } catch (e) { + // might fail if tab disconnected + } + }) + }) + .catch((e) => { + console.log(`exception during wallet handler for '${req.type}'`); + console.log("request", req); + console.error(e); + try { + sendResponse({ + error: "exception", + hint: e.message, + stack: e.stack.toString() + }); + + } catch (e) { + // might fail if tab disconnected + } + }); + // The sendResponse call is async + return true; + } else { + console.error(`Request type ${JSON.stringify(req)} unknown, req ${req.type}`); + try { + sendResponse({ error: "request unknown" }); + } catch (e) { + // might fail if tab disconnected + } + + // The sendResponse call is sync + return false; + } +} + +class ChromeNotifier implements Notifier { + ports: Port[] = []; + + constructor() { + chrome.runtime.onConnect.addListener((port) => { + console.log("got connect!"); + this.ports.push(port); + port.onDisconnect.addListener(() => { + let i = this.ports.indexOf(port); + if (i >= 0) { + this.ports.splice(i, 1); + } else { + console.error("port already removed"); + } + }); + }); + } + + notify() { + for (let p of this.ports) { + p.postMessage({ notify: true }); + } + } +} + + +/** + * Mapping from tab ID to payment information (if any). + */ +let paymentRequestCookies: { [n: number]: any } = {}; + +function handleHttpPayment(headerList: chrome.webRequest.HttpHeader[], + url: string, tabId: number): any { + const headers: { [s: string]: string } = {}; + for (let kv of headerList) { + if (kv.value) { + headers[kv.name.toLowerCase()] = kv.value; + } + } + + const contractUrl = headers["x-taler-contract-url"]; + if (contractUrl !== undefined) { + paymentRequestCookies[tabId] = { type: "fetch", contractUrl }; + return; + } + + const contractHash = headers["x-taler-contract-hash"]; + + if (contractHash !== undefined) { + const payUrl = headers["x-taler-pay-url"]; + if (payUrl === undefined) { + console.log("malformed 402, X-Taler-Pay-Url missing"); + return; + } + + // Offer URL is optional + const offerUrl = headers["x-taler-offer-url"]; + paymentRequestCookies[tabId] = { + type: "execute", + offerUrl, + payUrl, + contractHash + }; + return; + } + + // looks like it's not a taler request, it might be + // for a different payment system (or the shop is buggy) + console.log("ignoring non-taler 402 response"); +} + +// Useful for debugging ... +export let wallet: Wallet | undefined = undefined; +export let badge: ChromeBadge | undefined = undefined; + +// Rate limit cache for executePayment operations, to break redirect loops +let rateLimitCache: { [n: number]: number } = {}; + +function clearRateLimitCache() { + rateLimitCache = {}; +} + +export function wxMain() { + chrome.browserAction.setBadgeText({ text: "" }); + badge = new ChromeBadge(); + + chrome.tabs.query({}, function (tabs) { + for (let tab of tabs) { + if (!tab.url || !tab.id) { + return; + } + let uri = URI(tab.url); + if (uri.protocol() == "http" || uri.protocol() == "https") { + console.log("injecting into existing tab", tab.id); + chrome.tabs.executeScript(tab.id, { file: "/src/vendor/URI.js" }); + chrome.tabs.executeScript(tab.id, { file: "/src/taler-wallet-lib.js" }); + chrome.tabs.executeScript(tab.id, { file: "/src/content_scripts/notify.js" }); + } + } + }); + + chrome.extension.getBackgroundPage().setInterval(clearRateLimitCache, 5000); + + Promise.resolve() + .then(() => { + return openTalerDb(); + }) + .catch((e) => { + console.error("could not open database"); + console.error(e); + }) + .then((db: IDBDatabase) => { + let http = new BrowserHttpLib(); + let notifier = new ChromeNotifier(); + console.log("setting wallet"); + wallet = new Wallet(db, http, badge!, notifier); + + // Handlers for messages coming directly from the content + // script on the page + let handlers = makeHandlers(db, wallet!); + chrome.runtime.onMessage.addListener((req, sender, sendResponse) => { + try { + return dispatch(handlers, req, sender, sendResponse) + } catch (e) { + console.log(`exception during wallet handler (dispatch)`); + console.log("request", req); + console.error(e); + sendResponse({ + error: "exception", + hint: e.message, + stack: e.stack.toString() + }); + return false; + } + }); + + // Handlers for catching HTTP requests + chrome.webRequest.onHeadersReceived.addListener((details) => { + if (details.statusCode != 402) { + return; + } + console.log(`got 402 from ${details.url}`); + return handleHttpPayment(details.responseHeaders || [], + details.url, + details.tabId); + }, { urls: [""] }, ["responseHeaders", "blocking"]); + }) + .catch((e) => { + console.error("could not initialize wallet messaging"); + console.error(e); + }); +} + + + +/** + * Return a promise that resolves + * to the taler wallet db. + */ +function openTalerDb(): Promise { + return new Promise((resolve, reject) => { + const req = indexedDB.open(DB_NAME, DB_VERSION); + req.onerror = (e) => { + reject(e); + }; + req.onsuccess = (e) => { + resolve(req.result); + }; + req.onupgradeneeded = (e) => { + const db = req.result; + console.log("DB: upgrade needed: oldVersion = " + e.oldVersion); + switch (e.oldVersion) { + case 0: // DB does not exist yet + + for (let n in Stores) { + if ((Stores as any)[n] instanceof Store) { + let si: Store = (Stores as any)[n]; + const s = db.createObjectStore(si.name, si.storeParams); + for (let indexName in (si as any)) { + if ((si as any)[indexName] instanceof Index) { + let ii: Index = (si as any)[indexName]; + s.createIndex(ii.indexName, ii.keyPath); + } + } + } + } + break; + default: + if (e.oldVersion != DB_VERSION) { + window.alert("Incompatible wallet dababase version, please reset" + + " db."); + chrome.browserAction.setBadgeText({text: "err"}); + chrome.browserAction.setBadgeBackgroundColor({color: "#F00"}); + throw Error("incompatible DB"); + } + break; + } + }; + }); +} + + +function exportDb(db: IDBDatabase): Promise { + let dump = { + name: db.name, + version: db.version, + stores: {} as {[s: string]: any}, + }; + + return new Promise((resolve, reject) => { + + let tx = db.transaction(Array.from(db.objectStoreNames)); + tx.addEventListener("complete", () => { + resolve(dump); + }); + for (let i = 0; i < db.objectStoreNames.length; i++) { + let name = db.objectStoreNames[i]; + let storeDump = {} as {[s: string]: any}; + dump.stores[name] = storeDump; + let store = tx.objectStore(name) + .openCursor() + .addEventListener("success", (e: Event) => { + let cursor = (e.target as any).result; + if (cursor) { + storeDump[cursor.key] = cursor.value; + cursor.continue(); + } + }); + } + }); +} + +function deleteDb() { + indexedDB.deleteDatabase(DB_NAME); +} diff --git a/src/wxMessaging.ts b/src/wxMessaging.ts deleted file mode 100644 index 990f1488b..000000000 --- a/src/wxMessaging.ts +++ /dev/null @@ -1,439 +0,0 @@ -/* - This file is part of TALER - (C) 2016 GNUnet e.V. - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see - */ - - -import { - Wallet, - Offer, - Badge, - ConfirmReserveRequest, - CreateReserveRequest -} from "./wallet"; -import { deleteDb, exportDb, openTalerDb } from "./db"; -import { BrowserHttpLib } from "./http"; -import { Checkable } from "./checkable"; -import { AmountJson } from "./types"; -import Port = chrome.runtime.Port; -import { Notifier } from "./types"; -import { Contract } from "./types"; -import MessageSender = chrome.runtime.MessageSender; -import { ChromeBadge } from "./chromeBadge"; - -"use strict"; - -/** - * Messaging for the WebExtensions wallet. Should contain - * parts that are specific for WebExtensions, but as little business - * logic as possible. - * - * @author Florian Dold - */ - - -type Handler = (detail: any, sender: MessageSender) => Promise; - -function makeHandlers(db: IDBDatabase, - wallet: Wallet): { [msg: string]: Handler } { - return { - ["balances"]: function (detail, sender) { - return wallet.getBalances(); - }, - ["dump-db"]: function (detail, sender) { - return exportDb(db); - }, - ["get-tab-cookie"]: function (detail, sender) { - if (!sender || !sender.tab || !sender.tab.id) { - return Promise.resolve(); - } - let id: number = sender.tab.id; - let info: any = paymentRequestCookies[id]; - delete paymentRequestCookies[id]; - return Promise.resolve(info); - }, - ["ping"]: function (detail, sender) { - return Promise.resolve(); - }, - ["reset"]: function (detail, sender) { - if (db) { - let tx = db.transaction(Array.from(db.objectStoreNames), 'readwrite'); - for (let i = 0; i < db.objectStoreNames.length; i++) { - tx.objectStore(db.objectStoreNames[i]).clear(); - } - } - deleteDb(); - - chrome.browserAction.setBadgeText({ text: "" }); - console.log("reset done"); - // Response is synchronous - return Promise.resolve({}); - }, - ["create-reserve"]: function (detail, sender) { - const d = { - exchange: detail.exchange, - amount: detail.amount, - }; - const req = CreateReserveRequest.checked(d); - return wallet.createReserve(req); - }, - ["confirm-reserve"]: function (detail, sender) { - // TODO: make it a checkable - const d = { - reservePub: detail.reservePub - }; - const req = ConfirmReserveRequest.checked(d); - return wallet.confirmReserve(req); - }, - ["confirm-pay"]: function (detail, sender) { - let offer: Offer; - try { - offer = Offer.checked(detail.offer); - } catch (e) { - if (e instanceof Checkable.SchemaError) { - console.error("schema error:", e.message); - return Promise.resolve({ - error: "invalid contract", - hint: e.message, - detail: detail - }); - } else { - throw e; - } - } - - return wallet.confirmPay(offer); - }, - ["check-pay"]: function (detail, sender) { - let offer: Offer; - try { - offer = Offer.checked(detail.offer); - } catch (e) { - if (e instanceof Checkable.SchemaError) { - console.error("schema error:", e.message); - return Promise.resolve({ - error: "invalid contract", - hint: e.message, - detail: detail - }); - } else { - throw e; - } - } - return wallet.checkPay(offer); - }, - ["execute-payment"]: function (detail: any, sender: MessageSender) { - if (sender.tab && sender.tab.id) { - rateLimitCache[sender.tab.id]++; - if (rateLimitCache[sender.tab.id] > 10) { - console.warn("rate limit for execute payment exceeded"); - let msg = { - error: "rate limit exceeded for execute-payment", - rateLimitExceeded: true, - hint: "Check for redirect loops", - }; - return Promise.resolve(msg); - } - } - return wallet.executePayment(detail.H_contract); - }, - ["exchange-info"]: function (detail) { - if (!detail.baseUrl) { - return Promise.resolve({ error: "bad url" }); - } - 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); - }, - ["save-offer"]: function (detail: any) { - let offer = detail.offer; - if (!offer) { - return Promise.resolve({ error: "offer missing" }); - } - console.log("handling safe-offer"); - return wallet.saveOffer(offer); - }, - ["reserve-creation-info"]: function (detail, sender) { - if (!detail.baseUrl || typeof detail.baseUrl !== "string") { - return Promise.resolve({ error: "bad url" }); - } - let amount = AmountJson.checked(detail.amount); - return wallet.getReserveCreationInfo(detail.baseUrl, amount); - }, - ["check-repurchase"]: function (detail, sender) { - let contract = Contract.checked(detail.contract); - return wallet.checkRepurchase(contract); - }, - ["get-history"]: function (detail, sender) { - // TODO: limit history length - return wallet.getHistory(); - }, - ["get-offer"]: function (detail, sender) { - return wallet.getOffer(detail.offerId); - }, - ["get-exchanges"]: function (detail, sender) { - return wallet.getExchanges(); - }, - ["get-reserves"]: function (detail, sender) { - if (typeof detail.exchangeBaseUrl !== "string") { - return Promise.reject(Error("exchangeBaseUrl missing")); - } - return wallet.getReserves(detail.exchangeBaseUrl); - }, - ["get-coins"]: function (detail, sender) { - if (typeof detail.exchangeBaseUrl !== "string") { - return Promise.reject(Error("exchangBaseUrl missing")); - } - return wallet.getCoins(detail.exchangeBaseUrl); - }, - ["get-precoins"]: function (detail, sender) { - if (typeof detail.exchangeBaseUrl !== "string") { - return Promise.reject(Error("exchangBaseUrl missing")); - } - return wallet.getPreCoins(detail.exchangeBaseUrl); - }, - ["refresh-coin"]: function (detail, sender) { - if (typeof detail.coinPub !== "string") { - return Promise.reject(Error("coinPub missing")); - } - return wallet.refresh(detail.coinPub); - }, - ["payment-failed"]: function (detail, sender) { - // For now we just update exchanges (maybe the exchange did something - // wrong and the keys were messed up). - // FIXME: in the future we should look at what actually went wrong. - console.error("payment reported as failed"); - wallet.updateExchanges(); - return Promise.resolve(); - }, - ["payment-succeeded"]: function (detail, sender) { - let contractHash = detail.contractHash; - if (!contractHash) { - return Promise.reject(Error("contractHash missing")); - } - return wallet.paymentSucceeded(contractHash); - }, - }; -} - - -function dispatch(handlers: any, req: any, sender: any, sendResponse: any) { - if (req.type in handlers) { - Promise - .resolve() - .then(() => { - const p = handlers[req.type](req.detail, sender); - - return p.then((r: any) => { - try { - sendResponse(r); - } catch (e) { - // might fail if tab disconnected - } - }) - }) - .catch((e) => { - console.log(`exception during wallet handler for '${req.type}'`); - console.log("request", req); - console.error(e); - try { - sendResponse({ - error: "exception", - hint: e.message, - stack: e.stack.toString() - }); - - } catch (e) { - // might fail if tab disconnected - } - }); - // The sendResponse call is async - return true; - } else { - console.error(`Request type ${JSON.stringify(req)} unknown, req ${req.type}`); - try { - sendResponse({ error: "request unknown" }); - } catch (e) { - // might fail if tab disconnected - } - - // The sendResponse call is sync - return false; - } -} - -class ChromeNotifier implements Notifier { - ports: Port[] = []; - - constructor() { - chrome.runtime.onConnect.addListener((port) => { - console.log("got connect!"); - this.ports.push(port); - port.onDisconnect.addListener(() => { - let i = this.ports.indexOf(port); - if (i >= 0) { - this.ports.splice(i, 1); - } else { - console.error("port already removed"); - } - }); - }); - } - - notify() { - for (let p of this.ports) { - p.postMessage({ notify: true }); - } - } -} - - -/** - * Mapping from tab ID to payment information (if any). - */ -let paymentRequestCookies: { [n: number]: any } = {}; - -function handleHttpPayment(headerList: chrome.webRequest.HttpHeader[], - url: string, tabId: number): any { - const headers: { [s: string]: string } = {}; - for (let kv of headerList) { - if (kv.value) { - headers[kv.name.toLowerCase()] = kv.value; - } - } - - const contractUrl = headers["x-taler-contract-url"]; - if (contractUrl !== undefined) { - paymentRequestCookies[tabId] = { type: "fetch", contractUrl }; - return; - } - - const contractHash = headers["x-taler-contract-hash"]; - - if (contractHash !== undefined) { - const payUrl = headers["x-taler-pay-url"]; - if (payUrl === undefined) { - console.log("malformed 402, X-Taler-Pay-Url missing"); - return; - } - - // Offer URL is optional - const offerUrl = headers["x-taler-offer-url"]; - paymentRequestCookies[tabId] = { - type: "execute", - offerUrl, - payUrl, - contractHash - }; - return; - } - - // looks like it's not a taler request, it might be - // for a different payment system (or the shop is buggy) - console.log("ignoring non-taler 402 response"); -} - -// Useful for debugging ... -export let wallet: Wallet | undefined = undefined; -export let badge: ChromeBadge | undefined = undefined; - -// Rate limit cache for executePayment operations, to break redirect loops -let rateLimitCache: { [n: number]: number } = {}; - -function clearRateLimitCache() { - rateLimitCache = {}; -} - -export function wxMain() { - chrome.browserAction.setBadgeText({ text: "" }); - badge = new ChromeBadge(); - - chrome.tabs.query({}, function (tabs) { - for (let tab of tabs) { - if (!tab.url || !tab.id) { - return; - } - let uri = URI(tab.url); - if (uri.protocol() == "http" || uri.protocol() == "https") { - console.log("injecting into existing tab", tab.id); - chrome.tabs.executeScript(tab.id, { file: "/src/vendor/URI.js" }); - chrome.tabs.executeScript(tab.id, { file: "/src/taler-wallet-lib.js" }); - chrome.tabs.executeScript(tab.id, { file: "/src/content_scripts/notify.js" }); - } - } - }); - - chrome.extension.getBackgroundPage().setInterval(clearRateLimitCache, 5000); - - Promise.resolve() - .then(() => { - return openTalerDb(); - }) - .catch((e) => { - console.error("could not open database"); - console.error(e); - }) - .then((db: IDBDatabase) => { - let http = new BrowserHttpLib(); - let notifier = new ChromeNotifier(); - console.log("setting wallet"); - wallet = new Wallet(db, http, badge!, notifier); - - // Handlers for messages coming directly from the content - // script on the page - let handlers = makeHandlers(db, wallet!); - chrome.runtime.onMessage.addListener((req, sender, sendResponse) => { - try { - return dispatch(handlers, req, sender, sendResponse) - } catch (e) { - console.log(`exception during wallet handler (dispatch)`); - console.log("request", req); - console.error(e); - sendResponse({ - error: "exception", - hint: e.message, - stack: e.stack.toString() - }); - return false; - } - }); - - // Handlers for catching HTTP requests - chrome.webRequest.onHeadersReceived.addListener((details) => { - if (details.statusCode != 402) { - return; - } - console.log(`got 402 from ${details.url}`); - return handleHttpPayment(details.responseHeaders || [], - details.url, - details.tabId); - }, { urls: [""] }, ["responseHeaders", "blocking"]); - }) - .catch((e) => { - console.error("could not initialize wallet messaging"); - console.error(e); - }); -} diff --git a/tsconfig.json b/tsconfig.json index 2002012d0..502512ae0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,20 +16,19 @@ "src/checkable.ts", "decl/lib.es6.d.ts", "src/chromeBadge.ts", - "decl/urijs/URIjs.d.ts", "src/cryptoApi-test.ts", + "decl/urijs/URIjs.d.ts", "src/components.ts", - "decl/systemjs/systemjs.d.ts", "src/emscriptif-test.ts", + "decl/systemjs/systemjs.d.ts", "src/cryptoApi.ts", - "decl/react-global.d.ts", "src/helpers-test.ts", + "decl/react-global.d.ts", "src/cryptoLib.ts", "src/types-test.ts", "decl/chrome/chrome.d.ts", "src/cryptoWorker.ts", "src/wallet-test.ts", - "src/db.ts", "src/emscriptif.ts", "src/helpers.ts", "src/http.ts", @@ -39,7 +38,7 @@ "src/types.ts", "src/wallet.ts", "src/wxApi.ts", - "src/wxMessaging.ts", + "src/wxBackend.ts", "src/renderHtml.tsx", "src/background/background.ts", "src/content_scripts/notify.ts", -- cgit v1.2.3