implement new reserve creation dialog and auditor management
This commit is contained in:
parent
ce97b1076b
commit
d6bf24902a
2
node_modules/.yarn-integrity
generated
vendored
2
node_modules/.yarn-integrity
generated
vendored
@ -1 +1 @@
|
||||
751d3ff225403bea12799f2c0ad32d26a0ff81a4f88821c8f1615d3ddc5a9533
|
||||
f57c90a35fd7bae0b594a5c9114779b9b7c1629f6977a421d3e666087dc7ed0f
|
@ -14,6 +14,7 @@
|
||||
"author": "",
|
||||
"license": "GPL-3.0",
|
||||
"devDependencies": {
|
||||
"@types/moment": "^2.13.0",
|
||||
"@types/react": "^15.0.22",
|
||||
"@types/react-dom": "^15.5.0",
|
||||
"async": "^2.1.2",
|
||||
@ -39,6 +40,7 @@
|
||||
"map-stream": "0.0.6",
|
||||
"minimist": "^1.2.0",
|
||||
"mocha": "^2.4.5",
|
||||
"moment": "^2.18.1",
|
||||
"po2json": "git+https://github.com/mikeedwards/po2json",
|
||||
"react": "^15.5.4",
|
||||
"react-dom": "^15.5.4",
|
||||
@ -50,11 +52,11 @@
|
||||
"ts-loader": "^2.0.3",
|
||||
"typescript": "next",
|
||||
"typhonjs-istanbul-instrument-jspm": "^0.1.0",
|
||||
"uglify-js": "^2.8.22",
|
||||
"urijs": "^1.18.10",
|
||||
"vinyl": "^2.0.0",
|
||||
"vinyl-fs": "^2.4.3",
|
||||
"webpack": "^2.4.1",
|
||||
"webpack-merge": "^4.1.0",
|
||||
"uglify-js": "^2.8.22"
|
||||
"webpack-merge": "^4.1.0"
|
||||
}
|
||||
}
|
||||
|
@ -33,12 +33,23 @@ export interface StateHolder<T> {
|
||||
* but has multiple state holders.
|
||||
*/
|
||||
export abstract class ImplicitStateComponent<PropType> extends React.Component<PropType, any> {
|
||||
_implicit = {needsUpdate: false, didMount: false};
|
||||
componentDidMount() {
|
||||
this._implicit.didMount = true;
|
||||
if (this._implicit.needsUpdate) {
|
||||
this.setState({} as any);
|
||||
}
|
||||
}
|
||||
makeState<StateType>(initial: StateType): StateHolder<StateType> {
|
||||
let state: StateType = initial;
|
||||
return (s?: StateType): StateType => {
|
||||
if (s !== undefined) {
|
||||
state = s;
|
||||
this.setState({} as any);
|
||||
if (this._implicit.didMount) {
|
||||
this.setState({} as any);
|
||||
} else {
|
||||
this._implicit.needsUpdate = true;
|
||||
}
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
@ -177,11 +177,13 @@ interface TranslateProps {
|
||||
export class Translate extends React.Component<TranslateProps,void> {
|
||||
render(): JSX.Element {
|
||||
let s = stringifyChildren(this.props.children);
|
||||
console.log(`string "${s}"`);
|
||||
let tr = jed.ngettext(s, s, 1).split(/%(\d+)\$s/).filter((e: any, i: number) => i % 2 == 0);
|
||||
console.log(`tr "${JSON.stringify(tr)}"`);
|
||||
let childArray = React.Children.toArray(this.props.children!);
|
||||
for (let i = 0; i < childArray.length - 1; ++i) {
|
||||
if ((typeof childArray[i]) == "string" && (typeof childArray[i+1]) == "string") {
|
||||
childArray[i+i] = childArray[i] as string + childArray[i+1] as string;
|
||||
childArray[i+1] = (childArray[i] as string).concat(childArray[i+1] as string);
|
||||
childArray.splice(i,1);
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,9 @@
|
||||
<script src="/dist/auditors-bundle.js"></script>
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-size: 100%;
|
||||
}
|
||||
.tree-item {
|
||||
margin: 2em;
|
||||
border-radius: 5px;
|
||||
|
@ -23,6 +23,7 @@
|
||||
|
||||
import {
|
||||
ExchangeRecord,
|
||||
ExchangeForCurrencyRecord,
|
||||
DenominationRecord,
|
||||
AuditorRecord,
|
||||
CurrencyRecord,
|
||||
@ -65,13 +66,20 @@ class CurrencyList extends React.Component<any, CurrencyListState> {
|
||||
this.setState({ currencies });
|
||||
}
|
||||
|
||||
async confirmRemove(c: CurrencyRecord, a: AuditorRecord) {
|
||||
async confirmRemoveAuditor(c: CurrencyRecord, a: AuditorRecord) {
|
||||
if (window.confirm(`Do you really want to remove auditor ${a.baseUrl} for currency ${c.name}?`)) {
|
||||
c.auditors = c.auditors.filter((x) => x.auditorPub != a.auditorPub);
|
||||
await updateCurrency(c);
|
||||
}
|
||||
}
|
||||
|
||||
async confirmRemoveExchange(c: CurrencyRecord, e: ExchangeForCurrencyRecord) {
|
||||
if (window.confirm(`Do you really want to remove exchange ${e.baseUrl} for currency ${c.name}?`)) {
|
||||
c.exchanges = c.exchanges.filter((x) => x.baseUrl != e.baseUrl);
|
||||
await updateCurrency(c);
|
||||
}
|
||||
}
|
||||
|
||||
renderAuditors(c: CurrencyRecord): any {
|
||||
if (c.auditors.length == 0) {
|
||||
return <p>No trusted auditors for this currency.</p>
|
||||
@ -81,7 +89,7 @@ class CurrencyList extends React.Component<any, CurrencyListState> {
|
||||
<p>Trusted Auditors:</p>
|
||||
<ul>
|
||||
{c.auditors.map(a => (
|
||||
<li>{a.baseUrl} <button className="pure-button button-destructive" onClick={() => this.confirmRemove(c, a)}>Remove</button>
|
||||
<li>{a.baseUrl} <button className="pure-button button-destructive" onClick={() => this.confirmRemoveAuditor(c, a)}>Remove</button>
|
||||
<ul>
|
||||
<li>valid until {new Date(a.expirationStamp).toString()}</li>
|
||||
<li>public key {a.auditorPub}</li>
|
||||
@ -93,6 +101,23 @@ class CurrencyList extends React.Component<any, CurrencyListState> {
|
||||
);
|
||||
}
|
||||
|
||||
renderExchanges(c: CurrencyRecord): any {
|
||||
if (c.exchanges.length == 0) {
|
||||
return <p>No trusted exchanges for this currency.</p>
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<p>Trusted Exchanges:</p>
|
||||
<ul>
|
||||
{c.exchanges.map(e => (
|
||||
<li>{e.baseUrl} <button className="pure-button button-destructive" onClick={() => this.confirmRemoveExchange(c, e)}>Remove</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
let currencies = this.state.currencies;
|
||||
if (!currencies) {
|
||||
@ -104,7 +129,10 @@ class CurrencyList extends React.Component<any, CurrencyListState> {
|
||||
<div>
|
||||
<h1>Currency {c.name}</h1>
|
||||
<p>Displayed with {c.fractionalDigits} fractional digits.</p>
|
||||
<h2>Auditors</h2>
|
||||
<div>{this.renderAuditors(c)}</div>
|
||||
<h2>Exchanges</h2>
|
||||
<div>{this.renderExchanges(c)}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
@ -7,10 +7,39 @@
|
||||
|
||||
<link rel="icon" href="/img/icon.png">
|
||||
<link rel="stylesheet" type="text/css" href="/src/style/wallet.css">
|
||||
<link rel="stylesheet" type="text/css" href="/src/style/pure.css">
|
||||
|
||||
<script src="/dist/page-common-bundle.js"></script>
|
||||
<script src="/dist/confirm-create-reserve-bundle.js"></script>
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-size: 100%;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
.button-success {
|
||||
background: rgb(28, 184, 65); /* this is a green */
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
.button-secondary {
|
||||
background: rgb(66, 184, 221); /* this is a light blue */
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
a.opener {
|
||||
color: black;
|
||||
}
|
||||
.opener-open::before {
|
||||
content: "\25bc"
|
||||
}
|
||||
.opener-collapsed::before {
|
||||
content: "\25b6 "
|
||||
}
|
||||
</style>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
@ -26,14 +26,15 @@ import {amountToPretty, canonicalizeBaseUrl} from "../helpers";
|
||||
import {
|
||||
AmountJson, CreateReserveResponse,
|
||||
ReserveCreationInfo, Amounts,
|
||||
Denomination, DenominationRecord,
|
||||
Denomination, DenominationRecord, CurrencyRecord
|
||||
} from "../types";
|
||||
import {getReserveCreationInfo} from "../wxApi";
|
||||
import {getReserveCreationInfo, getCurrency, getExchangeInfo} from "../wxApi";
|
||||
import {ImplicitStateComponent, StateHolder} from "../components";
|
||||
import * as i18n from "../i18n";
|
||||
import * as React from "react";
|
||||
import * as ReactDOM from "react-dom";
|
||||
import URI = require("urijs");
|
||||
import * as moment from "moment";
|
||||
|
||||
|
||||
function delay<T>(delayMs: number, value: T): Promise<T> {
|
||||
@ -67,10 +68,72 @@ class EventTrigger {
|
||||
}
|
||||
|
||||
|
||||
interface CollapsibleState {
|
||||
collapsed: boolean;
|
||||
}
|
||||
|
||||
interface CollapsibleProps {
|
||||
initiallyCollapsed: boolean;
|
||||
title: string;
|
||||
}
|
||||
|
||||
class Collapsible extends React.Component<CollapsibleProps, CollapsibleState> {
|
||||
constructor(props: CollapsibleProps) {
|
||||
super(props);
|
||||
this.state = { collapsed: props.initiallyCollapsed };
|
||||
}
|
||||
render() {
|
||||
const doOpen = (e: any) => {
|
||||
this.setState({collapsed: false})
|
||||
e.preventDefault()
|
||||
};
|
||||
const doClose = (e: any) => {
|
||||
this.setState({collapsed: true})
|
||||
e.preventDefault();
|
||||
};
|
||||
if (this.state.collapsed) {
|
||||
return <h2><a className="opener opener-collapsed" href="#" onClick={doOpen}>{this.props.title}</a></h2>;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<h2><a className="opener opener-open" href="#" onClick={doClose}>{this.props.title}</a></h2>
|
||||
{this.props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function renderAuditorDetails(rci: ReserveCreationInfo|null) {
|
||||
if (!rci) {
|
||||
return (
|
||||
<p>
|
||||
Details will be displayed when a valid exchange provider URL is entered.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
if (rci.exchangeInfo.auditors.length == 0) {
|
||||
return (
|
||||
<p>
|
||||
The exchange is not audited by any auditors.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
{rci.exchangeInfo.auditors.map(a => (
|
||||
<h3>Auditor {a.url}</h3>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderReserveCreationDetails(rci: ReserveCreationInfo|null) {
|
||||
if (!rci) {
|
||||
return <p>
|
||||
Details will be displayed when a valid exchange provider URL is entered.</p>
|
||||
return (
|
||||
<p>
|
||||
Details will be displayed when a valid exchange provider URL is entered.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
let denoms = rci.selectedDenoms;
|
||||
@ -99,25 +162,57 @@ function renderReserveCreationDetails(rci: ReserveCreationInfo|null) {
|
||||
);
|
||||
}
|
||||
|
||||
function wireFee(s: string) {
|
||||
return [
|
||||
<thead>
|
||||
<tr>
|
||||
<th colSpan={3}>Wire Method {s}</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Applies Until</th>
|
||||
<th>Wire Fee</th>
|
||||
<th>Closing Fee</th>
|
||||
</tr>
|
||||
</thead>,
|
||||
<tbody>
|
||||
{rci!.wireFees.feesForType[s].map(f => (
|
||||
<tr>
|
||||
<td>{moment.unix(f.endStamp).format("llll")}</td>
|
||||
<td>{amountToPretty(f.wireFee)}</td>
|
||||
<td>{amountToPretty(f.closingFee)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
];
|
||||
}
|
||||
|
||||
let withdrawFeeStr = amountToPretty(rci.withdrawFee);
|
||||
let overheadStr = amountToPretty(rci.overhead);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3>Overview</h3>
|
||||
<p>{i18n.str`Withdrawal fees: ${withdrawFeeStr}`}</p>
|
||||
<p>{i18n.str`Rounding loss: ${overheadStr}`}</p>
|
||||
<table>
|
||||
<h3>Coin Fees</h3>
|
||||
<table className="pure-table">
|
||||
<thead>
|
||||
<th>{i18n.str`# Coins`}</th>
|
||||
<th>{i18n.str`Value`}</th>
|
||||
<th>{i18n.str`Withdraw Fee`}</th>
|
||||
<th>{i18n.str`Refresh Fee`}</th>
|
||||
<th>{i18n.str`Deposit Fee`}</th>
|
||||
<tr>
|
||||
<th>{i18n.str`# Coins`}</th>
|
||||
<th>{i18n.str`Value`}</th>
|
||||
<th>{i18n.str`Withdraw Fee`}</th>
|
||||
<th>{i18n.str`Refresh Fee`}</th>
|
||||
<th>{i18n.str`Deposit Fee`}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{uniq.map(row)}
|
||||
</tbody>
|
||||
</table>
|
||||
<h3>Wire Fees</h3>
|
||||
<table className="pure-table">
|
||||
{Object.keys(rci.wireFees.feesForType).map(wireFee)}
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -156,6 +251,87 @@ interface ExchangeSelectionProps {
|
||||
amount: AmountJson;
|
||||
callback_url: string;
|
||||
wt_types: string[];
|
||||
currencyRecord: CurrencyRecord|null;
|
||||
}
|
||||
|
||||
interface ManualSelectionProps {
|
||||
onSelect(url: string): void;
|
||||
initialUrl: string;
|
||||
}
|
||||
|
||||
class ManualSelection extends ImplicitStateComponent<ManualSelectionProps> {
|
||||
url: StateHolder<string> = this.makeState("");
|
||||
errorMessage: StateHolder<string|null> = this.makeState(null);
|
||||
isOkay: StateHolder<boolean> = this.makeState(false);
|
||||
updateEvent = new EventTrigger();
|
||||
constructor(p: ManualSelectionProps) {
|
||||
super(p);
|
||||
this.url(p.initialUrl);
|
||||
this.update();
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div className="pure-g pure-form pure-form-stacked">
|
||||
<div className="pure-u-1">
|
||||
<label>URL</label>
|
||||
<input className="url" type="text" spellCheck={false}
|
||||
value={this.url()}
|
||||
key="exchange-url-input"
|
||||
onInput={(e) => this.onUrlChanged((e.target as HTMLInputElement).value)} />
|
||||
</div>
|
||||
<div className="pure-u-1">
|
||||
<button className="pure-button button-success"
|
||||
disabled={!this.isOkay()}
|
||||
onClick={() => this.props.onSelect(this.url())}>
|
||||
{i18n.str`Select`}
|
||||
</button>
|
||||
{this.errorMessage()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
async update() {
|
||||
this.errorMessage(null);
|
||||
this.isOkay(false);
|
||||
if (!this.url()) {
|
||||
return;
|
||||
}
|
||||
let parsedUrl = new URI(this.url()!);
|
||||
if (parsedUrl.is("relative")) {
|
||||
this.errorMessage(i18n.str`Error: URL may not be relative`);
|
||||
this.isOkay(false);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
let url = canonicalizeBaseUrl(this.url()!);
|
||||
let r = await getExchangeInfo(url)
|
||||
console.log("getExchangeInfo returned")
|
||||
this.isOkay(true);
|
||||
} catch (e) {
|
||||
console.log("got error", e);
|
||||
if (e.hasOwnProperty("httpStatus")) {
|
||||
this.errorMessage(`Error: request failed with status ${e.httpStatus}`);
|
||||
} else if (e.hasOwnProperty("errorResponse")) {
|
||||
let resp = e.errorResponse;
|
||||
this.errorMessage(`Error: ${resp.error} (${resp.hint})`);
|
||||
} else {
|
||||
this.errorMessage("invalid exchange URL");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async onUrlChanged(s: string) {
|
||||
this.url(s);
|
||||
this.errorMessage(null);
|
||||
this.isOkay(false);
|
||||
this.updateEvent.trigger();
|
||||
let waited = await this.updateEvent.wait(200);
|
||||
if (waited) {
|
||||
// Run the actual update if nobody else preempted us.
|
||||
this.update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -164,60 +340,64 @@ class ExchangeSelection extends ImplicitStateComponent<ExchangeSelectionProps> {
|
||||
reserveCreationInfo: StateHolder<ReserveCreationInfo|null> = this.makeState(
|
||||
null);
|
||||
url: StateHolder<string|null> = this.makeState(null);
|
||||
detailCollapsed: StateHolder<boolean> = this.makeState(true);
|
||||
|
||||
updateEvent = new EventTrigger();
|
||||
selectingExchange: StateHolder<boolean> = this.makeState(false);
|
||||
|
||||
constructor(props: ExchangeSelectionProps) {
|
||||
super(props);
|
||||
this.onUrlChanged(props.suggestedExchangeUrl || null);
|
||||
this.forceReserveUpdate();
|
||||
}
|
||||
|
||||
|
||||
renderAdvanced(): JSX.Element {
|
||||
if (this.detailCollapsed() && this.url() !== null && !this.statusString()) {
|
||||
return (
|
||||
<button className="linky"
|
||||
onClick={() => this.detailCollapsed(false)}>
|
||||
{i18n.str`view fee structure / select different exchange provider`}
|
||||
</button>
|
||||
);
|
||||
let prefilledExchangesUrls = [];
|
||||
if (props.currencyRecord) {
|
||||
let exchanges = props.currencyRecord.exchanges.map((x) => x.baseUrl);
|
||||
prefilledExchangesUrls.push(...exchanges);
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<h2>Provider Selection</h2>
|
||||
<label>URL: </label>
|
||||
<input className="url" type="text" spellCheck={false}
|
||||
value={this.url()!}
|
||||
key="exchange-url-input"
|
||||
onInput={(e) => this.onUrlChanged((e.target as HTMLInputElement).value)}/>
|
||||
<br />
|
||||
{this.renderStatus()}
|
||||
<h2>{i18n.str`Detailed Fee Structure`}</h2>
|
||||
{renderReserveCreationDetails(this.reserveCreationInfo())}
|
||||
</div>)
|
||||
}
|
||||
|
||||
renderFee() {
|
||||
if (!this.reserveCreationInfo()) {
|
||||
return "??";
|
||||
if (props.suggestedExchangeUrl) {
|
||||
prefilledExchangesUrls.push(props.suggestedExchangeUrl);
|
||||
}
|
||||
if (prefilledExchangesUrls.length != 0) {
|
||||
this.url(prefilledExchangesUrls[0]);
|
||||
this.forceReserveUpdate();
|
||||
} else {
|
||||
this.selectingExchange(true);
|
||||
}
|
||||
let rci = this.reserveCreationInfo()!;
|
||||
let totalCost = Amounts.add(rci.overhead, rci.withdrawFee).amount;
|
||||
return `${amountToPretty(totalCost)}`;
|
||||
}
|
||||
|
||||
renderFeeStatus() {
|
||||
if (this.reserveCreationInfo()) {
|
||||
let rci = this.reserveCreationInfo();
|
||||
if (rci) {
|
||||
let totalCost = Amounts.add(rci.overhead, rci.withdrawFee).amount;
|
||||
let trustMessage;
|
||||
if (rci.isTrusted) {
|
||||
trustMessage = (
|
||||
<i18n.Translate wrap="p">
|
||||
The exchange is trusted by the wallet.
|
||||
</i18n.Translate>
|
||||
);
|
||||
} else if (rci.isAudited) {
|
||||
trustMessage = (
|
||||
<i18n.Translate wrap="p">
|
||||
The exchange is audited by a trusted auditor.
|
||||
</i18n.Translate>
|
||||
);
|
||||
} else {
|
||||
trustMessage = (
|
||||
<i18n.Translate wrap="p">
|
||||
Warning: The exchange is neither directly trusted nor audited by a trusted auditor.
|
||||
If you withdraw from this exchange, it will be trusted in the future.
|
||||
</i18n.Translate>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<i18n.Translate wrap="p">
|
||||
Using exchange provider <strong>{this.url()}</strong>.
|
||||
The exchange provider will charge
|
||||
{" "}
|
||||
<span>{this.renderFee()}</span>
|
||||
<span>{amountToPretty(totalCost)}</span>
|
||||
{" "}
|
||||
in fees.
|
||||
</i18n.Translate>
|
||||
{trustMessage}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (this.url() && !this.statusString()) {
|
||||
@ -233,7 +413,7 @@ class ExchangeSelection extends ImplicitStateComponent<ExchangeSelectionProps> {
|
||||
if (this.statusString()) {
|
||||
return (
|
||||
<p>
|
||||
<strong style={{color: "red"}}>{i18n.str`A problem occured, see below.`}</strong>
|
||||
<strong style={{color: "red"}}>{i18n.str`A problem occured, see below. ${this.statusString()}`}</strong>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
@ -244,6 +424,71 @@ class ExchangeSelection extends ImplicitStateComponent<ExchangeSelectionProps> {
|
||||
);
|
||||
}
|
||||
|
||||
renderConfirm() {
|
||||
return (
|
||||
<div>
|
||||
{this.renderFeeStatus()}
|
||||
<button className="pure-button button-success"
|
||||
disabled={this.reserveCreationInfo() == null}
|
||||
onClick={() => this.confirmReserve()}>
|
||||
{i18n.str`Accept fees and withdraw`}
|
||||
</button>
|
||||
{ " " }
|
||||
<button className="pure-button button-secondary"
|
||||
onClick={() => this.selectingExchange(true)}>
|
||||
{i18n.str`Change Exchange Provider`}
|
||||
</button>
|
||||
<br/>
|
||||
<Collapsible initiallyCollapsed={true} title="Fee Details">
|
||||
{renderReserveCreationDetails(this.reserveCreationInfo())}
|
||||
</Collapsible>
|
||||
<Collapsible initiallyCollapsed={true} title="Auditor Details">
|
||||
{renderAuditorDetails(this.reserveCreationInfo())}
|
||||
</Collapsible>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
select(url: string) {
|
||||
this.reserveCreationInfo(null);
|
||||
this.url(url);
|
||||
this.selectingExchange(false);
|
||||
this.forceReserveUpdate();
|
||||
}
|
||||
|
||||
renderSelect() {
|
||||
let exchanges = (this.props.currencyRecord && this.props.currencyRecord.exchanges) || [];
|
||||
console.log(exchanges);
|
||||
return (
|
||||
<div>
|
||||
Please select an exchange. You can review the details before after your selection.
|
||||
|
||||
{this.props.suggestedExchangeUrl && (
|
||||
<div>
|
||||
<h2>Bank Suggestion</h2>
|
||||
<button className="pure-button button-success" onClick={() => this.select(this.props.suggestedExchangeUrl)}>
|
||||
Select <strong>{this.props.suggestedExchangeUrl}</strong>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{exchanges.length > 0 && (
|
||||
<div>
|
||||
<h2>Known Exchanges</h2>
|
||||
{exchanges.map(e => (
|
||||
<button className="pure-button button-success" onClick={() => this.select(e.baseUrl)}>
|
||||
Select <strong>{e.baseUrl}</strong>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<h2>Manual Selection</h2>
|
||||
<ManualSelection initialUrl={this.url() || ""} onSelect={(url: string) => this.select(url)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
return (
|
||||
<div>
|
||||
@ -252,14 +497,7 @@ class ExchangeSelection extends ImplicitStateComponent<ExchangeSelectionProps> {
|
||||
<strong>{amountToPretty(this.props.amount)}</strong>
|
||||
{" from your bank account into your wallet."}
|
||||
</i18n.Translate>
|
||||
{this.renderFeeStatus()}
|
||||
<button className="accept"
|
||||
disabled={this.reserveCreationInfo() == null}
|
||||
onClick={() => this.confirmReserve()}>
|
||||
{i18n.str`Accept fees and withdraw`}
|
||||
</button>
|
||||
<br/>
|
||||
{this.renderAdvanced()}
|
||||
{this.selectingExchange() ? this.renderSelect() : this.renderConfirm()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -277,20 +515,6 @@ class ExchangeSelection extends ImplicitStateComponent<ExchangeSelectionProps> {
|
||||
*/
|
||||
async forceReserveUpdate() {
|
||||
this.reserveCreationInfo(null);
|
||||
if (!this.url()) {
|
||||
this.statusString(i18n.str`Error: URL is empty`);
|
||||
this.detailCollapsed(false);
|
||||
return;
|
||||
}
|
||||
|
||||
this.statusString(null);
|
||||
let parsedUrl = new URI(this.url()!);
|
||||
if (parsedUrl.is("relative")) {
|
||||
this.statusString(i18n.str`Error: URL may not be relative`);
|
||||
this.detailCollapsed(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
let url = canonicalizeBaseUrl(this.url()!);
|
||||
let r = await getReserveCreationInfo(url,
|
||||
@ -299,23 +523,16 @@ class ExchangeSelection extends ImplicitStateComponent<ExchangeSelectionProps> {
|
||||
this.reserveCreationInfo(r);
|
||||
console.dir(r);
|
||||
} catch (e) {
|
||||
console.log("get exchange info rejected");
|
||||
console.log("get exchange info rejected", e);
|
||||
if (e.hasOwnProperty("httpStatus")) {
|
||||
this.statusString(`Error: request failed with status ${e.httpStatus}`);
|
||||
this.detailCollapsed(false);
|
||||
} else if (e.hasOwnProperty("errorResponse")) {
|
||||
let resp = e.errorResponse;
|
||||
this.statusString(`Error: ${resp.error} (${resp.hint})`);
|
||||
this.detailCollapsed(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.statusString(null);
|
||||
this.reserveCreationInfo(null);
|
||||
}
|
||||
|
||||
confirmReserveImpl(rci: ReserveCreationInfo,
|
||||
exchange: string,
|
||||
amount: AmountJson,
|
||||
@ -358,30 +575,13 @@ class ExchangeSelection extends ImplicitStateComponent<ExchangeSelectionProps> {
|
||||
console.log("going to", url.href());
|
||||
document.location.href = url.href();
|
||||
} else {
|
||||
this.reset();
|
||||
this.statusString(
|
||||
i18n.str`Oops, something went wrong. The wallet responded with error status (${rawResp.error}).`);
|
||||
this.detailCollapsed(false);
|
||||
}
|
||||
};
|
||||
chrome.runtime.sendMessage({type: 'create-reserve', detail: d}, cb);
|
||||
}
|
||||
|
||||
async onUrlChanged(url: string|null) {
|
||||
this.reset();
|
||||
this.url(url);
|
||||
if (url == undefined) {
|
||||
return;
|
||||
}
|
||||
this.updateEvent.trigger();
|
||||
let waited = await this.updateEvent.wait(200);
|
||||
if (waited) {
|
||||
// Run the actual update if nobody else preempted us.
|
||||
this.forceReserveUpdate();
|
||||
this.forceUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
renderStatus(): any {
|
||||
if (this.statusString()) {
|
||||
return <p><strong style={{color: "red"}}>{this.statusString()}</strong></p>;
|
||||
@ -411,16 +611,15 @@ export async function main() {
|
||||
throw Error(i18n.str`Can't parse wire_types: ${e.message}`);
|
||||
}
|
||||
|
||||
let suggestedExchangeUrl = await getSuggestedExchange(amount.currency);
|
||||
if (!suggestedExchangeUrl && query.suggested_exchange_url) {
|
||||
suggestedExchangeUrl = query.suggested_exchange_url;
|
||||
}
|
||||
let suggestedExchangeUrl = query.suggested_exchange_url;
|
||||
let currencyRecord = await getCurrency(amount.currency);
|
||||
|
||||
let args = {
|
||||
wt_types,
|
||||
suggestedExchangeUrl,
|
||||
callback_url,
|
||||
amount
|
||||
amount,
|
||||
currencyRecord,
|
||||
};
|
||||
|
||||
ReactDOM.render(<ExchangeSelection {...args} />, document.getElementById(
|
||||
|
@ -5,6 +5,7 @@
|
||||
<meta charset="utf-8">
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="../style/lang.css">
|
||||
<link rel="stylesheet" type="text/css" href="../style/wallet.css">
|
||||
<link rel="stylesheet" type="text/css" href="popup.css">
|
||||
|
||||
<script src="/dist/page-common-bundle.js"></script>
|
||||
|
@ -309,18 +309,16 @@ class WalletBalanceView extends React.Component<any, any> {
|
||||
</p>
|
||||
);
|
||||
});
|
||||
if (listing.length > 0) {
|
||||
let link = chrome.extension.getURL("/src/pages/auditors.html");
|
||||
let linkElem = <a href={link} target="_blank">auditors</a>;
|
||||
return (
|
||||
<div>
|
||||
{listing}
|
||||
{linkElem}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return this.renderEmpty();
|
||||
let link = chrome.extension.getURL("/src/pages/auditors.html");
|
||||
let linkElem = <a className="actionLink" href={link} target="_blank">Trusted Auditors and Exchanges</a>;
|
||||
return (
|
||||
<div>
|
||||
<h2>Available Balance</h2>
|
||||
{listing.length > 0 ? listing : this.renderEmpty()}
|
||||
<h2>Settings</h2>
|
||||
{linkElem}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -216,3 +216,7 @@ span.spacer {
|
||||
.button-secondary {
|
||||
background: rgb(66, 184, 221);
|
||||
}
|
||||
|
||||
a.actionLink {
|
||||
color: black;
|
||||
}
|
||||
|
@ -87,7 +87,7 @@ export interface ExchangeForCurrencyRecord {
|
||||
* Priority for automatic selection when withdrawing.
|
||||
*/
|
||||
priority: number;
|
||||
pinnedPub: string;
|
||||
pinnedPub?: string;
|
||||
baseUrl: string;
|
||||
}
|
||||
|
||||
@ -232,6 +232,7 @@ export interface ExchangeRecord {
|
||||
baseUrl: string;
|
||||
masterPublicKey: string;
|
||||
auditors: Auditor[];
|
||||
currency: string;
|
||||
|
||||
/**
|
||||
* Timestamp for last update.
|
||||
@ -249,6 +250,9 @@ export interface ReserveCreationInfo {
|
||||
selectedDenoms: DenominationRecord[];
|
||||
withdrawFee: AmountJson;
|
||||
overhead: AmountJson;
|
||||
wireFees: ExchangeWireFeesRecord;
|
||||
isAudited: boolean;
|
||||
isTrusted: boolean;
|
||||
}
|
||||
|
||||
|
||||
|
@ -267,13 +267,22 @@ const builtinCurrencies: CurrencyRecord[] = [
|
||||
fractionalDigits: 2,
|
||||
auditors: [
|
||||
{
|
||||
baseUrl: "https://auditor.demo.taler.net",
|
||||
baseUrl: "https://auditor.demo.taler.net/",
|
||||
expirationStamp: (new Date(2027, 1)).getTime(),
|
||||
auditorPub: "XN9KMN5G2KGPCAN0E89MM5HE8FV4WBWA9KDTMTDR817MWBCYA7H0",
|
||||
},
|
||||
],
|
||||
exchanges: [],
|
||||
},
|
||||
{
|
||||
name: "PUDOS",
|
||||
fractionalDigits: 2,
|
||||
auditors: [
|
||||
],
|
||||
exchanges: [
|
||||
{ baseUrl: "https://exchange.test.taler.net/", priority: 0 },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@ -994,6 +1003,9 @@ export class Wallet {
|
||||
|
||||
/**
|
||||
* Create a reserve, but do not flag it as confirmed yet.
|
||||
*
|
||||
* Adds the corresponding exchange as a trusted exchange if it is neither
|
||||
* audited nor trusted already.
|
||||
*/
|
||||
async createReserve(req: CreateReserveRequest): Promise<CreateReserveResponse> {
|
||||
let keypair = await this.cryptoApi.createEddsaKeypair();
|
||||
@ -1023,7 +1035,24 @@ export class Wallet {
|
||||
}
|
||||
};
|
||||
|
||||
let exchangeInfo = await this.updateExchangeFromUrl(req.exchange);
|
||||
let {isAudited, isTrusted} = await this.getExchangeTrust(exchangeInfo);
|
||||
let currencyRecord = await this.q().get(Stores.currencies, exchangeInfo.currency);
|
||||
if (!currencyRecord) {
|
||||
currencyRecord = {
|
||||
name: exchangeInfo.currency,
|
||||
fractionalDigits: 2,
|
||||
exchanges: [],
|
||||
auditors: [],
|
||||
}
|
||||
}
|
||||
|
||||
if (!isAudited && !isTrusted) {
|
||||
currencyRecord.exchanges.push({baseUrl: req.exchange, priority: 0});
|
||||
}
|
||||
|
||||
await this.q()
|
||||
.put(Stores.currencies, currencyRecord)
|
||||
.put(Stores.reserves, reserveRecord)
|
||||
.put(Stores.history, historyEntry)
|
||||
.finish();
|
||||
@ -1295,6 +1324,34 @@ export class Wallet {
|
||||
return selectedDenoms;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Check if and how an exchange is trusted and/or audited.
|
||||
*/
|
||||
async getExchangeTrust(exchangeInfo: ExchangeRecord): Promise<{isTrusted: boolean, isAudited: boolean}> {
|
||||
let isTrusted = false;
|
||||
let isAudited = false;
|
||||
let currencyRecord = await this.q().get(Stores.currencies, exchangeInfo.currency);
|
||||
if (currencyRecord) {
|
||||
for (let trustedExchange of currencyRecord.exchanges) {
|
||||
if (trustedExchange.baseUrl == exchangeInfo.baseUrl) {
|
||||
isTrusted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (let trustedAuditor of currencyRecord.auditors) {
|
||||
for (let exchangeAuditor of exchangeInfo.auditors) {
|
||||
if (trustedAuditor.baseUrl == exchangeAuditor.url) {
|
||||
isAudited = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return {isTrusted, isAudited};
|
||||
}
|
||||
|
||||
async getReserveCreationInfo(baseUrl: string,
|
||||
amount: AmountJson): Promise<ReserveCreationInfo> {
|
||||
let exchangeInfo = await this.updateExchangeFromUrl(baseUrl);
|
||||
@ -1312,10 +1369,21 @@ export class Wallet {
|
||||
|
||||
let wireInfo = await this.getWireInfo(baseUrl);
|
||||
|
||||
let wireFees = await this.q().get(Stores.exchangeWireFees, baseUrl);
|
||||
if (!wireFees) {
|
||||
// should never happen unless DB is inconsistent
|
||||
throw Error(`no wire fees found for exchange ${baseUrl}`);
|
||||
}
|
||||
|
||||
let {isTrusted, isAudited} = await this.getExchangeTrust(exchangeInfo);
|
||||
|
||||
let ret: ReserveCreationInfo = {
|
||||
exchangeInfo,
|
||||
selectedDenoms,
|
||||
wireInfo,
|
||||
wireFees,
|
||||
isAudited,
|
||||
isTrusted,
|
||||
withdrawFee: acc,
|
||||
overhead: Amounts.sub(amount, actualCoinCost).amount,
|
||||
};
|
||||
@ -1388,6 +1456,10 @@ export class Wallet {
|
||||
throw Error("invalid update time");
|
||||
}
|
||||
|
||||
if (exchangeKeysJson.denoms.length == 0) {
|
||||
throw Error("exchange doesn't offer any denominations");
|
||||
}
|
||||
|
||||
const r = await this.q().get<ExchangeRecord>(Stores.exchanges, baseUrl);
|
||||
|
||||
let exchangeInfo: ExchangeRecord;
|
||||
@ -1398,6 +1470,7 @@ export class Wallet {
|
||||
lastUpdateTime: updateTimeSec,
|
||||
masterPublicKey: exchangeKeysJson.master_public_key,
|
||||
auditors: exchangeKeysJson.auditors,
|
||||
currency: exchangeKeysJson.denoms[0].value.currency,
|
||||
};
|
||||
console.log("making fresh exchange");
|
||||
} else {
|
||||
@ -1960,6 +2033,10 @@ export class Wallet {
|
||||
return pub;
|
||||
}
|
||||
|
||||
async getCurrencyRecord(currency: string): Promise<CurrencyRecord|undefined> {
|
||||
return this.q().get(Stores.currencies, currency);
|
||||
}
|
||||
|
||||
|
||||
async paymentSucceeded(contractHash: string, merchantSig: string): Promise<any> {
|
||||
const doPaymentSucceeded = async() => {
|
||||
|
17
src/wxApi.ts
17
src/wxApi.ts
@ -48,9 +48,13 @@ export function getReserveCreationInfo(baseUrl: string,
|
||||
}
|
||||
|
||||
export async function callBackend(type: string, detail?: any): Promise<any> {
|
||||
return new Promise<ExchangeRecord[]>((resolve, reject) => {
|
||||
return new Promise<any>((resolve, reject) => {
|
||||
chrome.runtime.sendMessage({ type, detail }, (resp) => {
|
||||
resolve(resp);
|
||||
if (resp.error) {
|
||||
reject(resp);
|
||||
} else {
|
||||
resolve(resp);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -63,6 +67,15 @@ export async function getCurrencies(): Promise<CurrencyRecord[]> {
|
||||
return await callBackend("get-currencies");
|
||||
}
|
||||
|
||||
|
||||
export async function getCurrency(name: string): Promise<CurrencyRecord|null> {
|
||||
return await callBackend("currency-info", {name});
|
||||
}
|
||||
|
||||
export async function getExchangeInfo(baseUrl: string): Promise<ExchangeRecord> {
|
||||
return await callBackend("exchange-info", {baseUrl});
|
||||
}
|
||||
|
||||
export async function updateCurrency(currencyRecord: CurrencyRecord): Promise<void> {
|
||||
return await callBackend("update-currency", { currencyRecord });
|
||||
}
|
||||
|
@ -167,6 +167,12 @@ function makeHandlers(db: IDBDatabase,
|
||||
}
|
||||
return wallet.updateExchangeFromUrl(detail.baseUrl);
|
||||
},
|
||||
["currency-info"]: function (detail) {
|
||||
if (!detail.name) {
|
||||
return Promise.resolve({ error: "name missing" });
|
||||
}
|
||||
return wallet.getCurrencyRecord(detail.name);
|
||||
},
|
||||
["hash-contract"]: function (detail) {
|
||||
if (!detail.contract) {
|
||||
return Promise.resolve({ error: "contract missing" });
|
||||
@ -289,13 +295,20 @@ async function dispatch(handlers: any, req: any, sender: any, sendResponse: any)
|
||||
console.log(`exception during wallet handler for '${req.type}'`);
|
||||
console.log("request", req);
|
||||
console.error(e);
|
||||
let stack = undefined;
|
||||
try {
|
||||
stack = e.stack.toString();
|
||||
} catch (e) {
|
||||
// might fail
|
||||
}
|
||||
try {
|
||||
sendResponse({
|
||||
stack,
|
||||
error: "exception",
|
||||
hint: e.message,
|
||||
stack: e.stack.toString()
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
// might fail if tab disconnected
|
||||
}
|
||||
}
|
||||
|
21
yarn.lock
21
yarn.lock
@ -2,6 +2,12 @@
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@types/moment@^2.13.0":
|
||||
version "2.13.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/moment/-/moment-2.13.0.tgz#604ebd189bc3bc34a1548689404e61a2a4aac896"
|
||||
dependencies:
|
||||
moment "*"
|
||||
|
||||
"@types/react-dom@^15.5.0":
|
||||
version "15.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-15.5.0.tgz#7f4fb9613d4051141773242f7b6b5f1a46b34bd9"
|
||||
@ -2554,6 +2560,10 @@ mocha@^2.4.5:
|
||||
supports-color "1.2.0"
|
||||
to-iso-string "0.0.2"
|
||||
|
||||
moment@*, moment@^2.18.1:
|
||||
version "2.18.1"
|
||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.18.1.tgz#c36193dd3ce1c2eed2adb7c802dbbc77a81b1c0f"
|
||||
|
||||
ms@0.7.1:
|
||||
version "0.7.1"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098"
|
||||
@ -3673,16 +3683,7 @@ ua-parser-js@^0.7.9:
|
||||
version "0.7.12"
|
||||
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.12.tgz#04c81a99bdd5dc52263ea29d24c6bf8d4818a4bb"
|
||||
|
||||
uglify-js@^2.6, uglify-js@^2.8.5, uglify-js@~2.8.22:
|
||||
version "2.8.22"
|
||||
resolved "git://github.com/mishoo/UglifyJS2#278577f3cb75e72320564805ee91be63e5f9c806"
|
||||
dependencies:
|
||||
source-map "~0.5.1"
|
||||
yargs "~3.10.0"
|
||||
optionalDependencies:
|
||||
uglify-to-browserify "~1.0.0"
|
||||
|
||||
uglify-js@^2.8.22:
|
||||
uglify-js@^2.6, uglify-js@^2.8.22, uglify-js@^2.8.5, uglify-js@~2.8.22:
|
||||
version "2.8.22"
|
||||
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.22.tgz#d54934778a8da14903fa29a326fb24c0ab51a1a0"
|
||||
dependencies:
|
||||
|
Loading…
Reference in New Issue
Block a user