From d4be3906e32ac7d9933c6030d6493f2f2152bdd9 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Wed, 12 Oct 2016 02:55:53 +0200 Subject: tree view of wallet db --- lib/components.ts | 45 +++++++++ lib/wallet/query.ts | 58 +++++++---- lib/wallet/renderHtml.tsx | 13 +++ lib/wallet/types.ts | 2 + lib/wallet/wallet.ts | 253 ++++++++++++++++++++++++++-------------------- lib/wallet/wxApi.ts | 38 ++++++- lib/wallet/wxMessaging.ts | 231 +++++++++++++++++++++++------------------- 7 files changed, 399 insertions(+), 241 deletions(-) create mode 100644 lib/components.ts (limited to 'lib') diff --git a/lib/components.ts b/lib/components.ts new file mode 100644 index 000000000..fb802ab47 --- /dev/null +++ b/lib/components.ts @@ -0,0 +1,45 @@ +/* + This file is part of TALER + (C) 2016 Inria + + 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 + */ + + +/** + * General helper components + * + * @author Florian Dold + */ + +export interface StateHolder { + (): T; + (newState: T): void; +} + +/** + * Component that doesn't hold its state in one object, + * but has multiple state holders. + */ +export abstract class ImplicitStateComponent extends preact.Component { + makeState(initial: StateType): StateHolder { + let state: StateType = initial; + return (s?: StateType): StateType => { + if (s !== undefined) { + state = s; + // In preact, this will always schedule a (debounced) redraw + this.setState({} as any); + } + return state; + }; + } +} \ No newline at end of file diff --git a/lib/wallet/query.ts b/lib/wallet/query.ts index 3119aa58f..77a4f8e35 100644 --- a/lib/wallet/query.ts +++ b/lib/wallet/query.ts @@ -34,11 +34,12 @@ export function Query(db: IDBDatabase) { */ export interface QueryStream { indexJoin(storeName: string, - indexName: string, - keyFn: (obj: any) => any): QueryStream<[T,S]>; + indexName: string, + keyFn: (obj: any) => any): QueryStream<[T, S]>; filter(f: (x: any) => boolean): QueryStream; reduce(f: (v: T, acc: S) => S, start?: S): Promise; flatMap(f: (x: T) => T[]): QueryStream; + toArray(): Promise; } @@ -57,14 +58,14 @@ function openPromise() { // Never happens, unless JS implementation is broken throw Error(); } - return {resolve, reject, promise}; + return { resolve, reject, promise }; } abstract class QueryStreamBase implements QueryStream { abstract subscribe(f: (isDone: boolean, - value: any, - tx: IDBTransaction) => void): void; + value: any, + tx: IDBTransaction) => void): void; root: QueryRoot; @@ -77,8 +78,8 @@ abstract class QueryStreamBase implements QueryStream { } indexJoin(storeName: string, - indexName: string, - key: any): QueryStream<[T,S]> { + indexName: string, + key: any): QueryStream<[T, S]> { this.root.addStoreAccess(storeName, false); return new QueryStreamIndexJoin(this, storeName, indexName, key); } @@ -87,6 +88,23 @@ abstract class QueryStreamBase implements QueryStream { return new QueryStreamFilter(this, f); } + toArray(): Promise { + let {resolve, promise} = openPromise(); + let values: T[] = []; + + this.subscribe((isDone, value) => { + if (isDone) { + resolve(values); + return; + } + values.push(value); + }); + + return Promise.resolve() + .then(() => this.root.finish()) + .then(() => promise); + } + reduce(f: (x: any, acc?: A) => A, init?: A): Promise { let {resolve, promise} = openPromise(); let acc = init; @@ -100,8 +118,8 @@ abstract class QueryStreamBase implements QueryStream { }); return Promise.resolve() - .then(() => this.root.finish()) - .then(() => promise); + .then(() => this.root.finish()) + .then(() => promise); } } @@ -161,7 +179,7 @@ class QueryStreamFlatMap extends QueryStreamBase { } -class QueryStreamIndexJoin extends QueryStreamBase<[T, S]> { +class QueryStreamIndexJoin extends QueryStreamBase<[T, S]> { s: QueryStreamBase; storeName: string; key: any; @@ -214,11 +232,11 @@ class IterQueryStream extends QueryStreamBase { let s: any; if (indexName !== void 0) { s = tx.objectStore(this.storeName) - .index(this.options.indexName); + .index(this.options.indexName); } else { s = tx.objectStore(this.storeName); } - let kr: IDBKeyRange|undefined = undefined; + let kr: IDBKeyRange | undefined = undefined; if (only !== undefined) { kr = IDBKeyRange.only(this.options.only); } @@ -264,9 +282,9 @@ class QueryRoot { } iter(storeName: string, - {only = undefined, indexName = undefined} = {}): QueryStream { + {only = undefined, indexName = undefined} = {}): QueryStream { this.stores.add(storeName); - return new IterQueryStream(this, storeName, {only, indexName}); + return new IterQueryStream(this, storeName, { only, indexName }); } /** @@ -330,8 +348,8 @@ class QueryRoot { this.addWork(doGet, storeName, false); return Promise.resolve() - .then(() => this.finish()) - .then(() => promise); + .then(() => this.finish()) + .then(() => promise); } /** @@ -353,8 +371,8 @@ class QueryRoot { this.addWork(doGetIndexed, storeName, false); return Promise.resolve() - .then(() => this.finish()) - .then(() => promise); + .then(() => this.finish()) + .then(() => promise); } /** @@ -396,8 +414,8 @@ class QueryRoot { * Low-level function to add a task to the internal work queue. */ addWork(workFn: (t: IDBTransaction) => void, - storeName?: string, - isWrite?: boolean) { + storeName?: string, + isWrite?: boolean) { this.work.push(workFn); if (storeName) { this.addStoreAccess(storeName, isWrite); diff --git a/lib/wallet/renderHtml.tsx b/lib/wallet/renderHtml.tsx index db0f00ec5..52c01cd09 100644 --- a/lib/wallet/renderHtml.tsx +++ b/lib/wallet/renderHtml.tsx @@ -47,4 +47,17 @@ export function renderContract(contract: Contract): JSX.Element { ); +} + + +export function abbrev(s: string, n: number = 5) { + let sAbbrev = s; + if (s.length > n) { + sAbbrev = s.slice(0, n) + ".."; + } + return ( + + {sAbbrev} + + ); } \ No newline at end of file diff --git a/lib/wallet/types.ts b/lib/wallet/types.ts index e8b7a1e39..5e139a9bc 100644 --- a/lib/wallet/types.ts +++ b/lib/wallet/types.ts @@ -152,6 +152,8 @@ export interface Reserve { exchange_base_url: string reserve_priv: string; reserve_pub: string; + created: number; + current_amount: AmountJson; } diff --git a/lib/wallet/wallet.ts b/lib/wallet/wallet.ts index 67288f666..337ed8255 100644 --- a/lib/wallet/wallet.ts +++ b/lib/wallet/wallet.ts @@ -29,19 +29,19 @@ import { Notifier, WireInfo } from "./types"; -import {HttpResponse, RequestException} from "./http"; -import {Query} from "./query"; -import {Checkable} from "./checkable"; -import {canonicalizeBaseUrl} from "./helpers"; -import {ReserveCreationInfo, Amounts} from "./types"; -import {PreCoin} from "./types"; -import {Reserve} from "./types"; -import {CryptoApi} from "./cryptoApi"; -import {Coin} from "./types"; -import {PayCoinInfo} from "./types"; -import {CheckRepurchaseResult} from "./types"; -import {Contract} from "./types"; -import {ExchangeHandle} from "./types"; +import { HttpResponse, RequestException } from "./http"; +import { Query } from "./query"; +import { Checkable } from "./checkable"; +import { canonicalizeBaseUrl } from "./helpers"; +import { ReserveCreationInfo, Amounts } from "./types"; +import { PreCoin } from "./types"; +import { Reserve } from "./types"; +import { CryptoApi } from "./cryptoApi"; +import { Coin } from "./types"; +import { PayCoinInfo } from "./types"; +import { CheckRepurchaseResult } from "./types"; +import { Contract } from "./types"; +import { ExchangeHandle } from "./types"; "use strict"; @@ -55,11 +55,11 @@ interface ReserveRecord { reserve_priv: string, exchange_base_url: string, created: number, - last_query: number|null, + last_query: number | null, /** * Current amount left in the reserve */ - current_amount: AmountJson|null, + current_amount: AmountJson | null, /** * Amount requested when the reserve was created. * When a reserve is re-used (rare!) the current_amount can @@ -229,7 +229,7 @@ function flatMap(xs: T[], f: (x: T) => U[]): U[] { } -function getTalerStampSec(stamp: string): number|null { +function getTalerStampSec(stamp: string): number | null { const m = stamp.match(/\/?Date\(([0-9]*)\)\/?/); if (!m) { return null; @@ -256,14 +256,14 @@ function isWithdrawableDenom(d: Denomination) { interface HttpRequestLibrary { req(method: string, - url: string|uri.URI, - options?: any): Promise; + url: string | uri.URI, + options?: any): Promise; - get(url: string|uri.URI): Promise; + get(url: string | uri.URI): Promise; - postJson(url: string|uri.URI, body: any): Promise; + postJson(url: string | uri.URI, body: any): Promise; - postForm(url: string|uri.URI, form: any): Promise; + postForm(url: string | uri.URI, form: any): Promise; } @@ -288,7 +288,7 @@ interface KeyUpdateInfo { * amount, but never larger. */ function getWithdrawDenomList(amountAvailable: AmountJson, - denoms: Denomination[]): Denomination[] { + denoms: Denomination[]): Denomination[] { let remaining = Amounts.copy(amountAvailable); const ds: Denomination[] = []; @@ -331,9 +331,9 @@ export class Wallet { private runningOperations: Set = new Set(); constructor(db: IDBDatabase, - http: HttpRequestLibrary, - badge: Badge, - notifier: Notifier) { + http: HttpRequestLibrary, + badge: Badge, + notifier: Notifier) { this.db = db; this.http = http; this.badge = badge; @@ -363,9 +363,9 @@ export class Wallet { .iter("exchanges") .reduce((exchange: IExchangeInfo) => { this.updateExchangeFromUrl(exchange.baseUrl) - .catch((e) => { - console.error("updating exchange failed", e); - }); + .catch((e) => { + console.error("updating exchange failed", e); + }); }); } @@ -397,8 +397,8 @@ export class Wallet { * but only if the sum the coins' remaining value exceeds the payment amount. */ private async getPossibleExchangeCoins(paymentAmount: AmountJson, - depositFeeLimit: AmountJson, - allowedExchanges: ExchangeHandle[]): Promise { + depositFeeLimit: AmountJson, + allowedExchanges: ExchangeHandle[]): Promise { // Mapping from exchange base URL to list of coins together with their // denomination let m: ExchangeCoins = {}; @@ -411,9 +411,9 @@ export class Wallet { let coin: Coin = mc[1]; if (coin.suspended) { console.log("skipping suspended coin", - coin.denomPub, - "from exchange", - exchange.baseUrl); + coin.denomPub, + "from exchange", + exchange.baseUrl); return; } let denom = exchange.active_denoms.find((e) => e.denom_pub === coin.denomPub); @@ -425,7 +425,7 @@ export class Wallet { console.warn("same pubkey for different currencies"); return; } - let cd = {coin, denom}; + let cd = { coin, denom }; let x = m[url]; if (!x) { m[url] = [cd]; @@ -446,7 +446,7 @@ export class Wallet { console.log("Checking for merchant's exchange", JSON.stringify(info)); return [ Query(this.db) - .iter("exchanges", {indexName: "pubKey", only: info.master_pub}) + .iter("exchanges", { indexName: "pubKey", only: info.master_pub }) .indexJoin("coins", "exchangeBaseUrl", (exchange) => exchange.baseUrl) .reduce((x) => storeExchangeCoin(x, info.url)) ]; @@ -467,38 +467,38 @@ export class Wallet { // under depositFeeLimit nextExchange: - for (let key in m) { - let coins = m[key]; - // Sort by ascending deposit fee - coins.sort((o1, o2) => Amounts.cmp(o1.denom.fee_deposit, - o2.denom.fee_deposit)); - let maxFee = Amounts.copy(depositFeeLimit); - let minAmount = Amounts.copy(paymentAmount); - let accFee = Amounts.copy(coins[0].denom.fee_deposit); - let accAmount = Amounts.getZero(coins[0].coin.currentAmount.currency); - let usableCoins: CoinWithDenom[] = []; - nextCoin: - for (let i = 0; i < coins.length; i++) { - let coinAmount = Amounts.copy(coins[i].coin.currentAmount); - let coinFee = coins[i].denom.fee_deposit; - if (Amounts.cmp(coinAmount, coinFee) <= 0) { - continue nextCoin; - } - accFee = Amounts.add(accFee, coinFee).amount; - accAmount = Amounts.add(accAmount, coinAmount).amount; - if (Amounts.cmp(accFee, maxFee) >= 0) { - // FIXME: if the fees are too high, we have - // to cover them ourselves .... - console.log("too much fees"); - continue nextExchange; - } - usableCoins.push(coins[i]); - if (Amounts.cmp(accAmount, minAmount) >= 0) { - ret[key] = usableCoins; - continue nextExchange; - } - } + for (let key in m) { + let coins = m[key]; + // Sort by ascending deposit fee + coins.sort((o1, o2) => Amounts.cmp(o1.denom.fee_deposit, + o2.denom.fee_deposit)); + let maxFee = Amounts.copy(depositFeeLimit); + let minAmount = Amounts.copy(paymentAmount); + let accFee = Amounts.copy(coins[0].denom.fee_deposit); + let accAmount = Amounts.getZero(coins[0].coin.currentAmount.currency); + let usableCoins: CoinWithDenom[] = []; + nextCoin: + for (let i = 0; i < coins.length; i++) { + let coinAmount = Amounts.copy(coins[i].coin.currentAmount); + let coinFee = coins[i].denom.fee_deposit; + if (Amounts.cmp(coinAmount, coinFee) <= 0) { + continue nextCoin; + } + accFee = Amounts.add(accFee, coinFee).amount; + accAmount = Amounts.add(accAmount, coinAmount).amount; + if (Amounts.cmp(accFee, maxFee) >= 0) { + // FIXME: if the fees are too high, we have + // to cover them ourselves .... + console.log("too much fees"); + continue nextExchange; + } + usableCoins.push(coins[i]); + if (Amounts.cmp(accAmount, minAmount) >= 0) { + ret[key] = usableCoins; + continue nextExchange; + } } + } return ret; } @@ -508,8 +508,8 @@ export class Wallet { * pay for a contract in the wallet's database. */ private async recordConfirmPay(offer: Offer, - payCoinInfo: PayCoinInfo, - chosenExchange: string): Promise { + payCoinInfo: PayCoinInfo, + chosenExchange: string): Promise { let payReq: any = {}; payReq["amount"] = offer.contract.amount; payReq["coins"] = payCoinInfo.map((x) => x.sig); @@ -571,8 +571,8 @@ export class Wallet { } let mcs = await this.getPossibleExchangeCoins(offer.contract.amount, - offer.contract.max_fee, - offer.contract.exchanges); + offer.contract.max_fee, + offer.contract.exchanges); if (Object.keys(mcs).length == 0) { console.log("not confirming payment, insufficient coins"); @@ -584,8 +584,8 @@ export class Wallet { let ds = await this.cryptoApi.signDeposit(offer, mcs[exchangeUrl]); await this.recordConfirmPay(offer, - ds, - exchangeUrl); + ds, + exchangeUrl); return {}; } @@ -600,13 +600,13 @@ export class Wallet { Query(this.db) .get("transactions", offer.H_contract); if (transaction) { - return {isPayed: true}; + return { isPayed: true }; } // If not already payed, check if we could pay for it. let mcs = await this.getPossibleExchangeCoins(offer.contract.amount, - offer.contract.max_fee, - offer.contract.exchanges); + offer.contract.max_fee, + offer.contract.exchanges); if (Object.keys(mcs).length == 0) { console.log("not confirming payment, insufficient coins"); @@ -614,7 +614,7 @@ export class Wallet { error: "coins-insufficient", }; } - return {isPayed: false}; + return { isPayed: false }; } @@ -645,14 +645,14 @@ export class Wallet { * then deplete the reserve, withdrawing coins until it is empty. */ private async processReserve(reserveRecord: ReserveRecord, - retryDelayMs: number = 250): Promise { + retryDelayMs: number = 250): Promise { const opId = "reserve-" + reserveRecord.reserve_pub; this.startOperation(opId); try { let exchange = await this.updateExchangeFromUrl(reserveRecord.exchange_base_url); let reserve = await this.updateReserve(reserveRecord.reserve_pub, - exchange); + exchange); let n = await this.depleteReserve(reserve, exchange); if (n != 0) { @@ -672,10 +672,10 @@ export class Wallet { } catch (e) { // random, exponential backoff truncated at 3 minutes let nextDelay = Math.min(2 * retryDelayMs + retryDelayMs * Math.random(), - 3000 * 60); + 3000 * 60); console.warn(`Failed to deplete reserve, trying again in ${retryDelayMs} ms`); setTimeout(() => this.processReserve(reserveRecord, nextDelay), - retryDelayMs); + retryDelayMs); } finally { this.stopOperation(opId); } @@ -683,18 +683,18 @@ export class Wallet { private async processPreCoin(preCoin: PreCoin, - retryDelayMs = 100): Promise { + retryDelayMs = 100): Promise { try { const coin = await this.withdrawExecute(preCoin); this.storeCoin(coin); } catch (e) { console.error("Failed to withdraw coin from precoin, retrying in", - retryDelayMs, - "ms", e); + retryDelayMs, + "ms", e); // exponential backoff truncated at one minute let nextRetryDelayMs = Math.min(retryDelayMs * 2, 1000 * 60); setTimeout(() => this.processPreCoin(preCoin, nextRetryDelayMs), - retryDelayMs); + retryDelayMs); } } @@ -801,8 +801,8 @@ export class Wallet { } let r = JSON.parse(resp.responseText); let denomSig = await this.cryptoApi.rsaUnblind(r.ev_sig, - pc.blindingKey, - pc.denomPub); + pc.blindingKey, + pc.denomPub); let coin: Coin = { coinPub: pc.coinPub, coinPriv: pc.coinPriv, @@ -840,7 +840,7 @@ export class Wallet { private async withdraw(denom: Denomination, reserve: Reserve): Promise { console.log("creating pre coin at", new Date()); let preCoin = await this.cryptoApi - .createPreCoin(denom, reserve); + .createPreCoin(denom, reserve); await Query(this.db) .put("precoins", preCoin) .finish(); @@ -852,10 +852,10 @@ export class Wallet { * Withdraw coins from a reserve until it is empty. */ private async depleteReserve(reserve: any, - exchange: IExchangeInfo): Promise { + exchange: IExchangeInfo): Promise { let denomsAvailable: Denomination[] = copy(exchange.active_denoms); let denomsForWithdraw = getWithdrawDenomList(reserve.current_amount, - denomsAvailable); + denomsAvailable); let ps = denomsForWithdraw.map((denom) => this.withdraw(denom, reserve)); await Promise.all(ps); @@ -868,11 +868,11 @@ export class Wallet { * by quering the reserve's exchange. */ private async updateReserve(reservePub: string, - exchange: IExchangeInfo): Promise { + exchange: IExchangeInfo): Promise { let reserve = await Query(this.db) .get("reserves", reservePub); let reqUrl = URI("reserve/status").absoluteTo(exchange.baseUrl); - reqUrl.query({'reserve_pub': reservePub}); + reqUrl.query({ 'reserve_pub': reservePub }); let resp = await this.http.get(reqUrl); if (resp.status != 200) { throw Error(); @@ -922,18 +922,18 @@ export class Wallet { } async getReserveCreationInfo(baseUrl: string, - amount: AmountJson): Promise { + amount: AmountJson): Promise { let exchangeInfo = await this.updateExchangeFromUrl(baseUrl); let selectedDenoms = getWithdrawDenomList(amount, - exchangeInfo.active_denoms); + exchangeInfo.active_denoms); let acc = Amounts.getZero(amount.currency); for (let d of selectedDenoms) { acc = Amounts.add(acc, d.fee_withdraw).amount; } let actualCoinCost = selectedDenoms .map((d: Denomination) => Amounts.add(d.value, - d.fee_withdraw).amount) + d.fee_withdraw).amount) .reduce((a, b) => Amounts.add(a, b).amount); let wireInfo = await this.getWireInfo(baseUrl); @@ -968,7 +968,7 @@ export class Wallet { private async suspendCoins(exchangeInfo: IExchangeInfo): Promise { let suspendedCoins = await Query(this.db) .iter("coins", - {indexName: "exchangeBaseUrl", only: exchangeInfo.baseUrl}) + { indexName: "exchangeBaseUrl", only: exchangeInfo.baseUrl }) .reduce((coin: Coin, suspendedCoins: Coin[]) => { if (!exchangeInfo.active_denoms.find((c) => c.denom_pub == coin.denomPub)) { return Array.prototype.concat(suspendedCoins, [coin]); @@ -987,7 +987,7 @@ 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"); @@ -1016,7 +1016,7 @@ export class Wallet { } let updatedExchangeInfo = await this.updateExchangeInfo(exchangeInfo, - exchangeKeysJson); + exchangeKeysJson); await this.suspendCoins(updatedExchangeInfo); await Query(this.db) @@ -1028,7 +1028,7 @@ export class Wallet { private async updateExchangeInfo(exchangeInfo: IExchangeInfo, - newKeys: KeysJson): Promise { + newKeys: KeysJson): Promise { if (exchangeInfo.masterPublicKey != newKeys.master_public_key) { throw Error("public keys do not match"); } @@ -1064,15 +1064,15 @@ export class Wallet { return true; }); - let ps = denomsToCheck.map(async(denom) => { + let ps = denomsToCheck.map(async (denom) => { let valid = await this.cryptoApi - .isValidDenom(denom, - exchangeInfo.masterPublicKey); + .isValidDenom(denom, + exchangeInfo.masterPublicKey); if (!valid) { console.error("invalid denomination", - denom, - "with key", - exchangeInfo.masterPublicKey); + denom, + "with key", + exchangeInfo.masterPublicKey); // FIXME: report to auditors } exchangeInfo.active_denoms.push(denom); @@ -1099,7 +1099,7 @@ export class Wallet { acc = Amounts.getZero(c.currentAmount.currency); } byCurrency[c.currentAmount.currency] = Amounts.add(c.currentAmount, - acc).amount; + acc).amount; return byCurrency; } @@ -1107,7 +1107,7 @@ export class Wallet { .iter("coins") .reduce(collectBalances, {}); - return {balances: byCurrency}; + return { balances: byCurrency }; } @@ -1122,12 +1122,41 @@ export class Wallet { let history = await Query(this.db) - .iter("history", {indexName: "timestamp"}) + .iter("history", { indexName: "timestamp" }) .reduce(collect, []); - return {history}; + return { history }; + } + + async getExchanges(): Promise { + return Query(this.db) + .iter("exchanges") + .flatMap((e) => [e]) + .toArray(); } + async getReserves(exchangeBaseUrl: string): Promise { + return Query(this.db) + .iter("reserves") + .filter((r: Reserve) => r.exchange_base_url === exchangeBaseUrl) + .toArray(); + } + + async getCoins(exchangeBaseUrl: string): Promise { + return Query(this.db) + .iter("coins") + .filter((c: Coin) => c.exchangeBaseUrl === exchangeBaseUrl) + .toArray(); + } + + async getPreCoins(exchangeBaseUrl: string): Promise { + return Query(this.db) + .iter("precoins") + .filter((c: PreCoin) => c.exchangeBaseUrl === exchangeBaseUrl) + .toArray(); + } + + async hashContract(contract: any): Promise { return this.cryptoApi.hashString(canonicalJson(contract)); } @@ -1138,12 +1167,12 @@ export class Wallet { async checkRepurchase(contract: Contract): Promise { if (!contract.repurchase_correlation_id) { console.log("no repurchase: no correlation id"); - return {isRepurchase: false}; + return { isRepurchase: false }; } let result: Transaction = await Query(this.db) .getIndexed("transactions", - "repurchase", - [contract.merchant_pub, contract.repurchase_correlation_id]); + "repurchase", + [contract.merchant_pub, contract.repurchase_correlation_id]); if (result) { console.assert(result.contract.repurchase_correlation_id == contract.repurchase_correlation_id); @@ -1153,7 +1182,7 @@ export class Wallet { existingFulfillmentUrl: result.contract.fulfillment_url, }; } else { - return {isRepurchase: false}; + return { isRepurchase: false }; } } } diff --git a/lib/wallet/wxApi.ts b/lib/wallet/wxApi.ts index 84235c6a9..549ce0a5a 100644 --- a/lib/wallet/wxApi.ts +++ b/lib/wallet/wxApi.ts @@ -14,8 +14,14 @@ TALER; see the file COPYING. If not, see */ -import {AmountJson} from "./types"; -import {ReserveCreationInfo} from "./types"; +import { + AmountJson, + Coin, + PreCoin, + ReserveCreationInfo, + IExchangeInfo, + Reserve +} from "./types"; /** * Interface to the wallet through WebExtension messaging. @@ -24,8 +30,8 @@ import {ReserveCreationInfo} from "./types"; export function getReserveCreationInfo(baseUrl: string, - amount: AmountJson): Promise { - let m = {type: "reserve-creation-info", detail: {baseUrl, amount}}; + amount: AmountJson): Promise { + let m = { type: "reserve-creation-info", detail: { baseUrl, amount } }; return new Promise((resolve, reject) => { chrome.runtime.sendMessage(m, (resp) => { if (resp.error) { @@ -39,3 +45,27 @@ export function getReserveCreationInfo(baseUrl: string, }); }); } + +export async function callBackend(type: string, detail?: any): Promise { + return new Promise((resolve, reject) => { + chrome.runtime.sendMessage({ type, detail }, (resp) => { + resolve(resp); + }); + }); +} + +export async function getExchanges(): Promise { + return await callBackend("get-exchanges"); +} + +export async function getReserves(exchangeBaseUrl: string): Promise { + return await callBackend("get-reserves", { exchangeBaseUrl }); +} + +export async function getCoins(exchangeBaseUrl: string): Promise { + return await callBackend("get-coins", { exchangeBaseUrl }); +} + +export async function getPreCoins(exchangeBaseUrl: string): Promise { + return await callBackend("get-precoins", { exchangeBaseUrl }); +} \ No newline at end of file diff --git a/lib/wallet/wxMessaging.ts b/lib/wallet/wxMessaging.ts index c8ffd7d7e..b1916b4bc 100644 --- a/lib/wallet/wxMessaging.ts +++ b/lib/wallet/wxMessaging.ts @@ -22,15 +22,15 @@ import { ConfirmReserveRequest, CreateReserveRequest } from "./wallet"; -import {deleteDb, exportDb, openTalerDb} from "./db"; -import {BrowserHttpLib} from "./http"; -import {Checkable} from "./checkable"; -import {AmountJson} from "./types"; +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 { Notifier } from "./types"; +import { Contract } from "./types"; import MessageSender = chrome.runtime.MessageSender; -import {ChromeBadge} from "./chromeBadge"; +import { ChromeBadge } from "./chromeBadge"; "use strict"; @@ -46,15 +46,15 @@ import {ChromeBadge} from "./chromeBadge"; type Handler = (detail: any, sender: MessageSender) => Promise; function makeHandlers(db: IDBDatabase, - wallet: Wallet): {[msg: string]: Handler} { + wallet: Wallet): { [msg: string]: Handler } { return { - ["balances"]: function(detail, sender) { + ["balances"]: function (detail, sender) { return wallet.getBalances(); }, - ["dump-db"]: function(detail, sender) { + ["dump-db"]: function (detail, sender) { return exportDb(db); }, - ["get-tab-cookie"]: function(detail, sender) { + ["get-tab-cookie"]: function (detail, sender) { if (!sender || !sender.tab || !sender.tab.id) { return Promise.resolve(); } @@ -63,10 +63,10 @@ function makeHandlers(db: IDBDatabase, delete paymentRequestCookies[id]; return Promise.resolve(info); }, - ["ping"]: function(detail, sender) { + ["ping"]: function (detail, sender) { return Promise.resolve(); }, - ["reset"]: function(detail, sender) { + ["reset"]: function (detail, sender) { if (db) { let tx = db.transaction(Array.from(db.objectStoreNames), 'readwrite'); for (let i = 0; i < db.objectStoreNames.length; i++) { @@ -75,12 +75,12 @@ function makeHandlers(db: IDBDatabase, } deleteDb(); - chrome.browserAction.setBadgeText({text: ""}); + chrome.browserAction.setBadgeText({ text: "" }); console.log("reset done"); // Response is synchronous return Promise.resolve({}); }, - ["create-reserve"]: function(detail, sender) { + ["create-reserve"]: function (detail, sender) { const d = { exchange: detail.exchange, amount: detail.amount, @@ -88,7 +88,7 @@ function makeHandlers(db: IDBDatabase, const req = CreateReserveRequest.checked(d); return wallet.createReserve(req); }, - ["confirm-reserve"]: function(detail, sender) { + ["confirm-reserve"]: function (detail, sender) { // TODO: make it a checkable const d = { reservePub: detail.reservePub @@ -96,7 +96,7 @@ function makeHandlers(db: IDBDatabase, const req = ConfirmReserveRequest.checked(d); return wallet.confirmReserve(req); }, - ["confirm-pay"]: function(detail, sender) { + ["confirm-pay"]: function (detail, sender) { let offer: Offer; try { offer = Offer.checked(detail.offer); @@ -104,10 +104,10 @@ function makeHandlers(db: IDBDatabase, if (e instanceof Checkable.SchemaError) { console.error("schema error:", e.message); return Promise.resolve({ - error: "invalid contract", - hint: e.message, - detail: detail - }); + error: "invalid contract", + hint: e.message, + detail: detail + }); } else { throw e; } @@ -115,7 +115,7 @@ function makeHandlers(db: IDBDatabase, return wallet.confirmPay(offer); }, - ["check-pay"]: function(detail, sender) { + ["check-pay"]: function (detail, sender) { let offer: Offer; try { offer = Offer.checked(detail.offer); @@ -123,22 +123,22 @@ function makeHandlers(db: IDBDatabase, if (e instanceof Checkable.SchemaError) { console.error("schema error:", e.message); return Promise.resolve({ - error: "invalid contract", - hint: e.message, - detail: detail - }); + error: "invalid contract", + hint: e.message, + detail: detail + }); } else { throw e; } } return wallet.checkPay(offer); }, - ["execute-payment"]: function(detail: any, sender: MessageSender) { + ["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 = { + let msg = { error: "rate limit exceeded for execute-payment", rateLimitExceeded: true, hint: "Check for redirect loops", @@ -148,42 +148,63 @@ function makeHandlers(db: IDBDatabase, } return wallet.executePayment(detail.H_contract); }, - ["exchange-info"]: function(detail) { + ["exchange-info"]: function (detail) { if (!detail.baseUrl) { - return Promise.resolve({error: "bad url"}); + return Promise.resolve({ error: "bad url" }); } return wallet.updateExchangeFromUrl(detail.baseUrl); }, - ["hash-contract"]: function(detail) { + ["hash-contract"]: function (detail) { if (!detail.contract) { - return Promise.resolve({error: "contract missing"}); + return Promise.resolve({ error: "contract missing" }); } return wallet.hashContract(detail.contract).then((hash) => { - return {hash}; + return { hash }; }); }, - ["put-history-entry"]: function(detail: any) { + ["put-history-entry"]: function (detail: any) { if (!detail.historyEntry) { - return Promise.resolve({error: "historyEntry missing"}); + return Promise.resolve({ error: "historyEntry missing" }); } return wallet.putHistory(detail.historyEntry); }, - ["reserve-creation-info"]: function(detail, sender) { + ["reserve-creation-info"]: function (detail, sender) { if (!detail.baseUrl || typeof detail.baseUrl !== "string") { - return Promise.resolve({error: "bad url"}); + return Promise.resolve({ error: "bad url" }); } let amount = AmountJson.checked(detail.amount); return wallet.getReserveCreationInfo(detail.baseUrl, amount); }, - ["check-repurchase"]: function(detail, sender) { + ["check-repurchase"]: function (detail, sender) { let contract = Contract.checked(detail.contract); return wallet.checkRepurchase(contract); }, - ["get-history"]: function(detail, sender) { + ["get-history"]: function (detail, sender) { // TODO: limit history length return wallet.getHistory(); }, - ["payment-failed"]: function(detail, sender) { + ["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); + }, + ["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. @@ -216,10 +237,10 @@ function dispatch(handlers: any, req: any, sender: any, sendResponse: any) { console.error(e); try { sendResponse({ - error: "exception", - hint: e.message, - stack: e.stack.toString() - }); + error: "exception", + hint: e.message, + stack: e.stack.toString() + }); } catch (e) { // might fail if tab disconnected @@ -230,7 +251,7 @@ function dispatch(handlers: any, req: any, sender: any, sendResponse: any) { } else { console.error(`Request type ${JSON.stringify(req)} unknown, req ${req.type}`); try { - sendResponse({error: "request unknown"}); + sendResponse({ error: "request unknown" }); } catch (e) { // might fail if tab disconnected } @@ -261,7 +282,7 @@ class ChromeNotifier implements Notifier { notify() { console.log("notifying all ports"); for (let p of this.ports) { - p.postMessage({notify: true}); + p.postMessage({ notify: true }); } } } @@ -270,11 +291,11 @@ class ChromeNotifier implements Notifier { /** * Mapping from tab ID to payment information (if any). */ -let paymentRequestCookies: {[n: number]: any} = {}; +let paymentRequestCookies: { [n: number]: any } = {}; function handleHttpPayment(headerList: chrome.webRequest.HttpHeader[], - url: string, tabId: number): any { - const headers: {[s: string]: string} = {}; + url: string, tabId: number): any { + const headers: { [s: string]: string } = {}; for (let kv of headerList) { if (kv.value) { headers[kv.name.toLowerCase()] = kv.value; @@ -283,7 +304,7 @@ function handleHttpPayment(headerList: chrome.webRequest.HttpHeader[], const contractUrl = headers["x-taler-contract-url"]; if (contractUrl !== undefined) { - paymentRequestCookies[tabId] = {type: "fetch", contractUrl}; + paymentRequestCookies[tabId] = { type: "fetch", contractUrl }; return; } @@ -313,21 +334,21 @@ function handleHttpPayment(headerList: chrome.webRequest.HttpHeader[], } // Useful for debugging ... -export let wallet: Wallet|undefined = undefined; -export let badge: ChromeBadge|undefined = undefined; +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} = {}; +let rateLimitCache: { [n: number]: number } = {}; function clearRateLimitCache() { rateLimitCache = {}; } export function wxMain() { - chrome.browserAction.setBadgeText({text: ""}); + chrome.browserAction.setBadgeText({ text: "" }); badge = new ChromeBadge(); - chrome.tabs.query({}, function(tabs) { + chrome.tabs.query({}, function (tabs) { for (let tab of tabs) { if (!tab.url || !tab.id) { return; @@ -335,9 +356,9 @@ export function wxMain() { 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: "lib/vendor/URI.js"}); - chrome.tabs.executeScript(tab.id, {file: "lib/taler-wallet-lib.js"}); - chrome.tabs.executeScript(tab.id, {file: "content_scripts/notify.js"}); + chrome.tabs.executeScript(tab.id, { file: "lib/vendor/URI.js" }); + chrome.tabs.executeScript(tab.id, { file: "lib/taler-wallet-lib.js" }); + chrome.tabs.executeScript(tab.id, { file: "content_scripts/notify.js" }); } } }); @@ -345,51 +366,51 @@ export function wxMain() { 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); - }); + .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); + }); } -- cgit v1.2.3