aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian.dold@gmail.com>2016-10-12 02:55:53 +0200
committerFlorian Dold <florian.dold@gmail.com>2016-10-12 02:55:53 +0200
commitd4be3906e32ac7d9933c6030d6493f2f2152bdd9 (patch)
tree3341586381b6975a3c1fa99ca69c63fba5ac9c35
parentdbcd85451edfc33f2e0a42c431f0cf3ab3b12876 (diff)
tree view of wallet db
-rw-r--r--.vscode/settings.json3
-rw-r--r--lib/components.ts45
-rw-r--r--lib/wallet/query.ts58
-rw-r--r--lib/wallet/renderHtml.tsx13
-rw-r--r--lib/wallet/types.ts2
-rw-r--r--lib/wallet/wallet.ts253
-rw-r--r--lib/wallet/wxApi.ts38
-rw-r--r--lib/wallet/wxMessaging.ts231
-rw-r--r--pages/confirm-create-reserve.tsx25
-rw-r--r--pages/tree.html32
-rw-r--r--pages/tree.tsx305
-rw-r--r--popup/popup.tsx30
-rw-r--r--tsconfig.json1
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