aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--node_modules/.yarn-integrity2
-rw-r--r--package.json6
-rw-r--r--src/components.ts13
-rw-r--r--src/i18n.tsx4
-rw-r--r--src/pages/auditors.html3
-rw-r--r--src/pages/auditors.tsx32
-rw-r--r--src/pages/confirm-create-reserve.html29
-rw-r--r--src/pages/confirm-create-reserve.tsx401
-rw-r--r--src/pages/popup.html1
-rw-r--r--src/pages/popup.tsx22
-rw-r--r--src/style/wallet.css4
-rw-r--r--src/types.ts6
-rw-r--r--src/wallet.ts79
-rw-r--r--src/wxApi.ts17
-rw-r--r--src/wxBackend.ts15
-rw-r--r--yarn.lock21
16 files changed, 520 insertions, 135 deletions
diff --git a/node_modules/.yarn-integrity b/node_modules/.yarn-integrity
index 6d4c42d0c..d45440279 100644
--- a/node_modules/.yarn-integrity
+++ b/node_modules/.yarn-integrity
@@ -1 +1 @@
-751d3ff225403bea12799f2c0ad32d26a0ff81a4f88821c8f1615d3ddc5a9533 \ No newline at end of file
+f57c90a35fd7bae0b594a5c9114779b9b7c1629f6977a421d3e666087dc7ed0f \ No newline at end of file
diff --git a/package.json b/package.json
index f6675834a..0709bc29b 100644
--- a/package.json
+++ b/package.json
@@ -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"
}
}
diff --git a/src/components.ts b/src/components.ts
index 4ed746f67..569810f3a 100644
--- a/src/components.ts
+++ b/src/components.ts
@@ -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;
};
diff --git a/src/i18n.tsx b/src/i18n.tsx
index ff32e62a8..aa26407d9 100644
--- a/src/i18n.tsx
+++ b/src/i18n.tsx
@@ -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);
}
}
diff --git a/src/pages/auditors.html b/src/pages/auditors.html
index 7e01f4e1f..2f50b28a1 100644
--- a/src/pages/auditors.html
+++ b/src/pages/auditors.html
@@ -14,6 +14,9 @@
<script src="/dist/auditors-bundle.js"></script>
<style>
+ body {
+ font-size: 100%;
+ }
.tree-item {
margin: 2em;
border-radius: 5px;
diff --git a/src/pages/auditors.tsx b/src/pages/auditors.tsx
index 41339b0d8..762d22ad8 100644
--- a/src/pages/auditors.tsx
+++ b/src/pages/auditors.tsx
@@ -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>
diff --git a/src/pages/confirm-create-reserve.html b/src/pages/confirm-create-reserve.html
index c1e4b7ce3..16ab12a30 100644
--- a/src/pages/confirm-create-reserve.html
+++ b/src/pages/confirm-create-reserve.html
@@ -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>
diff --git a/src/pages/confirm-create-reserve.tsx b/src/pages/confirm-create-reserve.tsx
index a7fd7b0fd..6b618c273 100644
--- a/src/pages/confirm-create-reserve.tsx
+++ b/src/pages/confirm-create-reserve.tsx
@@ -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,22 +424,80 @@ class ExchangeSelection extends ImplicitStateComponent<ExchangeSelectionProps> {
);
}
- render(): JSX.Element {
+ renderConfirm() {
return (
<div>
- <i18n.Translate wrap="p">
- {"You are about to withdraw "}
- <strong>{amountToPretty(this.props.amount)}</strong>
- {" from your bank account into your wallet."}
- </i18n.Translate>
{this.renderFeeStatus()}
- <button className="accept"
+ <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/>
- {this.renderAdvanced()}
+ <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>
+ <i18n.Translate wrap="p">
+ {"You are about to withdraw "}
+ <strong>{amountToPretty(this.props.amount)}</strong>
+ {" from your bank account into your wallet."}
+ </i18n.Translate>
+ {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(
diff --git a/src/pages/popup.html b/src/pages/popup.html
index 7ff5cffaf..702f43cde 100644
--- a/src/pages/popup.html
+++ b/src/pages/popup.html
@@ -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>
diff --git a/src/pages/popup.tsx b/src/pages/popup.tsx
index c8d52b45c..7f179366a 100644
--- a/src/pages/popup.tsx
+++ b/src/pages/popup.tsx
@@ -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>
+ );
}
}
diff --git a/src/style/wallet.css b/src/style/wallet.css
index 7fe5e37c8..752fc6d75 100644
--- a/src/style/wallet.css
+++ b/src/style/wallet.css
@@ -216,3 +216,7 @@ span.spacer {
.button-secondary {
background: rgb(66, 184, 221);
}
+
+a.actionLink {
+ color: black;
+}
diff --git a/src/types.ts b/src/types.ts
index 5d53f8db0..4707edd95 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -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;
}
diff --git a/src/wallet.ts b/src/wallet.ts
index 982801f43..bc3cd59fc 100644
--- a/src/wallet.ts
+++ b/src/wallet.ts
@@ -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() => {
diff --git a/src/wxApi.ts b/src/wxApi.ts
index de59914a1..bdc02af1b 100644
--- a/src/wxApi.ts
+++ b/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 });
}
diff --git a/src/wxBackend.ts b/src/wxBackend.ts
index 984cad21a..716dc66be 100644
--- a/src/wxBackend.ts
+++ b/src/wxBackend.ts
@@ -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
}
}
diff --git a/yarn.lock b/yarn.lock
index b334da648..1b21aba5a 100644
--- a/yarn.lock
+++ b/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: