tree view of wallet db

This commit is contained in:
Florian Dold 2016-10-12 02:55:53 +02:00
parent dbcd85451e
commit d4be3906e3
13 changed files with 745 additions and 287 deletions

View File

@ -29,6 +29,9 @@
"**/*.js": { "**/*.js": {
"when": "$(basename).ts" "when": "$(basename).ts"
}, },
"**/*?.js": {
"when": "$(basename).tsx"
},
"**/*.js.map": true "**/*.js.map": true
} }
} }

45
lib/components.ts Normal file
View File

@ -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;
};
}
}

View File

@ -35,10 +35,11 @@ export function Query(db: IDBDatabase) {
export interface QueryStream<T> { export interface QueryStream<T> {
indexJoin<S>(storeName: string, indexJoin<S>(storeName: string,
indexName: string, indexName: string,
keyFn: (obj: any) => any): QueryStream<[T,S]>; keyFn: (obj: any) => any): QueryStream<[T, S]>;
filter(f: (x: any) => boolean): QueryStream<T>; filter(f: (x: any) => boolean): QueryStream<T>;
reduce<S>(f: (v: T, acc: S) => S, start?: S): Promise<S>; reduce<S>(f: (v: T, acc: S) => S, start?: S): Promise<S>;
flatMap(f: (x: T) => T[]): QueryStream<T>; flatMap(f: (x: T) => T[]): QueryStream<T>;
toArray(): Promise<T[]>;
} }
@ -57,7 +58,7 @@ function openPromise<T>() {
// Never happens, unless JS implementation is broken // Never happens, unless JS implementation is broken
throw Error(); throw Error();
} }
return {resolve, reject, promise}; return { resolve, reject, promise };
} }
@ -78,7 +79,7 @@ abstract class QueryStreamBase<T> implements QueryStream<T> {
indexJoin<S>(storeName: string, indexJoin<S>(storeName: string,
indexName: string, indexName: string,
key: any): QueryStream<[T,S]> { key: any): QueryStream<[T, S]> {
this.root.addStoreAccess(storeName, false); this.root.addStoreAccess(storeName, false);
return new QueryStreamIndexJoin(this, storeName, indexName, key); return new QueryStreamIndexJoin(this, storeName, indexName, key);
} }
@ -87,6 +88,23 @@ abstract class QueryStreamBase<T> implements QueryStream<T> {
return new QueryStreamFilter(this, f); 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> { reduce<A>(f: (x: any, acc?: A) => A, init?: A): Promise<any> {
let {resolve, promise} = openPromise(); let {resolve, promise} = openPromise();
let acc = init; let acc = init;
@ -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>; s: QueryStreamBase<T>;
storeName: string; storeName: string;
key: any; key: any;
@ -218,7 +236,7 @@ class IterQueryStream<T> extends QueryStreamBase<T> {
} else { } else {
s = tx.objectStore(this.storeName); s = tx.objectStore(this.storeName);
} }
let kr: IDBKeyRange|undefined = undefined; let kr: IDBKeyRange | undefined = undefined;
if (only !== undefined) { if (only !== undefined) {
kr = IDBKeyRange.only(this.options.only); kr = IDBKeyRange.only(this.options.only);
} }
@ -264,9 +282,9 @@ class QueryRoot {
} }
iter<T>(storeName: string, 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); this.stores.add(storeName);
return new IterQueryStream(this, storeName, {only, indexName}); return new IterQueryStream(this, storeName, { only, indexName });
} }
/** /**

View File

@ -48,3 +48,16 @@ export function renderContract(contract: Contract): JSX.Element {
</div> </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>
);
}

View File

@ -152,6 +152,8 @@ export interface Reserve {
exchange_base_url: string exchange_base_url: string
reserve_priv: string; reserve_priv: string;
reserve_pub: string; reserve_pub: string;
created: number;
current_amount: AmountJson;
} }

View File

@ -29,19 +29,19 @@ import {
Notifier, Notifier,
WireInfo WireInfo
} from "./types"; } from "./types";
import {HttpResponse, RequestException} from "./http"; import { HttpResponse, RequestException } from "./http";
import {Query} from "./query"; import { Query } from "./query";
import {Checkable} from "./checkable"; import { Checkable } from "./checkable";
import {canonicalizeBaseUrl} from "./helpers"; import { canonicalizeBaseUrl } from "./helpers";
import {ReserveCreationInfo, Amounts} from "./types"; import { ReserveCreationInfo, Amounts } from "./types";
import {PreCoin} from "./types"; import { PreCoin } from "./types";
import {Reserve} from "./types"; import { Reserve } from "./types";
import {CryptoApi} from "./cryptoApi"; import { CryptoApi } from "./cryptoApi";
import {Coin} from "./types"; import { Coin } from "./types";
import {PayCoinInfo} from "./types"; import { PayCoinInfo } from "./types";
import {CheckRepurchaseResult} from "./types"; import { CheckRepurchaseResult } from "./types";
import {Contract} from "./types"; import { Contract } from "./types";
import {ExchangeHandle} from "./types"; import { ExchangeHandle } from "./types";
"use strict"; "use strict";
@ -55,11 +55,11 @@ interface ReserveRecord {
reserve_priv: string, reserve_priv: string,
exchange_base_url: string, exchange_base_url: string,
created: number, created: number,
last_query: number|null, last_query: number | null,
/** /**
* Current amount left in the reserve * Current amount left in the reserve
*/ */
current_amount: AmountJson|null, current_amount: AmountJson | null,
/** /**
* Amount requested when the reserve was created. * Amount requested when the reserve was created.
* When a reserve is re-used (rare!) the current_amount can * 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]*)\)\/?/); const m = stamp.match(/\/?Date\(([0-9]*)\)\/?/);
if (!m) { if (!m) {
return null; return null;
@ -256,14 +256,14 @@ function isWithdrawableDenom(d: Denomination) {
interface HttpRequestLibrary { interface HttpRequestLibrary {
req(method: string, req(method: string,
url: string|uri.URI, url: string | uri.URI,
options?: any): Promise<HttpResponse>; 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>;
} }
@ -425,7 +425,7 @@ export class Wallet {
console.warn("same pubkey for different currencies"); console.warn("same pubkey for different currencies");
return; return;
} }
let cd = {coin, denom}; let cd = { coin, denom };
let x = m[url]; let x = m[url];
if (!x) { if (!x) {
m[url] = [cd]; m[url] = [cd];
@ -446,7 +446,7 @@ export class Wallet {
console.log("Checking for merchant's exchange", JSON.stringify(info)); console.log("Checking for merchant's exchange", JSON.stringify(info));
return [ return [
Query(this.db) 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) .indexJoin("coins", "exchangeBaseUrl", (exchange) => exchange.baseUrl)
.reduce((x) => storeExchangeCoin(x, info.url)) .reduce((x) => storeExchangeCoin(x, info.url))
]; ];
@ -600,7 +600,7 @@ export class Wallet {
Query(this.db) Query(this.db)
.get("transactions", offer.H_contract); .get("transactions", offer.H_contract);
if (transaction) { if (transaction) {
return {isPayed: true}; return { isPayed: true };
} }
// If not already payed, check if we could pay for it. // If not already payed, check if we could pay for it.
@ -614,7 +614,7 @@ export class Wallet {
error: "coins-insufficient", error: "coins-insufficient",
}; };
} }
return {isPayed: false}; return { isPayed: false };
} }
@ -872,7 +872,7 @@ export class Wallet {
let reserve = await Query(this.db) let reserve = await Query(this.db)
.get("reserves", reservePub); .get("reserves", reservePub);
let reqUrl = URI("reserve/status").absoluteTo(exchange.baseUrl); 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); let resp = await this.http.get(reqUrl);
if (resp.status != 200) { if (resp.status != 200) {
throw Error(); throw Error();
@ -968,7 +968,7 @@ export class Wallet {
private async suspendCoins(exchangeInfo: IExchangeInfo): Promise<void> { private async suspendCoins(exchangeInfo: IExchangeInfo): Promise<void> {
let suspendedCoins = await Query(this.db) let suspendedCoins = await Query(this.db)
.iter("coins", .iter("coins",
{indexName: "exchangeBaseUrl", only: exchangeInfo.baseUrl}) { indexName: "exchangeBaseUrl", only: exchangeInfo.baseUrl })
.reduce((coin: Coin, suspendedCoins: Coin[]) => { .reduce((coin: Coin, suspendedCoins: Coin[]) => {
if (!exchangeInfo.active_denoms.find((c) => c.denom_pub == coin.denomPub)) { if (!exchangeInfo.active_denoms.find((c) => c.denom_pub == coin.denomPub)) {
return Array.prototype.concat(suspendedCoins, [coin]); return Array.prototype.concat(suspendedCoins, [coin]);
@ -1064,7 +1064,7 @@ export class Wallet {
return true; return true;
}); });
let ps = denomsToCheck.map(async(denom) => { let ps = denomsToCheck.map(async (denom) => {
let valid = await this.cryptoApi let valid = await this.cryptoApi
.isValidDenom(denom, .isValidDenom(denom,
exchangeInfo.masterPublicKey); exchangeInfo.masterPublicKey);
@ -1107,7 +1107,7 @@ export class Wallet {
.iter("coins") .iter("coins")
.reduce(collectBalances, {}); .reduce(collectBalances, {});
return {balances: byCurrency}; return { balances: byCurrency };
} }
@ -1122,12 +1122,41 @@ export class Wallet {
let history = await let history = await
Query(this.db) Query(this.db)
.iter("history", {indexName: "timestamp"}) .iter("history", { indexName: "timestamp" })
.reduce(collect, []); .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> { async hashContract(contract: any): Promise<string> {
return this.cryptoApi.hashString(canonicalJson(contract)); return this.cryptoApi.hashString(canonicalJson(contract));
} }
@ -1138,7 +1167,7 @@ export class Wallet {
async checkRepurchase(contract: Contract): Promise<CheckRepurchaseResult> { async checkRepurchase(contract: Contract): Promise<CheckRepurchaseResult> {
if (!contract.repurchase_correlation_id) { if (!contract.repurchase_correlation_id) {
console.log("no repurchase: no correlation id"); console.log("no repurchase: no correlation id");
return {isRepurchase: false}; return { isRepurchase: false };
} }
let result: Transaction = await Query(this.db) let result: Transaction = await Query(this.db)
.getIndexed("transactions", .getIndexed("transactions",
@ -1153,7 +1182,7 @@ export class Wallet {
existingFulfillmentUrl: result.contract.fulfillment_url, existingFulfillmentUrl: result.contract.fulfillment_url,
}; };
} else { } else {
return {isRepurchase: false}; return { isRepurchase: false };
} }
} }
} }

View File

@ -14,8 +14,14 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/ */
import {AmountJson} from "./types"; import {
import {ReserveCreationInfo} from "./types"; AmountJson,
Coin,
PreCoin,
ReserveCreationInfo,
IExchangeInfo,
Reserve
} from "./types";
/** /**
* Interface to the wallet through WebExtension messaging. * Interface to the wallet through WebExtension messaging.
@ -25,7 +31,7 @@ import {ReserveCreationInfo} from "./types";
export function getReserveCreationInfo(baseUrl: string, export function getReserveCreationInfo(baseUrl: string,
amount: AmountJson): Promise<ReserveCreationInfo> { amount: AmountJson): Promise<ReserveCreationInfo> {
let m = {type: "reserve-creation-info", detail: {baseUrl, amount}}; let m = { type: "reserve-creation-info", detail: { baseUrl, amount } };
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
chrome.runtime.sendMessage(m, (resp) => { chrome.runtime.sendMessage(m, (resp) => {
if (resp.error) { 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 });
}

View File

@ -22,15 +22,15 @@ import {
ConfirmReserveRequest, ConfirmReserveRequest,
CreateReserveRequest CreateReserveRequest
} from "./wallet"; } from "./wallet";
import {deleteDb, exportDb, openTalerDb} from "./db"; import { deleteDb, exportDb, openTalerDb } from "./db";
import {BrowserHttpLib} from "./http"; import { BrowserHttpLib } from "./http";
import {Checkable} from "./checkable"; import { Checkable } from "./checkable";
import {AmountJson} from "./types"; import { AmountJson } from "./types";
import Port = chrome.runtime.Port; import Port = chrome.runtime.Port;
import {Notifier} from "./types"; import { Notifier } from "./types";
import {Contract} from "./types"; import { Contract } from "./types";
import MessageSender = chrome.runtime.MessageSender; import MessageSender = chrome.runtime.MessageSender;
import {ChromeBadge} from "./chromeBadge"; import { ChromeBadge } from "./chromeBadge";
"use strict"; "use strict";
@ -46,15 +46,15 @@ import {ChromeBadge} from "./chromeBadge";
type Handler = (detail: any, sender: MessageSender) => Promise<any>; type Handler = (detail: any, sender: MessageSender) => Promise<any>;
function makeHandlers(db: IDBDatabase, function makeHandlers(db: IDBDatabase,
wallet: Wallet): {[msg: string]: Handler} { wallet: Wallet): { [msg: string]: Handler } {
return { return {
["balances"]: function(detail, sender) { ["balances"]: function (detail, sender) {
return wallet.getBalances(); return wallet.getBalances();
}, },
["dump-db"]: function(detail, sender) { ["dump-db"]: function (detail, sender) {
return exportDb(db); return exportDb(db);
}, },
["get-tab-cookie"]: function(detail, sender) { ["get-tab-cookie"]: function (detail, sender) {
if (!sender || !sender.tab || !sender.tab.id) { if (!sender || !sender.tab || !sender.tab.id) {
return Promise.resolve(); return Promise.resolve();
} }
@ -63,10 +63,10 @@ function makeHandlers(db: IDBDatabase,
delete paymentRequestCookies[id]; delete paymentRequestCookies[id];
return Promise.resolve(info); return Promise.resolve(info);
}, },
["ping"]: function(detail, sender) { ["ping"]: function (detail, sender) {
return Promise.resolve(); return Promise.resolve();
}, },
["reset"]: function(detail, sender) { ["reset"]: function (detail, sender) {
if (db) { if (db) {
let tx = db.transaction(Array.from(db.objectStoreNames), 'readwrite'); let tx = db.transaction(Array.from(db.objectStoreNames), 'readwrite');
for (let i = 0; i < db.objectStoreNames.length; i++) { for (let i = 0; i < db.objectStoreNames.length; i++) {
@ -75,12 +75,12 @@ function makeHandlers(db: IDBDatabase,
} }
deleteDb(); deleteDb();
chrome.browserAction.setBadgeText({text: ""}); chrome.browserAction.setBadgeText({ text: "" });
console.log("reset done"); console.log("reset done");
// Response is synchronous // Response is synchronous
return Promise.resolve({}); return Promise.resolve({});
}, },
["create-reserve"]: function(detail, sender) { ["create-reserve"]: function (detail, sender) {
const d = { const d = {
exchange: detail.exchange, exchange: detail.exchange,
amount: detail.amount, amount: detail.amount,
@ -88,7 +88,7 @@ function makeHandlers(db: IDBDatabase,
const req = CreateReserveRequest.checked(d); const req = CreateReserveRequest.checked(d);
return wallet.createReserve(req); return wallet.createReserve(req);
}, },
["confirm-reserve"]: function(detail, sender) { ["confirm-reserve"]: function (detail, sender) {
// TODO: make it a checkable // TODO: make it a checkable
const d = { const d = {
reservePub: detail.reservePub reservePub: detail.reservePub
@ -96,7 +96,7 @@ function makeHandlers(db: IDBDatabase,
const req = ConfirmReserveRequest.checked(d); const req = ConfirmReserveRequest.checked(d);
return wallet.confirmReserve(req); return wallet.confirmReserve(req);
}, },
["confirm-pay"]: function(detail, sender) { ["confirm-pay"]: function (detail, sender) {
let offer: Offer; let offer: Offer;
try { try {
offer = Offer.checked(detail.offer); offer = Offer.checked(detail.offer);
@ -115,7 +115,7 @@ function makeHandlers(db: IDBDatabase,
return wallet.confirmPay(offer); return wallet.confirmPay(offer);
}, },
["check-pay"]: function(detail, sender) { ["check-pay"]: function (detail, sender) {
let offer: Offer; let offer: Offer;
try { try {
offer = Offer.checked(detail.offer); offer = Offer.checked(detail.offer);
@ -133,7 +133,7 @@ function makeHandlers(db: IDBDatabase,
} }
return wallet.checkPay(offer); return wallet.checkPay(offer);
}, },
["execute-payment"]: function(detail: any, sender: MessageSender) { ["execute-payment"]: function (detail: any, sender: MessageSender) {
if (sender.tab && sender.tab.id) { if (sender.tab && sender.tab.id) {
rateLimitCache[sender.tab.id]++; rateLimitCache[sender.tab.id]++;
if (rateLimitCache[sender.tab.id] > 10) { if (rateLimitCache[sender.tab.id] > 10) {
@ -148,42 +148,63 @@ function makeHandlers(db: IDBDatabase,
} }
return wallet.executePayment(detail.H_contract); return wallet.executePayment(detail.H_contract);
}, },
["exchange-info"]: function(detail) { ["exchange-info"]: function (detail) {
if (!detail.baseUrl) { if (!detail.baseUrl) {
return Promise.resolve({error: "bad url"}); return Promise.resolve({ error: "bad url" });
} }
return wallet.updateExchangeFromUrl(detail.baseUrl); return wallet.updateExchangeFromUrl(detail.baseUrl);
}, },
["hash-contract"]: function(detail) { ["hash-contract"]: function (detail) {
if (!detail.contract) { if (!detail.contract) {
return Promise.resolve({error: "contract missing"}); return Promise.resolve({ error: "contract missing" });
} }
return wallet.hashContract(detail.contract).then((hash) => { 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) { if (!detail.historyEntry) {
return Promise.resolve({error: "historyEntry missing"}); return Promise.resolve({ error: "historyEntry missing" });
} }
return wallet.putHistory(detail.historyEntry); return wallet.putHistory(detail.historyEntry);
}, },
["reserve-creation-info"]: function(detail, sender) { ["reserve-creation-info"]: function (detail, sender) {
if (!detail.baseUrl || typeof detail.baseUrl !== "string") { 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); let amount = AmountJson.checked(detail.amount);
return wallet.getReserveCreationInfo(detail.baseUrl, amount); return wallet.getReserveCreationInfo(detail.baseUrl, amount);
}, },
["check-repurchase"]: function(detail, sender) { ["check-repurchase"]: function (detail, sender) {
let contract = Contract.checked(detail.contract); let contract = Contract.checked(detail.contract);
return wallet.checkRepurchase(contract); return wallet.checkRepurchase(contract);
}, },
["get-history"]: function(detail, sender) { ["get-history"]: function (detail, sender) {
// TODO: limit history length // TODO: limit history length
return wallet.getHistory(); 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 // For now we just update exchanges (maybe the exchange did something
// wrong and the keys were messed up). // wrong and the keys were messed up).
// FIXME: in the future we should look at what actually went wrong. // FIXME: in the future we should look at what actually went wrong.
@ -230,7 +251,7 @@ function dispatch(handlers: any, req: any, sender: any, sendResponse: any) {
} else { } else {
console.error(`Request type ${JSON.stringify(req)} unknown, req ${req.type}`); console.error(`Request type ${JSON.stringify(req)} unknown, req ${req.type}`);
try { try {
sendResponse({error: "request unknown"}); sendResponse({ error: "request unknown" });
} catch (e) { } catch (e) {
// might fail if tab disconnected // might fail if tab disconnected
} }
@ -261,7 +282,7 @@ class ChromeNotifier implements Notifier {
notify() { notify() {
console.log("notifying all ports"); console.log("notifying all ports");
for (let p of this.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). * 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[], function handleHttpPayment(headerList: chrome.webRequest.HttpHeader[],
url: string, tabId: number): any { url: string, tabId: number): any {
const headers: {[s: string]: string} = {}; const headers: { [s: string]: string } = {};
for (let kv of headerList) { for (let kv of headerList) {
if (kv.value) { if (kv.value) {
headers[kv.name.toLowerCase()] = 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"]; const contractUrl = headers["x-taler-contract-url"];
if (contractUrl !== undefined) { if (contractUrl !== undefined) {
paymentRequestCookies[tabId] = {type: "fetch", contractUrl}; paymentRequestCookies[tabId] = { type: "fetch", contractUrl };
return; return;
} }
@ -313,21 +334,21 @@ function handleHttpPayment(headerList: chrome.webRequest.HttpHeader[],
} }
// Useful for debugging ... // Useful for debugging ...
export let wallet: Wallet|undefined = undefined; export let wallet: Wallet | undefined = undefined;
export let badge: ChromeBadge|undefined = undefined; export let badge: ChromeBadge | undefined = undefined;
// Rate limit cache for executePayment operations, to break redirect loops // Rate limit cache for executePayment operations, to break redirect loops
let rateLimitCache: {[n: number]: number} = {}; let rateLimitCache: { [n: number]: number } = {};
function clearRateLimitCache() { function clearRateLimitCache() {
rateLimitCache = {}; rateLimitCache = {};
} }
export function wxMain() { export function wxMain() {
chrome.browserAction.setBadgeText({text: ""}); chrome.browserAction.setBadgeText({ text: "" });
badge = new ChromeBadge(); badge = new ChromeBadge();
chrome.tabs.query({}, function(tabs) { chrome.tabs.query({}, function (tabs) {
for (let tab of tabs) { for (let tab of tabs) {
if (!tab.url || !tab.id) { if (!tab.url || !tab.id) {
return; return;
@ -335,9 +356,9 @@ export function wxMain() {
let uri = URI(tab.url); let uri = URI(tab.url);
if (uri.protocol() == "http" || uri.protocol() == "https") { if (uri.protocol() == "http" || uri.protocol() == "https") {
console.log("injecting into existing tab", tab.id); 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/vendor/URI.js" });
chrome.tabs.executeScript(tab.id, {file: "lib/taler-wallet-lib.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: "content_scripts/notify.js" });
} }
} }
}); });
@ -386,7 +407,7 @@ export function wxMain() {
return handleHttpPayment(details.responseHeaders || [], return handleHttpPayment(details.responseHeaders || [],
details.url, details.url,
details.tabId); details.tabId);
}, {urls: ["<all_urls>"]}, ["responseHeaders", "blocking"]); }, { urls: ["<all_urls>"] }, ["responseHeaders", "blocking"]);
}) })
.catch((e) => { .catch((e) => {
console.error("could not initialize wallet messaging"); console.error("could not initialize wallet messaging");

View File

@ -27,6 +27,7 @@ import {AmountJson, CreateReserveResponse} from "../lib/wallet/types";
import {ReserveCreationInfo, Amounts} from "../lib/wallet/types"; import {ReserveCreationInfo, Amounts} from "../lib/wallet/types";
import {Denomination} from "../lib/wallet/types"; import {Denomination} from "../lib/wallet/types";
import {getReserveCreationInfo} from "../lib/wallet/wxApi"; import {getReserveCreationInfo} from "../lib/wallet/wxApi";
import {ImplicitStateComponent, StateHolder} from "../lib/components";
"use strict"; "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) { function renderReserveCreationDetails(rci: ReserveCreationInfo|null) {
if (!rci) { if (!rci) {
return <p> return <p>

32
pages/tree.html Normal file
View File

@ -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>

305
pages/tree.tsx Normal file
View File

@ -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);
}

View File

@ -29,6 +29,7 @@ import {substituteFulfillmentUrl} from "../lib/wallet/helpers";
import BrowserClickedEvent = chrome.browserAction.BrowserClickedEvent; import BrowserClickedEvent = chrome.browserAction.BrowserClickedEvent;
import {HistoryRecord, HistoryLevel} from "../lib/wallet/wallet"; import {HistoryRecord, HistoryLevel} from "../lib/wallet/wallet";
import {AmountJson} from "../lib/wallet/types"; import {AmountJson} from "../lib/wallet/types";
import {abbrev, prettyAmount} from "../lib/wallet/renderHtml";
declare var i18n: any; declare var i18n: any;
@ -226,7 +227,7 @@ class WalletBalance extends preact.Component<any, any> {
} }
console.log(wallet); console.log(wallet);
let listing = Object.keys(wallet).map((key) => { let listing = Object.keys(wallet).map((key) => {
return <p>{formatAmount(wallet[key])}</p> return <p>{prettyAmount(wallet[key])}</p>
}); });
if (listing.length > 0) { if (listing.length > 0) {
return <div>{listing}</div>; 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) { function formatHistoryItem(historyItem: HistoryRecord) {
const d = historyItem.detail; const d = historyItem.detail;
const t = historyItem.timestamp; const t = historyItem.timestamp;
@ -264,14 +246,14 @@ function formatHistoryItem(historyItem: HistoryRecord) {
case "create-reserve": case "create-reserve":
return ( return (
<p> <p>
{i18n.parts`Bank requested reserve (${abbrev(d.reservePub)}) for ${formatAmount( {i18n.parts`Bank requested reserve (${abbrev(d.reservePub)}) for ${prettyAmount(
d.requestedAmount)}.`} d.requestedAmount)}.`}
</p> </p>
); );
case "confirm-reserve": { case "confirm-reserve": {
// FIXME: eventually remove compat fix // FIXME: eventually remove compat fix
let exchange = d.exchangeBaseUrl ? URI(d.exchangeBaseUrl).host() : "??"; let exchange = d.exchangeBaseUrl ? URI(d.exchangeBaseUrl).host() : "??";
let amount = formatAmount(d.requestedAmount); let amount = prettyAmount(d.requestedAmount);
let pub = abbrev(d.reservePub); let pub = abbrev(d.reservePub);
return ( return (
<p> <p>
@ -291,7 +273,7 @@ function formatHistoryItem(historyItem: HistoryRecord) {
} }
case "depleted-reserve": { case "depleted-reserve": {
let exchange = d.exchangeBaseUrl ? URI(d.exchangeBaseUrl).host() : "??"; let exchange = d.exchangeBaseUrl ? URI(d.exchangeBaseUrl).host() : "??";
let amount = formatAmount(d.requestedAmount); let amount = prettyAmount(d.requestedAmount);
let pub = abbrev(d.reservePub); let pub = abbrev(d.reservePub);
return (<p> return (<p>
{i18n.parts`Withdrew ${amount} from ${exchange} (${pub}).`} {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>; let fulfillmentLinkElem = <a href={url} onClick={openTab(url)}>view product</a>;
return ( return (
<p> <p>
{i18n.parts`Paid ${formatAmount(d.amount)} to merchant ${merchantElem}. (${fulfillmentLinkElem})`} {i18n.parts`Paid ${prettyAmount(d.amount)} to merchant ${merchantElem}. (${fulfillmentLinkElem})`}
</p>); </p>);
} }
default: default:

View File

@ -38,6 +38,7 @@
"pages/show-db.ts", "pages/show-db.ts",
"pages/confirm-contract.tsx", "pages/confirm-contract.tsx",
"pages/confirm-create-reserve.tsx", "pages/confirm-create-reserve.tsx",
"pages/tree.tsx",
"test/tests/taler.ts" "test/tests/taler.ts"
] ]
} }