diff options
-rw-r--r-- | .vscode/settings.json | 3 | ||||
-rw-r--r-- | lib/components.ts | 45 | ||||
-rw-r--r-- | lib/wallet/query.ts | 58 | ||||
-rw-r--r-- | lib/wallet/renderHtml.tsx | 13 | ||||
-rw-r--r-- | lib/wallet/types.ts | 2 | ||||
-rw-r--r-- | lib/wallet/wallet.ts | 253 | ||||
-rw-r--r-- | lib/wallet/wxApi.ts | 38 | ||||
-rw-r--r-- | lib/wallet/wxMessaging.ts | 231 | ||||
-rw-r--r-- | pages/confirm-create-reserve.tsx | 25 | ||||
-rw-r--r-- | pages/tree.html | 32 | ||||
-rw-r--r-- | pages/tree.tsx | 305 | ||||
-rw-r--r-- | popup/popup.tsx | 30 | ||||
-rw-r--r-- | tsconfig.json | 1 |
13 files changed, 747 insertions, 289 deletions
diff --git a/.vscode/settings.json b/.vscode/settings.json index 46ce686a5..d6f4a1a1d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -29,6 +29,9 @@ "**/*.js": { "when": "$(basename).ts" }, + "**/*?.js": { + "when": "$(basename).tsx" + }, "**/*.js.map": true } }
\ No newline at end of file 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 <http://www.gnu.org/licenses/> + */ + + +/** + * General helper components + * + * @author Florian Dold + */ + +export interface StateHolder<T> { + (): T; + (newState: T): void; +} + +/** + * Component that doesn't hold its state in one object, + * but has multiple state holders. + */ +export abstract class ImplicitStateComponent<PropType> extends preact.Component<PropType, any> { + makeState<StateType>(initial: StateType): StateHolder<StateType> { + 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<T> { indexJoin<S>(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<T>; reduce<S>(f: (v: T, acc: S) => S, start?: S): Promise<S>; flatMap(f: (x: T) => T[]): QueryStream<T>; + toArray(): Promise<T[]>; } @@ -57,14 +58,14 @@ function openPromise<T>() { // Never happens, unless JS implementation is broken throw Error(); } - return {resolve, reject, promise}; + return { resolve, reject, promise }; } abstract class QueryStreamBase<T> implements QueryStream<T> { 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<T> implements QueryStream<T> { } indexJoin<S>(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<T> implements QueryStream<T> { return new QueryStreamFilter(this, f); } + toArray(): Promise<T[]> { + 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<A>(f: (x: any, acc?: A) => A, init?: A): Promise<any> { let {resolve, promise} = openPromise(); let acc = init; @@ -100,8 +118,8 @@ abstract class QueryStreamBase<T> implements QueryStream<T> { }); return Promise.resolve() - .then(() => this.root.finish()) - .then(() => promise); + .then(() => this.root.finish()) + .then(() => promise); } } @@ -161,7 +179,7 @@ class QueryStreamFlatMap<T> extends QueryStreamBase<T> { } -class QueryStreamIndexJoin<T,S> extends QueryStreamBase<[T, S]> { +class QueryStreamIndexJoin<T, S> extends QueryStreamBase<[T, S]> { s: QueryStreamBase<T>; storeName: string; key: any; @@ -214,11 +232,11 @@ class IterQueryStream<T> extends QueryStreamBase<T> { 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<T>(storeName: string, - {only = <string|undefined>undefined, indexName = <string|undefined>undefined} = {}): QueryStream<T> { + {only = <string | undefined>undefined, indexName = <string | undefined>undefined} = {}): QueryStream<T> { 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 { </ul> </div> ); +} + + +export function abbrev(s: string, n: number = 5) { + let sAbbrev = s; + if (s.length > n) { + sAbbrev = s.slice(0, n) + ".."; + } + return ( + <span className="abbrev" title={s}> + {sAbbrev} + </span> + ); }
\ 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<T, U>(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<HttpResponse>; + url: string | uri.URI, + options?: any): Promise<HttpResponse>; - get(url: string|uri.URI): Promise<HttpResponse>; + get(url: string | uri.URI): Promise<HttpResponse>; - postJson(url: string|uri.URI, body: any): Promise<HttpResponse>; + postJson(url: string | uri.URI, body: any): Promise<HttpResponse>; - postForm(url: string|uri.URI, form: any): Promise<HttpResponse>; + postForm(url: string | uri.URI, form: any): Promise<HttpResponse>; } @@ -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<string> = 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<ExchangeCoins> { + depositFeeLimit: AmountJson, + allowedExchanges: ExchangeHandle[]): Promise<ExchangeCoins> { // 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<void> { + payCoinInfo: PayCoinInfo, + chosenExchange: string): Promise<void> { 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<void> { + retryDelayMs: number = 250): Promise<void> { 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<void> { + retryDelayMs = 100): Promise<void> { 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<void> { 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<number> { + exchange: IExchangeInfo): Promise<number> { 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<Reserve> { + exchange: IExchangeInfo): Promise<Reserve> { 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<ReserveCreationInfo> { + amount: AmountJson): Promise<ReserveCreationInfo> { 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<void> { 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<IExchangeInfo> { + exchangeKeysJson: KeysJson): Promise<IExchangeInfo> { 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<IExchangeInfo> { + newKeys: KeysJson): Promise<IExchangeInfo> { 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<IExchangeInfo[]> { + return Query(this.db) + .iter<IExchangeInfo>("exchanges") + .flatMap((e) => [e]) + .toArray(); } + async getReserves(exchangeBaseUrl: string): Promise<Reserve[]> { + return Query(this.db) + .iter<Reserve>("reserves") + .filter((r: Reserve) => r.exchange_base_url === exchangeBaseUrl) + .toArray(); + } + + async getCoins(exchangeBaseUrl: string): Promise<Coin[]> { + return Query(this.db) + .iter<Coin>("coins") + .filter((c: Coin) => c.exchangeBaseUrl === exchangeBaseUrl) + .toArray(); + } + + async getPreCoins(exchangeBaseUrl: string): Promise<PreCoin[]> { + return Query(this.db) + .iter<PreCoin>("precoins") + .filter((c: PreCoin) => c.exchangeBaseUrl === exchangeBaseUrl) + .toArray(); + } + + async hashContract(contract: any): Promise<string> { return this.cryptoApi.hashString(canonicalJson(contract)); } @@ -1138,12 +1167,12 @@ export class Wallet { async checkRepurchase(contract: Contract): Promise<CheckRepurchaseResult> { 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 <http://www.gnu.org/licenses/> */ -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<ReserveCreationInfo> { - let m = {type: "reserve-creation-info", detail: {baseUrl, amount}}; + amount: AmountJson): Promise<ReserveCreationInfo> { + 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<any> { + return new Promise<IExchangeInfo[]>((resolve, reject) => { + chrome.runtime.sendMessage({ type, detail }, (resp) => { + resolve(resp); + }); + }); +} + +export async function getExchanges(): Promise<IExchangeInfo[]> { + return await callBackend("get-exchanges"); +} + +export async function getReserves(exchangeBaseUrl: string): Promise<Reserve[]> { + return await callBackend("get-reserves", { exchangeBaseUrl }); +} + +export async function getCoins(exchangeBaseUrl: string): Promise<Coin[]> { + return await callBackend("get-coins", { exchangeBaseUrl }); +} + +export async function getPreCoins(exchangeBaseUrl: string): Promise<PreCoin[]> { + 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<any>; 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: ["<all_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: ["<all_urls>"] }, ["responseHeaders", "blocking"]); + }) + .catch((e) => { + console.error("could not initialize wallet messaging"); + console.error(e); + }); } diff --git a/pages/confirm-create-reserve.tsx b/pages/confirm-create-reserve.tsx index a95bc46cb..3b5a4d161 100644 --- a/pages/confirm-create-reserve.tsx +++ b/pages/confirm-create-reserve.tsx @@ -27,6 +27,7 @@ import {AmountJson, CreateReserveResponse} from "../lib/wallet/types"; import {ReserveCreationInfo, Amounts} from "../lib/wallet/types"; import {Denomination} from "../lib/wallet/types"; import {getReserveCreationInfo} from "../lib/wallet/wxApi"; +import {ImplicitStateComponent, StateHolder} from "../lib/components"; "use strict"; @@ -63,30 +64,6 @@ class EventTrigger { } -interface StateHolder<T> { - (): T; - (newState: T): void; -} - -/** - * Component that doesn't hold its state in one object, - * but has multiple state holders. - */ -abstract class ImplicitStateComponent<PropType> extends preact.Component<PropType, void> { - makeState<StateType>(initial: StateType): StateHolder<StateType> { - 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; - }; - } -} - - function renderReserveCreationDetails(rci: ReserveCreationInfo|null) { if (!rci) { return <p> diff --git a/pages/tree.html b/pages/tree.html new file mode 100644 index 000000000..ee12a82e4 --- /dev/null +++ b/pages/tree.html @@ -0,0 +1,32 @@ +<!DOCTYPE html> +<html> + +<head> + <title>Taler Wallet: Tree View</title> + + <link rel="stylesheet" type="text/css" href="../style/lang.css"> + <link rel="stylesheet" type="text/css" href="../style/wallet.css"> + + <link rel="icon" href="../img/icon.png"> + + <script src="../lib/vendor/URI.js"></script> + <script src="../lib/vendor/preact.js"></script> + + <!-- i18n --> + <script src="../lib/vendor/jed.js"></script> + <script src="../lib/i18n.js"></script> + <script src="../i18n/strings.js"></script> + + <script src="../lib/vendor/system-csp-production.src.js"></script> + <script src="../lib/module-trampoline.js"></script> + + <style> + .tree-item { + margin: 2em; + border-radius: 5px; + border: 1px solid gray; + padding: 1em; + } + </style> + +</html>
\ No newline at end of file diff --git a/pages/tree.tsx b/pages/tree.tsx new file mode 100644 index 000000000..acc470216 --- /dev/null +++ b/pages/tree.tsx @@ -0,0 +1,305 @@ +/* + 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 <http://www.gnu.org/licenses/> + */ + +/** + * Show contents of the wallet as a tree. + * + * @author Florian Dold + */ + +/// <reference path="../lib/decl/preact.d.ts" /> + +import { IExchangeInfo } from "../lib/wallet/types"; +import { Reserve, Coin, PreCoin, Denomination } from "../lib/wallet/types"; +import { ImplicitStateComponent, StateHolder } from "../lib/components"; +import { getReserves, getExchanges, getCoins, getPreCoins } from "../lib/wallet/wxApi"; +import { prettyAmount, abbrev } from "../lib/wallet/renderHtml"; + +interface ReserveViewProps { + reserve: Reserve; +} + +class ReserveView extends preact.Component<ReserveViewProps, void> { + render(): JSX.Element { + let r: Reserve = this.props.reserve; + return ( + <div className="tree-item"> + <ul> + <li>Key: {r.reserve_pub}</li> + <li>Created: {(new Date(r.created * 1000).toString())}</li> + </ul> + </div> + ); + } +} + +interface ReserveListProps { + exchangeBaseUrl: string; +} + +interface ToggleProps { + expanded: StateHolder<boolean>; +} + +class Toggle extends ImplicitStateComponent<ToggleProps> { + renderButton() { + let show = () => { + this.props.expanded(true); + this.setState({}); + }; + let hide = () => { + this.props.expanded(false); + this.setState({}); + }; + if (this.props.expanded()) { + return <button onClick={hide}>hide</button>; + } + return <button onClick={show}>show</button>; + + } + render() { + return ( + <div style="display:inline;"> + {this.renderButton()} + {this.props.expanded() ? this.props.children : []} + </div>); + } +} + + +interface CoinViewProps { + coin: Coin; +} + +class CoinView extends preact.Component<CoinViewProps, void> { + render() { + let c = this.props.coin; + return ( + <div className="tree-item"> + <ul> + <li>Key: {c.coinPub}</li> + <li>Current amount: {prettyAmount(c.currentAmount)}</li> + <li>Denomination: {abbrev(c.denomPub, 20)}</li> + <li>Suspended: {(c.suspended || false).toString()}</li> + </ul> + </div> + ); + } +} + + + +interface PreCoinViewProps { + precoin: PreCoin; +} + +class PreCoinView extends preact.Component<PreCoinViewProps, void> { + render() { + let c = this.props.precoin; + return ( + <div className="tree-item"> + <ul> + <li>Key: {c.coinPub}</li> + </ul> + </div> + ); + } +} + +interface CoinListProps { + exchangeBaseUrl: string; +} + +class CoinList extends ImplicitStateComponent<CoinListProps> { + coins = this.makeState<Coin[] | null>(null); + expanded = this.makeState<boolean>(false); + + constructor(props: CoinListProps) { + super(props); + this.update(); + } + + async update() { + let coins = await getCoins(this.props.exchangeBaseUrl); + this.coins(coins); + } + + render(): JSX.Element { + if (!this.coins()) { + return <div>...</div>; + } + return ( + <div className="tree-item"> + Coins ({this.coins() !.length.toString()}) + {" "} + <Toggle expanded={this.expanded}> + {this.coins() !.map((c) => <CoinView coin={c} />)} + </Toggle> + </div> + ); + } +} + + +interface PreCoinListProps { + exchangeBaseUrl: string; +} + +class PreCoinList extends ImplicitStateComponent<PreCoinListProps> { + precoins = this.makeState<PreCoin[] | null>(null); + expanded = this.makeState<boolean>(false); + + constructor(props: PreCoinListProps) { + super(props); + this.update(); + } + + async update() { + let precoins = await getPreCoins(this.props.exchangeBaseUrl); + this.precoins(precoins); + } + + render(): JSX.Element { + if (!this.precoins()) { + return <div>...</div>; + } + return ( + <div className="tree-item"> + Pre-Coins ({this.precoins() !.length.toString()}) + {" "} + <Toggle expanded={this.expanded}> + {this.precoins() !.map((c) => <PreCoinView precoin={c} />)} + </Toggle> + </div> + ); + } +} + +interface DenominationListProps { + exchange: IExchangeInfo; +} + +class DenominationList extends ImplicitStateComponent<DenominationListProps> { + expanded = this.makeState<boolean>(false); + + renderDenom(d: Denomination) { + return ( + <div className="tree-item"> + <ul> + <li>Value: {prettyAmount(d.value)}</li> + <li>Withdraw fee: {prettyAmount(d.fee_withdraw)}</li> + <li>Refresh fee: {prettyAmount(d.fee_refresh)}</li> + <li>Deposit fee: {prettyAmount(d.fee_deposit)}</li> + <li>Refund fee: {prettyAmount(d.fee_refund)}</li> + </ul> + </div> + ); + } + + render(): JSX.Element { + return ( + <div className="tree-item"> + Denominations ({this.props.exchange.active_denoms.length.toString()}) + {" "} + <Toggle expanded={this.expanded}> + {this.props.exchange.active_denoms.map((d) => this.renderDenom(d))} + </Toggle> + </div> + ); + } +} + +class ReserveList extends ImplicitStateComponent<ReserveListProps> { + reserves = this.makeState<Reserve[] | null>(null); + expanded = this.makeState<boolean>(false); + + constructor(props: ReserveListProps) { + super(props); + this.update(); + } + + async update() { + let reserves = await getReserves(this.props.exchangeBaseUrl); + this.reserves(reserves); + } + + render(): JSX.Element { + if (!this.reserves()) { + return <div>...</div>; + } + return ( + <div className="tree-item"> + Reserves ({this.reserves() !.length.toString()}) + {" "} + <Toggle expanded={this.expanded}> + {this.reserves() !.map((r) => <ReserveView reserve={r} />)} + </Toggle> + </div> + ); + } +} + +interface ExchangeProps { + exchange: IExchangeInfo; +} + +class ExchangeView extends preact.Component<ExchangeProps, void> { + render(): JSX.Element { + let e = this.props.exchange; + return ( + <div className="tree-item"> + Url: {this.props.exchange.baseUrl} + <DenominationList exchange={e} /> + <ReserveList exchangeBaseUrl={this.props.exchange.baseUrl} /> + <CoinList exchangeBaseUrl={this.props.exchange.baseUrl} /> + <PreCoinList exchangeBaseUrl={this.props.exchange.baseUrl} /> + </div> + ); + } +} + +interface ExchangesListState { + exchanges: IExchangeInfo[]; +} + +class ExchangesList extends preact.Component<any, ExchangesListState> { + constructor() { + super(); + this.update(); + } + + async update() { + let exchanges = await getExchanges(); + console.log("exchanges: ", exchanges); + this.setState({ exchanges }); + } + + render(): JSX.Element { + if (!this.state.exchanges) { + return <span>...</span>; + } + return ( + <div className="tree-item"> + Exchanges ({this.state.exchanges.length.toString()}): + {this.state.exchanges.map(e => <ExchangeView exchange={e} />)} + </div> + ); + } +} + +export function main() { + preact.render(<ExchangesList />, document.body); +} diff --git a/popup/popup.tsx b/popup/popup.tsx index c4727d598..5364b4170 100644 --- a/popup/popup.tsx +++ b/popup/popup.tsx @@ -29,6 +29,7 @@ import {substituteFulfillmentUrl} from "../lib/wallet/helpers"; import BrowserClickedEvent = chrome.browserAction.BrowserClickedEvent; import {HistoryRecord, HistoryLevel} from "../lib/wallet/wallet"; import {AmountJson} from "../lib/wallet/types"; +import {abbrev, prettyAmount} from "../lib/wallet/renderHtml"; declare var i18n: any; @@ -226,7 +227,7 @@ class WalletBalance extends preact.Component<any, any> { } console.log(wallet); let listing = Object.keys(wallet).map((key) => { - return <p>{formatAmount(wallet[key])}</p> + return <p>{prettyAmount(wallet[key])}</p> }); if (listing.length > 0) { return <div>{listing}</div>; @@ -237,25 +238,6 @@ class WalletBalance extends preact.Component<any, any> { } -function formatAmount(amount: AmountJson) { - let v = amount.value + amount.fraction / 1e6; - return `${v.toFixed(2)} ${amount.currency}`; -} - - -function abbrev(s: string, n: number = 5) { - let sAbbrev = s; - if (s.length > n) { - sAbbrev = s.slice(0, n) + ".."; - } - return ( - <span className="abbrev" title={s}> - {sAbbrev} - </span> - ); -} - - function formatHistoryItem(historyItem: HistoryRecord) { const d = historyItem.detail; const t = historyItem.timestamp; @@ -264,14 +246,14 @@ function formatHistoryItem(historyItem: HistoryRecord) { case "create-reserve": return ( <p> - {i18n.parts`Bank requested reserve (${abbrev(d.reservePub)}) for ${formatAmount( + {i18n.parts`Bank requested reserve (${abbrev(d.reservePub)}) for ${prettyAmount( d.requestedAmount)}.`} </p> ); case "confirm-reserve": { // FIXME: eventually remove compat fix let exchange = d.exchangeBaseUrl ? URI(d.exchangeBaseUrl).host() : "??"; - let amount = formatAmount(d.requestedAmount); + let amount = prettyAmount(d.requestedAmount); let pub = abbrev(d.reservePub); return ( <p> @@ -291,7 +273,7 @@ function formatHistoryItem(historyItem: HistoryRecord) { } case "depleted-reserve": { let exchange = d.exchangeBaseUrl ? URI(d.exchangeBaseUrl).host() : "??"; - let amount = formatAmount(d.requestedAmount); + let amount = prettyAmount(d.requestedAmount); let pub = abbrev(d.reservePub); return (<p> {i18n.parts`Withdrew ${amount} from ${exchange} (${pub}).`} @@ -304,7 +286,7 @@ function formatHistoryItem(historyItem: HistoryRecord) { let fulfillmentLinkElem = <a href={url} onClick={openTab(url)}>view product</a>; return ( <p> - {i18n.parts`Paid ${formatAmount(d.amount)} to merchant ${merchantElem}. (${fulfillmentLinkElem})`} + {i18n.parts`Paid ${prettyAmount(d.amount)} to merchant ${merchantElem}. (${fulfillmentLinkElem})`} </p>); } default: diff --git a/tsconfig.json b/tsconfig.json index 7c964ff94..fa6cde6d3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -38,6 +38,7 @@ "pages/show-db.ts", "pages/confirm-contract.tsx", "pages/confirm-create-reserve.tsx", + "pages/tree.tsx", "test/tests/taler.ts" ] }
\ No newline at end of file |