tree view of wallet db
This commit is contained in:
parent
dbcd85451e
commit
d4be3906e3
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -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
45
lib/components.ts
Normal 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;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -34,11 +34,12 @@ 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,14 +58,14 @@ 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 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
abstract class QueryStreamBase<T> implements QueryStream<T> {
|
abstract class QueryStreamBase<T> implements QueryStream<T> {
|
||||||
abstract subscribe(f: (isDone: boolean,
|
abstract subscribe(f: (isDone: boolean,
|
||||||
value: any,
|
value: any,
|
||||||
tx: IDBTransaction) => void): void;
|
tx: IDBTransaction) => void): void;
|
||||||
|
|
||||||
root: QueryRoot;
|
root: QueryRoot;
|
||||||
|
|
||||||
@ -77,8 +78,8 @@ 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;
|
||||||
@ -100,8 +118,8 @@ abstract class QueryStreamBase<T> implements QueryStream<T> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
.then(() => this.root.finish())
|
.then(() => this.root.finish())
|
||||||
.then(() => promise);
|
.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>;
|
s: QueryStreamBase<T>;
|
||||||
storeName: string;
|
storeName: string;
|
||||||
key: any;
|
key: any;
|
||||||
@ -214,11 +232,11 @@ class IterQueryStream<T> extends QueryStreamBase<T> {
|
|||||||
let s: any;
|
let s: any;
|
||||||
if (indexName !== void 0) {
|
if (indexName !== void 0) {
|
||||||
s = tx.objectStore(this.storeName)
|
s = tx.objectStore(this.storeName)
|
||||||
.index(this.options.indexName);
|
.index(this.options.indexName);
|
||||||
} 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 });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -330,8 +348,8 @@ class QueryRoot {
|
|||||||
|
|
||||||
this.addWork(doGet, storeName, false);
|
this.addWork(doGet, storeName, false);
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
.then(() => this.finish())
|
.then(() => this.finish())
|
||||||
.then(() => promise);
|
.then(() => promise);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -353,8 +371,8 @@ class QueryRoot {
|
|||||||
|
|
||||||
this.addWork(doGetIndexed, storeName, false);
|
this.addWork(doGetIndexed, storeName, false);
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
.then(() => this.finish())
|
.then(() => this.finish())
|
||||||
.then(() => promise);
|
.then(() => promise);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -396,8 +414,8 @@ class QueryRoot {
|
|||||||
* Low-level function to add a task to the internal work queue.
|
* Low-level function to add a task to the internal work queue.
|
||||||
*/
|
*/
|
||||||
addWork(workFn: (t: IDBTransaction) => void,
|
addWork(workFn: (t: IDBTransaction) => void,
|
||||||
storeName?: string,
|
storeName?: string,
|
||||||
isWrite?: boolean) {
|
isWrite?: boolean) {
|
||||||
this.work.push(workFn);
|
this.work.push(workFn);
|
||||||
if (storeName) {
|
if (storeName) {
|
||||||
this.addStoreAccess(storeName, isWrite);
|
this.addStoreAccess(storeName, isWrite);
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -288,7 +288,7 @@ interface KeyUpdateInfo {
|
|||||||
* amount, but never larger.
|
* amount, but never larger.
|
||||||
*/
|
*/
|
||||||
function getWithdrawDenomList(amountAvailable: AmountJson,
|
function getWithdrawDenomList(amountAvailable: AmountJson,
|
||||||
denoms: Denomination[]): Denomination[] {
|
denoms: Denomination[]): Denomination[] {
|
||||||
let remaining = Amounts.copy(amountAvailable);
|
let remaining = Amounts.copy(amountAvailable);
|
||||||
const ds: Denomination[] = [];
|
const ds: Denomination[] = [];
|
||||||
|
|
||||||
@ -331,9 +331,9 @@ export class Wallet {
|
|||||||
private runningOperations: Set<string> = new Set();
|
private runningOperations: Set<string> = new Set();
|
||||||
|
|
||||||
constructor(db: IDBDatabase,
|
constructor(db: IDBDatabase,
|
||||||
http: HttpRequestLibrary,
|
http: HttpRequestLibrary,
|
||||||
badge: Badge,
|
badge: Badge,
|
||||||
notifier: Notifier) {
|
notifier: Notifier) {
|
||||||
this.db = db;
|
this.db = db;
|
||||||
this.http = http;
|
this.http = http;
|
||||||
this.badge = badge;
|
this.badge = badge;
|
||||||
@ -363,9 +363,9 @@ export class Wallet {
|
|||||||
.iter("exchanges")
|
.iter("exchanges")
|
||||||
.reduce((exchange: IExchangeInfo) => {
|
.reduce((exchange: IExchangeInfo) => {
|
||||||
this.updateExchangeFromUrl(exchange.baseUrl)
|
this.updateExchangeFromUrl(exchange.baseUrl)
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.error("updating exchange failed", 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.
|
* but only if the sum the coins' remaining value exceeds the payment amount.
|
||||||
*/
|
*/
|
||||||
private async getPossibleExchangeCoins(paymentAmount: AmountJson,
|
private async getPossibleExchangeCoins(paymentAmount: AmountJson,
|
||||||
depositFeeLimit: AmountJson,
|
depositFeeLimit: AmountJson,
|
||||||
allowedExchanges: ExchangeHandle[]): Promise<ExchangeCoins> {
|
allowedExchanges: ExchangeHandle[]): Promise<ExchangeCoins> {
|
||||||
// Mapping from exchange base URL to list of coins together with their
|
// Mapping from exchange base URL to list of coins together with their
|
||||||
// denomination
|
// denomination
|
||||||
let m: ExchangeCoins = {};
|
let m: ExchangeCoins = {};
|
||||||
@ -411,9 +411,9 @@ export class Wallet {
|
|||||||
let coin: Coin = mc[1];
|
let coin: Coin = mc[1];
|
||||||
if (coin.suspended) {
|
if (coin.suspended) {
|
||||||
console.log("skipping suspended coin",
|
console.log("skipping suspended coin",
|
||||||
coin.denomPub,
|
coin.denomPub,
|
||||||
"from exchange",
|
"from exchange",
|
||||||
exchange.baseUrl);
|
exchange.baseUrl);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let denom = exchange.active_denoms.find((e) => e.denom_pub === coin.denomPub);
|
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");
|
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))
|
||||||
];
|
];
|
||||||
@ -467,38 +467,38 @@ export class Wallet {
|
|||||||
// under depositFeeLimit
|
// under depositFeeLimit
|
||||||
|
|
||||||
nextExchange:
|
nextExchange:
|
||||||
for (let key in m) {
|
for (let key in m) {
|
||||||
let coins = m[key];
|
let coins = m[key];
|
||||||
// Sort by ascending deposit fee
|
// Sort by ascending deposit fee
|
||||||
coins.sort((o1, o2) => Amounts.cmp(o1.denom.fee_deposit,
|
coins.sort((o1, o2) => Amounts.cmp(o1.denom.fee_deposit,
|
||||||
o2.denom.fee_deposit));
|
o2.denom.fee_deposit));
|
||||||
let maxFee = Amounts.copy(depositFeeLimit);
|
let maxFee = Amounts.copy(depositFeeLimit);
|
||||||
let minAmount = Amounts.copy(paymentAmount);
|
let minAmount = Amounts.copy(paymentAmount);
|
||||||
let accFee = Amounts.copy(coins[0].denom.fee_deposit);
|
let accFee = Amounts.copy(coins[0].denom.fee_deposit);
|
||||||
let accAmount = Amounts.getZero(coins[0].coin.currentAmount.currency);
|
let accAmount = Amounts.getZero(coins[0].coin.currentAmount.currency);
|
||||||
let usableCoins: CoinWithDenom[] = [];
|
let usableCoins: CoinWithDenom[] = [];
|
||||||
nextCoin:
|
nextCoin:
|
||||||
for (let i = 0; i < coins.length; i++) {
|
for (let i = 0; i < coins.length; i++) {
|
||||||
let coinAmount = Amounts.copy(coins[i].coin.currentAmount);
|
let coinAmount = Amounts.copy(coins[i].coin.currentAmount);
|
||||||
let coinFee = coins[i].denom.fee_deposit;
|
let coinFee = coins[i].denom.fee_deposit;
|
||||||
if (Amounts.cmp(coinAmount, coinFee) <= 0) {
|
if (Amounts.cmp(coinAmount, coinFee) <= 0) {
|
||||||
continue nextCoin;
|
continue nextCoin;
|
||||||
}
|
}
|
||||||
accFee = Amounts.add(accFee, coinFee).amount;
|
accFee = Amounts.add(accFee, coinFee).amount;
|
||||||
accAmount = Amounts.add(accAmount, coinAmount).amount;
|
accAmount = Amounts.add(accAmount, coinAmount).amount;
|
||||||
if (Amounts.cmp(accFee, maxFee) >= 0) {
|
if (Amounts.cmp(accFee, maxFee) >= 0) {
|
||||||
// FIXME: if the fees are too high, we have
|
// FIXME: if the fees are too high, we have
|
||||||
// to cover them ourselves ....
|
// to cover them ourselves ....
|
||||||
console.log("too much fees");
|
console.log("too much fees");
|
||||||
continue nextExchange;
|
continue nextExchange;
|
||||||
}
|
}
|
||||||
usableCoins.push(coins[i]);
|
usableCoins.push(coins[i]);
|
||||||
if (Amounts.cmp(accAmount, minAmount) >= 0) {
|
if (Amounts.cmp(accAmount, minAmount) >= 0) {
|
||||||
ret[key] = usableCoins;
|
ret[key] = usableCoins;
|
||||||
continue nextExchange;
|
continue nextExchange;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -508,8 +508,8 @@ export class Wallet {
|
|||||||
* pay for a contract in the wallet's database.
|
* pay for a contract in the wallet's database.
|
||||||
*/
|
*/
|
||||||
private async recordConfirmPay(offer: Offer,
|
private async recordConfirmPay(offer: Offer,
|
||||||
payCoinInfo: PayCoinInfo,
|
payCoinInfo: PayCoinInfo,
|
||||||
chosenExchange: string): Promise<void> {
|
chosenExchange: string): Promise<void> {
|
||||||
let payReq: any = {};
|
let payReq: any = {};
|
||||||
payReq["amount"] = offer.contract.amount;
|
payReq["amount"] = offer.contract.amount;
|
||||||
payReq["coins"] = payCoinInfo.map((x) => x.sig);
|
payReq["coins"] = payCoinInfo.map((x) => x.sig);
|
||||||
@ -571,8 +571,8 @@ export class Wallet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mcs = await this.getPossibleExchangeCoins(offer.contract.amount,
|
let mcs = await this.getPossibleExchangeCoins(offer.contract.amount,
|
||||||
offer.contract.max_fee,
|
offer.contract.max_fee,
|
||||||
offer.contract.exchanges);
|
offer.contract.exchanges);
|
||||||
|
|
||||||
if (Object.keys(mcs).length == 0) {
|
if (Object.keys(mcs).length == 0) {
|
||||||
console.log("not confirming payment, insufficient coins");
|
console.log("not confirming payment, insufficient coins");
|
||||||
@ -584,8 +584,8 @@ export class Wallet {
|
|||||||
|
|
||||||
let ds = await this.cryptoApi.signDeposit(offer, mcs[exchangeUrl]);
|
let ds = await this.cryptoApi.signDeposit(offer, mcs[exchangeUrl]);
|
||||||
await this.recordConfirmPay(offer,
|
await this.recordConfirmPay(offer,
|
||||||
ds,
|
ds,
|
||||||
exchangeUrl);
|
exchangeUrl);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -600,13 +600,13 @@ 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.
|
||||||
let mcs = await this.getPossibleExchangeCoins(offer.contract.amount,
|
let mcs = await this.getPossibleExchangeCoins(offer.contract.amount,
|
||||||
offer.contract.max_fee,
|
offer.contract.max_fee,
|
||||||
offer.contract.exchanges);
|
offer.contract.exchanges);
|
||||||
|
|
||||||
if (Object.keys(mcs).length == 0) {
|
if (Object.keys(mcs).length == 0) {
|
||||||
console.log("not confirming payment, insufficient coins");
|
console.log("not confirming payment, insufficient coins");
|
||||||
@ -614,7 +614,7 @@ export class Wallet {
|
|||||||
error: "coins-insufficient",
|
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.
|
* then deplete the reserve, withdrawing coins until it is empty.
|
||||||
*/
|
*/
|
||||||
private async processReserve(reserveRecord: ReserveRecord,
|
private async processReserve(reserveRecord: ReserveRecord,
|
||||||
retryDelayMs: number = 250): Promise<void> {
|
retryDelayMs: number = 250): Promise<void> {
|
||||||
const opId = "reserve-" + reserveRecord.reserve_pub;
|
const opId = "reserve-" + reserveRecord.reserve_pub;
|
||||||
this.startOperation(opId);
|
this.startOperation(opId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let exchange = await this.updateExchangeFromUrl(reserveRecord.exchange_base_url);
|
let exchange = await this.updateExchangeFromUrl(reserveRecord.exchange_base_url);
|
||||||
let reserve = await this.updateReserve(reserveRecord.reserve_pub,
|
let reserve = await this.updateReserve(reserveRecord.reserve_pub,
|
||||||
exchange);
|
exchange);
|
||||||
let n = await this.depleteReserve(reserve, exchange);
|
let n = await this.depleteReserve(reserve, exchange);
|
||||||
|
|
||||||
if (n != 0) {
|
if (n != 0) {
|
||||||
@ -672,10 +672,10 @@ export class Wallet {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
// random, exponential backoff truncated at 3 minutes
|
// random, exponential backoff truncated at 3 minutes
|
||||||
let nextDelay = Math.min(2 * retryDelayMs + retryDelayMs * Math.random(),
|
let nextDelay = Math.min(2 * retryDelayMs + retryDelayMs * Math.random(),
|
||||||
3000 * 60);
|
3000 * 60);
|
||||||
console.warn(`Failed to deplete reserve, trying again in ${retryDelayMs} ms`);
|
console.warn(`Failed to deplete reserve, trying again in ${retryDelayMs} ms`);
|
||||||
setTimeout(() => this.processReserve(reserveRecord, nextDelay),
|
setTimeout(() => this.processReserve(reserveRecord, nextDelay),
|
||||||
retryDelayMs);
|
retryDelayMs);
|
||||||
} finally {
|
} finally {
|
||||||
this.stopOperation(opId);
|
this.stopOperation(opId);
|
||||||
}
|
}
|
||||||
@ -683,18 +683,18 @@ export class Wallet {
|
|||||||
|
|
||||||
|
|
||||||
private async processPreCoin(preCoin: PreCoin,
|
private async processPreCoin(preCoin: PreCoin,
|
||||||
retryDelayMs = 100): Promise<void> {
|
retryDelayMs = 100): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const coin = await this.withdrawExecute(preCoin);
|
const coin = await this.withdrawExecute(preCoin);
|
||||||
this.storeCoin(coin);
|
this.storeCoin(coin);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to withdraw coin from precoin, retrying in",
|
console.error("Failed to withdraw coin from precoin, retrying in",
|
||||||
retryDelayMs,
|
retryDelayMs,
|
||||||
"ms", e);
|
"ms", e);
|
||||||
// exponential backoff truncated at one minute
|
// exponential backoff truncated at one minute
|
||||||
let nextRetryDelayMs = Math.min(retryDelayMs * 2, 1000 * 60);
|
let nextRetryDelayMs = Math.min(retryDelayMs * 2, 1000 * 60);
|
||||||
setTimeout(() => this.processPreCoin(preCoin, nextRetryDelayMs),
|
setTimeout(() => this.processPreCoin(preCoin, nextRetryDelayMs),
|
||||||
retryDelayMs);
|
retryDelayMs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -801,8 +801,8 @@ export class Wallet {
|
|||||||
}
|
}
|
||||||
let r = JSON.parse(resp.responseText);
|
let r = JSON.parse(resp.responseText);
|
||||||
let denomSig = await this.cryptoApi.rsaUnblind(r.ev_sig,
|
let denomSig = await this.cryptoApi.rsaUnblind(r.ev_sig,
|
||||||
pc.blindingKey,
|
pc.blindingKey,
|
||||||
pc.denomPub);
|
pc.denomPub);
|
||||||
let coin: Coin = {
|
let coin: Coin = {
|
||||||
coinPub: pc.coinPub,
|
coinPub: pc.coinPub,
|
||||||
coinPriv: pc.coinPriv,
|
coinPriv: pc.coinPriv,
|
||||||
@ -840,7 +840,7 @@ export class Wallet {
|
|||||||
private async withdraw(denom: Denomination, reserve: Reserve): Promise<void> {
|
private async withdraw(denom: Denomination, reserve: Reserve): Promise<void> {
|
||||||
console.log("creating pre coin at", new Date());
|
console.log("creating pre coin at", new Date());
|
||||||
let preCoin = await this.cryptoApi
|
let preCoin = await this.cryptoApi
|
||||||
.createPreCoin(denom, reserve);
|
.createPreCoin(denom, reserve);
|
||||||
await Query(this.db)
|
await Query(this.db)
|
||||||
.put("precoins", preCoin)
|
.put("precoins", preCoin)
|
||||||
.finish();
|
.finish();
|
||||||
@ -852,10 +852,10 @@ export class Wallet {
|
|||||||
* Withdraw coins from a reserve until it is empty.
|
* Withdraw coins from a reserve until it is empty.
|
||||||
*/
|
*/
|
||||||
private async depleteReserve(reserve: any,
|
private async depleteReserve(reserve: any,
|
||||||
exchange: IExchangeInfo): Promise<number> {
|
exchange: IExchangeInfo): Promise<number> {
|
||||||
let denomsAvailable: Denomination[] = copy(exchange.active_denoms);
|
let denomsAvailable: Denomination[] = copy(exchange.active_denoms);
|
||||||
let denomsForWithdraw = getWithdrawDenomList(reserve.current_amount,
|
let denomsForWithdraw = getWithdrawDenomList(reserve.current_amount,
|
||||||
denomsAvailable);
|
denomsAvailable);
|
||||||
|
|
||||||
let ps = denomsForWithdraw.map((denom) => this.withdraw(denom, reserve));
|
let ps = denomsForWithdraw.map((denom) => this.withdraw(denom, reserve));
|
||||||
await Promise.all(ps);
|
await Promise.all(ps);
|
||||||
@ -868,11 +868,11 @@ export class Wallet {
|
|||||||
* by quering the reserve's exchange.
|
* by quering the reserve's exchange.
|
||||||
*/
|
*/
|
||||||
private async updateReserve(reservePub: string,
|
private async updateReserve(reservePub: string,
|
||||||
exchange: IExchangeInfo): Promise<Reserve> {
|
exchange: IExchangeInfo): Promise<Reserve> {
|
||||||
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();
|
||||||
@ -922,18 +922,18 @@ export class Wallet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getReserveCreationInfo(baseUrl: string,
|
async getReserveCreationInfo(baseUrl: string,
|
||||||
amount: AmountJson): Promise<ReserveCreationInfo> {
|
amount: AmountJson): Promise<ReserveCreationInfo> {
|
||||||
let exchangeInfo = await this.updateExchangeFromUrl(baseUrl);
|
let exchangeInfo = await this.updateExchangeFromUrl(baseUrl);
|
||||||
|
|
||||||
let selectedDenoms = getWithdrawDenomList(amount,
|
let selectedDenoms = getWithdrawDenomList(amount,
|
||||||
exchangeInfo.active_denoms);
|
exchangeInfo.active_denoms);
|
||||||
let acc = Amounts.getZero(amount.currency);
|
let acc = Amounts.getZero(amount.currency);
|
||||||
for (let d of selectedDenoms) {
|
for (let d of selectedDenoms) {
|
||||||
acc = Amounts.add(acc, d.fee_withdraw).amount;
|
acc = Amounts.add(acc, d.fee_withdraw).amount;
|
||||||
}
|
}
|
||||||
let actualCoinCost = selectedDenoms
|
let actualCoinCost = selectedDenoms
|
||||||
.map((d: Denomination) => Amounts.add(d.value,
|
.map((d: Denomination) => Amounts.add(d.value,
|
||||||
d.fee_withdraw).amount)
|
d.fee_withdraw).amount)
|
||||||
.reduce((a, b) => Amounts.add(a, b).amount);
|
.reduce((a, b) => Amounts.add(a, b).amount);
|
||||||
|
|
||||||
let wireInfo = await this.getWireInfo(baseUrl);
|
let wireInfo = await this.getWireInfo(baseUrl);
|
||||||
@ -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]);
|
||||||
@ -987,7 +987,7 @@ export class Wallet {
|
|||||||
|
|
||||||
|
|
||||||
private async updateExchangeFromJson(baseUrl: string,
|
private async updateExchangeFromJson(baseUrl: string,
|
||||||
exchangeKeysJson: KeysJson): Promise<IExchangeInfo> {
|
exchangeKeysJson: KeysJson): Promise<IExchangeInfo> {
|
||||||
const updateTimeSec = getTalerStampSec(exchangeKeysJson.list_issue_date);
|
const updateTimeSec = getTalerStampSec(exchangeKeysJson.list_issue_date);
|
||||||
if (updateTimeSec === null) {
|
if (updateTimeSec === null) {
|
||||||
throw Error("invalid update time");
|
throw Error("invalid update time");
|
||||||
@ -1016,7 +1016,7 @@ export class Wallet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let updatedExchangeInfo = await this.updateExchangeInfo(exchangeInfo,
|
let updatedExchangeInfo = await this.updateExchangeInfo(exchangeInfo,
|
||||||
exchangeKeysJson);
|
exchangeKeysJson);
|
||||||
await this.suspendCoins(updatedExchangeInfo);
|
await this.suspendCoins(updatedExchangeInfo);
|
||||||
|
|
||||||
await Query(this.db)
|
await Query(this.db)
|
||||||
@ -1028,7 +1028,7 @@ export class Wallet {
|
|||||||
|
|
||||||
|
|
||||||
private async updateExchangeInfo(exchangeInfo: IExchangeInfo,
|
private async updateExchangeInfo(exchangeInfo: IExchangeInfo,
|
||||||
newKeys: KeysJson): Promise<IExchangeInfo> {
|
newKeys: KeysJson): Promise<IExchangeInfo> {
|
||||||
if (exchangeInfo.masterPublicKey != newKeys.master_public_key) {
|
if (exchangeInfo.masterPublicKey != newKeys.master_public_key) {
|
||||||
throw Error("public keys do not match");
|
throw Error("public keys do not match");
|
||||||
}
|
}
|
||||||
@ -1064,15 +1064,15 @@ 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);
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
console.error("invalid denomination",
|
console.error("invalid denomination",
|
||||||
denom,
|
denom,
|
||||||
"with key",
|
"with key",
|
||||||
exchangeInfo.masterPublicKey);
|
exchangeInfo.masterPublicKey);
|
||||||
// FIXME: report to auditors
|
// FIXME: report to auditors
|
||||||
}
|
}
|
||||||
exchangeInfo.active_denoms.push(denom);
|
exchangeInfo.active_denoms.push(denom);
|
||||||
@ -1099,7 +1099,7 @@ export class Wallet {
|
|||||||
acc = Amounts.getZero(c.currentAmount.currency);
|
acc = Amounts.getZero(c.currentAmount.currency);
|
||||||
}
|
}
|
||||||
byCurrency[c.currentAmount.currency] = Amounts.add(c.currentAmount,
|
byCurrency[c.currentAmount.currency] = Amounts.add(c.currentAmount,
|
||||||
acc).amount;
|
acc).amount;
|
||||||
return byCurrency;
|
return byCurrency;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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,12 +1167,12 @@ 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",
|
||||||
"repurchase",
|
"repurchase",
|
||||||
[contract.merchant_pub, contract.repurchase_correlation_id]);
|
[contract.merchant_pub, contract.repurchase_correlation_id]);
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
console.assert(result.contract.repurchase_correlation_id == contract.repurchase_correlation_id);
|
console.assert(result.contract.repurchase_correlation_id == contract.repurchase_correlation_id);
|
||||||
@ -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 };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.
|
||||||
@ -24,8 +30,8 @@ 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 });
|
||||||
|
}
|
@ -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);
|
||||||
@ -104,10 +104,10 @@ function makeHandlers(db: IDBDatabase,
|
|||||||
if (e instanceof Checkable.SchemaError) {
|
if (e instanceof Checkable.SchemaError) {
|
||||||
console.error("schema error:", e.message);
|
console.error("schema error:", e.message);
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
error: "invalid contract",
|
error: "invalid contract",
|
||||||
hint: e.message,
|
hint: e.message,
|
||||||
detail: detail
|
detail: detail
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
@ -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);
|
||||||
@ -123,22 +123,22 @@ function makeHandlers(db: IDBDatabase,
|
|||||||
if (e instanceof Checkable.SchemaError) {
|
if (e instanceof Checkable.SchemaError) {
|
||||||
console.error("schema error:", e.message);
|
console.error("schema error:", e.message);
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
error: "invalid contract",
|
error: "invalid contract",
|
||||||
hint: e.message,
|
hint: e.message,
|
||||||
detail: detail
|
detail: detail
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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) {
|
||||||
console.warn("rate limit for execute payment exceeded");
|
console.warn("rate limit for execute payment exceeded");
|
||||||
let msg = {
|
let msg = {
|
||||||
error: "rate limit exceeded for execute-payment",
|
error: "rate limit exceeded for execute-payment",
|
||||||
rateLimitExceeded: true,
|
rateLimitExceeded: true,
|
||||||
hint: "Check for redirect loops",
|
hint: "Check for redirect loops",
|
||||||
@ -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.
|
||||||
@ -216,10 +237,10 @@ function dispatch(handlers: any, req: any, sender: any, sendResponse: any) {
|
|||||||
console.error(e);
|
console.error(e);
|
||||||
try {
|
try {
|
||||||
sendResponse({
|
sendResponse({
|
||||||
error: "exception",
|
error: "exception",
|
||||||
hint: e.message,
|
hint: e.message,
|
||||||
stack: e.stack.toString()
|
stack: e.stack.toString()
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// might fail if tab disconnected
|
// might fail if tab disconnected
|
||||||
@ -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" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -345,51 +366,51 @@ export function wxMain() {
|
|||||||
chrome.extension.getBackgroundPage().setInterval(clearRateLimitCache, 5000);
|
chrome.extension.getBackgroundPage().setInterval(clearRateLimitCache, 5000);
|
||||||
|
|
||||||
Promise.resolve()
|
Promise.resolve()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return openTalerDb();
|
return openTalerDb();
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.error("could not open database");
|
console.error("could not open database");
|
||||||
console.error(e);
|
console.error(e);
|
||||||
})
|
})
|
||||||
.then((db: IDBDatabase) => {
|
.then((db: IDBDatabase) => {
|
||||||
let http = new BrowserHttpLib();
|
let http = new BrowserHttpLib();
|
||||||
let notifier = new ChromeNotifier();
|
let notifier = new ChromeNotifier();
|
||||||
console.log("setting wallet");
|
console.log("setting wallet");
|
||||||
wallet = new Wallet(db, http, badge!, notifier);
|
wallet = new Wallet(db, http, badge!, notifier);
|
||||||
|
|
||||||
// Handlers for messages coming directly from the content
|
// Handlers for messages coming directly from the content
|
||||||
// script on the page
|
// script on the page
|
||||||
let handlers = makeHandlers(db, wallet!);
|
let handlers = makeHandlers(db, wallet!);
|
||||||
chrome.runtime.onMessage.addListener((req, sender, sendResponse) => {
|
chrome.runtime.onMessage.addListener((req, sender, sendResponse) => {
|
||||||
try {
|
try {
|
||||||
return dispatch(handlers, req, sender, sendResponse)
|
return dispatch(handlers, req, sender, sendResponse)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(`exception during wallet handler (dispatch)`);
|
console.log(`exception during wallet handler (dispatch)`);
|
||||||
console.log("request", req);
|
console.log("request", req);
|
||||||
console.error(e);
|
console.error(e);
|
||||||
sendResponse({
|
sendResponse({
|
||||||
error: "exception",
|
error: "exception",
|
||||||
hint: e.message,
|
hint: e.message,
|
||||||
stack: e.stack.toString()
|
stack: e.stack.toString()
|
||||||
});
|
});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handlers for catching HTTP requests
|
// Handlers for catching HTTP requests
|
||||||
chrome.webRequest.onHeadersReceived.addListener((details) => {
|
chrome.webRequest.onHeadersReceived.addListener((details) => {
|
||||||
if (details.statusCode != 402) {
|
if (details.statusCode != 402) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log(`got 402 from ${details.url}`);
|
console.log(`got 402 from ${details.url}`);
|
||||||
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");
|
||||||
console.error(e);
|
console.error(e);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -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
32
pages/tree.html
Normal 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
305
pages/tree.tsx
Normal 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);
|
||||||
|
}
|
@ -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:
|
||||||
|
@ -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"
|
||||||
]
|
]
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user