2015-12-25 22:42:14 +01:00
|
|
|
/*
|
|
|
|
This file is part of TALER
|
2016-01-26 17:21:17 +01:00
|
|
|
(C) 2015-2016 GNUnet e.V.
|
2015-12-25 22:42:14 +01:00
|
|
|
|
|
|
|
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
|
2016-07-07 17:59:29 +02:00
|
|
|
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
2015-12-25 22:42:14 +01:00
|
|
|
*/
|
|
|
|
|
2016-03-01 19:46:20 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Page shown to the user to confirm creation
|
|
|
|
* of a reserve, usually requested by the bank.
|
|
|
|
*
|
|
|
|
* @author Florian Dold
|
|
|
|
*/
|
|
|
|
|
2017-05-28 23:15:41 +02:00
|
|
|
import {amountToPretty, canonicalizeBaseUrl} from "../../helpers";
|
2017-05-29 16:27:53 +02:00
|
|
|
import * as i18n from "../../i18n";
|
2016-11-13 23:30:18 +01:00
|
|
|
import {
|
2017-05-29 16:27:53 +02:00
|
|
|
AmountJson,
|
|
|
|
Amounts,
|
|
|
|
CreateReserveResponse,
|
|
|
|
CurrencyRecord,
|
|
|
|
Denomination,
|
|
|
|
DenominationRecord,
|
|
|
|
ReserveCreationInfo,
|
2017-05-28 23:15:41 +02:00
|
|
|
} from "../../types";
|
|
|
|
|
2017-04-20 03:09:25 +02:00
|
|
|
import {ImplicitStateComponent, StateHolder} from "../components";
|
2017-05-29 16:27:53 +02:00
|
|
|
import {
|
|
|
|
getCurrency,
|
|
|
|
getExchangeInfo,
|
|
|
|
getReserveCreationInfo,
|
|
|
|
} from "../wxApi";
|
2017-05-28 23:15:41 +02:00
|
|
|
|
2017-04-20 03:09:25 +02:00
|
|
|
import * as React from "react";
|
|
|
|
import * as ReactDOM from "react-dom";
|
|
|
|
import URI = require("urijs");
|
2017-04-28 23:28:27 +02:00
|
|
|
import * as moment from "moment";
|
2015-12-20 20:34:20 +01:00
|
|
|
|
2016-10-07 14:34:31 +02:00
|
|
|
|
2016-10-07 17:10:22 +02:00
|
|
|
function delay<T>(delayMs: number, value: T): Promise<T> {
|
|
|
|
return new Promise<T>((resolve, reject) => {
|
|
|
|
setTimeout(() => resolve(value), delayMs);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
class EventTrigger {
|
2017-05-29 16:27:53 +02:00
|
|
|
private triggerResolve: any;
|
|
|
|
private triggerPromise: Promise<boolean>;
|
2016-10-07 17:10:22 +02:00
|
|
|
|
|
|
|
constructor() {
|
|
|
|
this.reset();
|
2016-02-11 11:29:57 +01:00
|
|
|
}
|
|
|
|
|
2016-10-07 17:10:22 +02:00
|
|
|
private reset() {
|
|
|
|
this.triggerPromise = new Promise<boolean>((resolve, reject) => {
|
|
|
|
this.triggerResolve = resolve;
|
|
|
|
});
|
2015-12-20 20:34:20 +01:00
|
|
|
}
|
2016-02-15 11:29:58 +01:00
|
|
|
|
2016-10-07 17:10:22 +02:00
|
|
|
trigger() {
|
|
|
|
this.triggerResolve(false);
|
|
|
|
this.reset();
|
|
|
|
}
|
|
|
|
|
|
|
|
async wait(delayMs: number): Promise<boolean> {
|
|
|
|
return await Promise.race([this.triggerPromise, delay(delayMs, true)]);
|
2016-02-15 11:29:58 +01:00
|
|
|
}
|
2016-02-11 11:29:57 +01:00
|
|
|
}
|
2015-12-20 20:34:20 +01:00
|
|
|
|
2016-10-07 17:10:22 +02:00
|
|
|
|
2017-04-28 23:28:27 +02:00
|
|
|
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) => {
|
2017-05-29 16:27:53 +02:00
|
|
|
this.setState({collapsed: false});
|
|
|
|
e.preventDefault();
|
2017-04-28 23:28:27 +02:00
|
|
|
};
|
|
|
|
const doClose = (e: any) => {
|
2017-05-29 16:27:53 +02:00
|
|
|
this.setState({collapsed: true});
|
2017-04-28 23:28:27 +02:00
|
|
|
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>
|
|
|
|
);
|
|
|
|
}
|
2017-05-29 16:27:53 +02:00
|
|
|
if (rci.exchangeInfo.auditors.length === 0) {
|
2017-04-28 23:28:27 +02:00
|
|
|
return (
|
|
|
|
<p>
|
|
|
|
The exchange is not audited by any auditors.
|
|
|
|
</p>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return (
|
|
|
|
<div>
|
2017-05-29 16:27:53 +02:00
|
|
|
{rci.exchangeInfo.auditors.map((a) => (
|
2017-04-28 23:28:27 +02:00
|
|
|
<h3>Auditor {a.url}</h3>
|
|
|
|
))}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2016-10-07 17:10:22 +02:00
|
|
|
function renderReserveCreationDetails(rci: ReserveCreationInfo|null) {
|
|
|
|
if (!rci) {
|
2017-04-28 23:28:27 +02:00
|
|
|
return (
|
|
|
|
<p>
|
|
|
|
Details will be displayed when a valid exchange provider URL is entered.
|
|
|
|
</p>
|
|
|
|
);
|
2016-10-07 17:10:22 +02:00
|
|
|
}
|
|
|
|
|
2017-05-29 16:27:53 +02:00
|
|
|
const denoms = rci.selectedDenoms;
|
2016-10-07 14:34:31 +02:00
|
|
|
|
2017-05-29 16:27:53 +02:00
|
|
|
const countByPub: {[s: string]: number} = {};
|
|
|
|
const uniq: DenominationRecord[] = [];
|
2016-10-07 14:34:31 +02:00
|
|
|
|
2016-11-16 01:59:39 +01:00
|
|
|
denoms.forEach((x: DenominationRecord) => {
|
|
|
|
let c = countByPub[x.denomPub] || 0;
|
2017-05-29 16:27:53 +02:00
|
|
|
if (c === 0) {
|
2016-10-07 14:34:31 +02:00
|
|
|
uniq.push(x);
|
|
|
|
}
|
|
|
|
c += 1;
|
2016-11-16 01:59:39 +01:00
|
|
|
countByPub[x.denomPub] = c;
|
2016-10-07 14:34:31 +02:00
|
|
|
});
|
|
|
|
|
2016-11-16 01:59:39 +01:00
|
|
|
function row(denom: DenominationRecord) {
|
2016-10-07 14:34:31 +02:00
|
|
|
return (
|
|
|
|
<tr>
|
2016-11-16 01:59:39 +01:00
|
|
|
<td>{countByPub[denom.denomPub] + "x"}</td>
|
2016-10-07 14:34:31 +02:00
|
|
|
<td>{amountToPretty(denom.value)}</td>
|
2016-11-16 01:59:39 +01:00
|
|
|
<td>{amountToPretty(denom.feeWithdraw)}</td>
|
|
|
|
<td>{amountToPretty(denom.feeRefresh)}</td>
|
|
|
|
<td>{amountToPretty(denom.feeDeposit)}</td>
|
2016-10-07 14:34:31 +02:00
|
|
|
</tr>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2017-04-28 23:28:27 +02:00
|
|
|
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>
|
2017-05-29 16:27:53 +02:00
|
|
|
{rci!.wireFees.feesForType[s].map((f) => (
|
2017-04-28 23:28:27 +02:00
|
|
|
<tr>
|
|
|
|
<td>{moment.unix(f.endStamp).format("llll")}</td>
|
|
|
|
<td>{amountToPretty(f.wireFee)}</td>
|
|
|
|
<td>{amountToPretty(f.closingFee)}</td>
|
|
|
|
</tr>
|
|
|
|
))}
|
2017-05-29 16:27:53 +02:00
|
|
|
</tbody>,
|
2017-04-28 23:28:27 +02:00
|
|
|
];
|
|
|
|
}
|
|
|
|
|
2017-05-29 16:27:53 +02:00
|
|
|
const withdrawFeeStr = amountToPretty(rci.withdrawFee);
|
|
|
|
const overheadStr = amountToPretty(rci.overhead);
|
2016-10-07 14:34:31 +02:00
|
|
|
|
|
|
|
return (
|
|
|
|
<div>
|
2017-04-28 23:28:27 +02:00
|
|
|
<h3>Overview</h3>
|
2016-11-27 22:13:24 +01:00
|
|
|
<p>{i18n.str`Withdrawal fees: ${withdrawFeeStr}`}</p>
|
|
|
|
<p>{i18n.str`Rounding loss: ${overheadStr}`}</p>
|
2017-04-28 23:42:14 +02:00
|
|
|
<p>{i18n.str`Earliest expiration (for deposit): ${moment.unix(rci.earliestDepositExpiration).fromNow()}`}</p>
|
2017-04-28 23:28:27 +02:00
|
|
|
<h3>Coin Fees</h3>
|
|
|
|
<table className="pure-table">
|
2016-10-07 14:34:31 +02:00
|
|
|
<thead>
|
2017-04-28 23:28:27 +02:00
|
|
|
<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>
|
2016-10-07 14:34:31 +02:00
|
|
|
</thead>
|
|
|
|
<tbody>
|
|
|
|
{uniq.map(row)}
|
|
|
|
</tbody>
|
|
|
|
</table>
|
2017-04-28 23:28:27 +02:00
|
|
|
<h3>Wire Fees</h3>
|
|
|
|
<table className="pure-table">
|
|
|
|
{Object.keys(rci.wireFees.feesForType).map(wireFee)}
|
|
|
|
</table>
|
2016-10-07 14:34:31 +02:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2016-10-07 17:10:22 +02:00
|
|
|
function WithdrawFee(props: {reserveCreationInfo: ReserveCreationInfo|null}): JSX.Element {
|
|
|
|
if (props.reserveCreationInfo) {
|
2017-05-29 16:27:53 +02:00
|
|
|
const {overhead, withdrawFee} = props.reserveCreationInfo;
|
|
|
|
const totalCost = Amounts.add(overhead, withdrawFee).amount;
|
2016-11-27 22:13:24 +01:00
|
|
|
return <p>{i18n.str`Withdraw fees:`} {amountToPretty(totalCost)}</p>;
|
2016-10-07 17:10:22 +02:00
|
|
|
}
|
|
|
|
return <p />;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2016-10-07 14:34:31 +02:00
|
|
|
interface ExchangeSelectionProps {
|
2016-04-27 06:50:38 +02:00
|
|
|
suggestedExchangeUrl: string;
|
2016-10-07 14:34:31 +02:00
|
|
|
amount: AmountJson;
|
|
|
|
callback_url: string;
|
|
|
|
wt_types: string[];
|
2017-04-28 23:28:27 +02:00
|
|
|
currencyRecord: CurrencyRecord|null;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface ManualSelectionProps {
|
|
|
|
onSelect(url: string): void;
|
|
|
|
initialUrl: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
class ManualSelection extends ImplicitStateComponent<ManualSelectionProps> {
|
2017-05-29 16:27:53 +02:00
|
|
|
private url: StateHolder<string> = this.makeState("");
|
|
|
|
private errorMessage: StateHolder<string|null> = this.makeState(null);
|
|
|
|
private isOkay: StateHolder<boolean> = this.makeState(false);
|
|
|
|
private updateEvent = new EventTrigger();
|
2017-04-28 23:28:27 +02:00
|
|
|
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;
|
|
|
|
}
|
2017-05-29 16:27:53 +02:00
|
|
|
const parsedUrl = new URI(this.url()!);
|
2017-04-28 23:28:27 +02:00
|
|
|
if (parsedUrl.is("relative")) {
|
|
|
|
this.errorMessage(i18n.str`Error: URL may not be relative`);
|
|
|
|
this.isOkay(false);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
try {
|
2017-05-29 16:27:53 +02:00
|
|
|
const url = canonicalizeBaseUrl(this.url()!);
|
|
|
|
const r = await getExchangeInfo(url);
|
|
|
|
console.log("getExchangeInfo returned");
|
2017-04-28 23:28:27 +02:00
|
|
|
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")) {
|
2017-05-29 16:27:53 +02:00
|
|
|
const resp = e.errorResponse;
|
2017-04-28 23:28:27 +02:00
|
|
|
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();
|
2017-05-29 16:27:53 +02:00
|
|
|
const waited = await this.updateEvent.wait(200);
|
2017-04-28 23:28:27 +02:00
|
|
|
if (waited) {
|
|
|
|
// Run the actual update if nobody else preempted us.
|
|
|
|
this.update();
|
|
|
|
}
|
|
|
|
}
|
2016-10-07 14:34:31 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class ExchangeSelection extends ImplicitStateComponent<ExchangeSelectionProps> {
|
2017-05-29 16:27:53 +02:00
|
|
|
private statusString: StateHolder<string|null> = this.makeState(null);
|
|
|
|
private reserveCreationInfo: StateHolder<ReserveCreationInfo|null> = this.makeState(
|
2016-10-07 14:34:31 +02:00
|
|
|
null);
|
2017-05-29 16:27:53 +02:00
|
|
|
private url: StateHolder<string|null> = this.makeState(null);
|
2016-10-07 14:34:31 +02:00
|
|
|
|
2017-05-29 16:27:53 +02:00
|
|
|
private selectingExchange: StateHolder<boolean> = this.makeState(false);
|
2016-10-07 14:34:31 +02:00
|
|
|
|
|
|
|
constructor(props: ExchangeSelectionProps) {
|
|
|
|
super(props);
|
2017-05-29 16:27:53 +02:00
|
|
|
const prefilledExchangesUrls = [];
|
2017-04-28 23:28:27 +02:00
|
|
|
if (props.currencyRecord) {
|
2017-05-29 16:27:53 +02:00
|
|
|
const exchanges = props.currencyRecord.exchanges.map((x) => x.baseUrl);
|
2017-04-28 23:28:27 +02:00
|
|
|
prefilledExchangesUrls.push(...exchanges);
|
2016-10-07 14:34:31 +02:00
|
|
|
}
|
2017-04-28 23:28:27 +02:00
|
|
|
if (props.suggestedExchangeUrl) {
|
|
|
|
prefilledExchangesUrls.push(props.suggestedExchangeUrl);
|
|
|
|
}
|
2017-05-29 16:27:53 +02:00
|
|
|
if (prefilledExchangesUrls.length !== 0) {
|
2017-04-28 23:28:27 +02:00
|
|
|
this.url(prefilledExchangesUrls[0]);
|
|
|
|
this.forceReserveUpdate();
|
|
|
|
} else {
|
|
|
|
this.selectingExchange(true);
|
2016-10-07 17:10:22 +02:00
|
|
|
}
|
|
|
|
}
|
2016-10-07 14:34:31 +02:00
|
|
|
|
2016-10-10 03:32:18 +02:00
|
|
|
renderFeeStatus() {
|
2017-05-29 16:27:53 +02:00
|
|
|
const rci = this.reserveCreationInfo();
|
2017-04-28 23:28:27 +02:00
|
|
|
if (rci) {
|
2017-05-29 16:27:53 +02:00
|
|
|
const totalCost = Amounts.add(rci.overhead, rci.withdrawFee).amount;
|
2017-04-28 23:28:27 +02:00
|
|
|
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>
|
|
|
|
);
|
|
|
|
}
|
2016-10-10 03:32:18 +02:00
|
|
|
return (
|
2017-04-28 23:28:27 +02:00
|
|
|
<div>
|
2016-11-23 01:14:45 +01:00
|
|
|
<i18n.Translate wrap="p">
|
2017-04-28 23:28:27 +02:00
|
|
|
Using exchange provider <strong>{this.url()}</strong>.
|
2016-10-10 03:32:18 +02:00
|
|
|
The exchange provider will charge
|
|
|
|
{" "}
|
2017-04-28 23:28:27 +02:00
|
|
|
<span>{amountToPretty(totalCost)}</span>
|
2016-10-10 03:32:18 +02:00
|
|
|
{" "}
|
|
|
|
in fees.
|
2016-11-23 01:14:45 +01:00
|
|
|
</i18n.Translate>
|
2017-04-28 23:28:27 +02:00
|
|
|
{trustMessage}
|
|
|
|
</div>
|
2016-10-10 03:32:18 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
if (this.url() && !this.statusString()) {
|
2017-05-29 16:27:53 +02:00
|
|
|
const shortName = new URI(this.url()!).host();
|
2016-11-23 01:14:45 +01:00
|
|
|
return (
|
|
|
|
<i18n.Translate wrap="p">
|
|
|
|
Waiting for a response from
|
|
|
|
{" "}
|
|
|
|
<em>{shortName}</em>
|
|
|
|
</i18n.Translate>
|
|
|
|
);
|
2016-10-10 03:32:18 +02:00
|
|
|
}
|
2016-10-13 20:02:42 +02:00
|
|
|
if (this.statusString()) {
|
|
|
|
return (
|
|
|
|
<p>
|
2017-04-28 23:28:27 +02:00
|
|
|
<strong style={{color: "red"}}>{i18n.str`A problem occured, see below. ${this.statusString()}`}</strong>
|
2016-10-13 20:02:42 +02:00
|
|
|
</p>
|
|
|
|
);
|
|
|
|
}
|
2016-10-10 03:32:18 +02:00
|
|
|
return (
|
|
|
|
<p>
|
2016-11-27 22:13:24 +01:00
|
|
|
{i18n.str`Information about fees will be available when an exchange provider is selected.`}
|
2016-10-10 03:32:18 +02:00
|
|
|
</p>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2017-04-28 23:28:27 +02:00
|
|
|
renderConfirm() {
|
2016-10-07 17:10:22 +02:00
|
|
|
return (
|
|
|
|
<div>
|
2016-10-10 03:32:18 +02:00
|
|
|
{this.renderFeeStatus()}
|
2017-04-28 23:28:27 +02:00
|
|
|
<button className="pure-button button-success"
|
2017-05-29 16:27:53 +02:00
|
|
|
disabled={this.reserveCreationInfo() === null}
|
2016-10-07 17:10:22 +02:00
|
|
|
onClick={() => this.confirmReserve()}>
|
2016-11-27 22:13:24 +01:00
|
|
|
{i18n.str`Accept fees and withdraw`}
|
2016-10-07 17:10:22 +02:00
|
|
|
</button>
|
2017-04-28 23:28:27 +02:00
|
|
|
{ " " }
|
|
|
|
<button className="pure-button button-secondary"
|
|
|
|
onClick={() => this.selectingExchange(true)}>
|
|
|
|
{i18n.str`Change Exchange Provider`}
|
|
|
|
</button>
|
2016-10-07 17:10:22 +02:00
|
|
|
<br/>
|
2017-04-28 23:42:14 +02:00
|
|
|
<Collapsible initiallyCollapsed={true} title="Fee and Spending Details">
|
2017-04-28 23:28:27 +02:00
|
|
|
{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() {
|
2017-05-29 16:27:53 +02:00
|
|
|
const exchanges = (this.props.currencyRecord && this.props.currencyRecord.exchanges) || [];
|
2017-04-28 23:28:27 +02:00
|
|
|
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>
|
2017-05-29 16:27:53 +02:00
|
|
|
{exchanges.map((e) => (
|
2017-04-28 23:28:27 +02:00
|
|
|
<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()}
|
2016-10-07 17:10:22 +02:00
|
|
|
</div>
|
2016-10-07 14:34:31 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
confirmReserve() {
|
|
|
|
this.confirmReserveImpl(this.reserveCreationInfo()!,
|
|
|
|
this.url()!,
|
|
|
|
this.props.amount,
|
|
|
|
this.props.callback_url);
|
|
|
|
}
|
|
|
|
|
2016-10-07 17:10:22 +02:00
|
|
|
/**
|
|
|
|
* Do an update of the reserve creation info, without any debouncing.
|
|
|
|
*/
|
|
|
|
async forceReserveUpdate() {
|
|
|
|
this.reserveCreationInfo(null);
|
|
|
|
try {
|
2017-05-29 16:27:53 +02:00
|
|
|
const url = canonicalizeBaseUrl(this.url()!);
|
|
|
|
const r = await getReserveCreationInfo(url,
|
2016-10-07 17:10:22 +02:00
|
|
|
this.props.amount);
|
|
|
|
console.log("get exchange info resolved");
|
|
|
|
this.reserveCreationInfo(r);
|
|
|
|
console.dir(r);
|
|
|
|
} catch (e) {
|
2017-04-28 23:28:27 +02:00
|
|
|
console.log("get exchange info rejected", e);
|
2016-10-07 17:10:22 +02:00
|
|
|
if (e.hasOwnProperty("httpStatus")) {
|
|
|
|
this.statusString(`Error: request failed with status ${e.httpStatus}`);
|
|
|
|
} else if (e.hasOwnProperty("errorResponse")) {
|
2017-05-29 16:27:53 +02:00
|
|
|
const resp = e.errorResponse;
|
2016-10-07 17:10:22 +02:00
|
|
|
this.statusString(`Error: ${resp.error} (${resp.hint})`);
|
|
|
|
}
|
|
|
|
}
|
2016-02-11 11:29:57 +01:00
|
|
|
}
|
2015-12-20 20:34:20 +01:00
|
|
|
|
2016-10-07 14:34:31 +02:00
|
|
|
confirmReserveImpl(rci: ReserveCreationInfo,
|
|
|
|
exchange: string,
|
|
|
|
amount: AmountJson,
|
|
|
|
callback_url: string) {
|
2016-11-16 01:59:39 +01:00
|
|
|
const d = {exchange: canonicalizeBaseUrl(exchange), amount};
|
2016-09-12 20:25:56 +02:00
|
|
|
const cb = (rawResp: any) => {
|
2016-02-09 21:56:06 +01:00
|
|
|
if (!rawResp) {
|
|
|
|
throw Error("empty response");
|
|
|
|
}
|
2016-04-06 02:06:57 +02:00
|
|
|
// FIXME: filter out types that bank/exchange don't have in common
|
2017-05-29 16:27:53 +02:00
|
|
|
const wireDetails = rci.wireInfo;
|
|
|
|
const filteredWireDetails: any = {};
|
|
|
|
for (const wireType in wireDetails) {
|
|
|
|
if (this.props.wt_types.findIndex((x) => x.toLowerCase() === wireType.toLowerCase()) < 0) {
|
2017-03-08 17:14:20 +01:00
|
|
|
continue;
|
|
|
|
}
|
2017-05-29 16:27:53 +02:00
|
|
|
const obj = Object.assign({}, wireDetails[wireType]);
|
2017-03-08 17:14:20 +01:00
|
|
|
// The bank doesn't need to know about fees
|
|
|
|
delete obj.fees;
|
|
|
|
// Consequently the bank can't verify signatures anyway, so
|
|
|
|
// we delete this extra data, to make the request URL shorter.
|
|
|
|
delete obj.salt;
|
|
|
|
delete obj.sig;
|
|
|
|
filteredWireDetails[wireType] = obj;
|
|
|
|
}
|
2016-02-09 21:56:06 +01:00
|
|
|
if (!rawResp.error) {
|
|
|
|
const resp = CreateReserveResponse.checked(rawResp);
|
2017-05-29 16:27:53 +02:00
|
|
|
const q: {[name: string]: string|number} = {
|
|
|
|
amount_currency: amount.currency,
|
|
|
|
amount_fraction: amount.fraction,
|
|
|
|
amount_value: amount.value,
|
2016-03-01 19:39:17 +01:00
|
|
|
exchange: resp.exchange,
|
2016-02-09 21:56:06 +01:00
|
|
|
reserve_pub: resp.reservePub,
|
2017-05-29 16:27:53 +02:00
|
|
|
wire_details: JSON.stringify(filteredWireDetails),
|
2016-02-09 21:56:06 +01:00
|
|
|
};
|
2017-05-29 16:27:53 +02:00
|
|
|
const url = new URI(callback_url).addQuery(q);
|
2016-02-09 21:56:06 +01:00
|
|
|
if (!url.is("absolute")) {
|
|
|
|
throw Error("callback url is not absolute");
|
|
|
|
}
|
2016-02-11 11:29:57 +01:00
|
|
|
console.log("going to", url.href());
|
2016-02-09 21:56:06 +01:00
|
|
|
document.location.href = url.href();
|
2016-01-26 17:21:17 +01:00
|
|
|
} else {
|
2016-10-07 14:34:31 +02:00
|
|
|
this.statusString(
|
2016-11-27 22:13:24 +01:00
|
|
|
i18n.str`Oops, something went wrong. The wallet responded with error status (${rawResp.error}).`);
|
2016-01-26 17:21:17 +01:00
|
|
|
}
|
|
|
|
};
|
2017-05-29 16:27:53 +02:00
|
|
|
chrome.runtime.sendMessage({type: "create-reserve", detail: d}, cb);
|
2016-02-11 11:29:57 +01:00
|
|
|
}
|
|
|
|
|
2016-10-07 17:10:22 +02:00
|
|
|
renderStatus(): any {
|
|
|
|
if (this.statusString()) {
|
2016-11-13 08:16:12 +01:00
|
|
|
return <p><strong style={{color: "red"}}>{this.statusString()}</strong></p>;
|
2016-10-07 17:10:22 +02:00
|
|
|
} else if (!this.reserveCreationInfo()) {
|
2016-11-27 22:13:24 +01:00
|
|
|
return <p>{i18n.str`Checking URL, please wait ...`}</p>;
|
2016-04-27 06:03:04 +02:00
|
|
|
}
|
2016-10-07 17:10:22 +02:00
|
|
|
return "";
|
2016-02-18 22:50:17 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-05-29 16:27:53 +02:00
|
|
|
async function main() {
|
2016-10-07 14:34:31 +02:00
|
|
|
try {
|
2017-04-20 03:09:25 +02:00
|
|
|
const url = new URI(document.location.href);
|
2016-11-19 16:33:29 +01:00
|
|
|
const query: any = URI.parseQuery(url.query());
|
|
|
|
let amount;
|
|
|
|
try {
|
|
|
|
amount = AmountJson.checked(JSON.parse(query.amount));
|
|
|
|
} catch (e) {
|
2016-11-27 22:13:24 +01:00
|
|
|
throw Error(i18n.str`Can't parse amount: ${e.message}`);
|
2016-11-19 16:33:29 +01:00
|
|
|
}
|
|
|
|
const callback_url = query.callback_url;
|
|
|
|
const bank_url = query.bank_url;
|
|
|
|
let wt_types;
|
|
|
|
try {
|
|
|
|
wt_types = JSON.parse(query.wt_types);
|
|
|
|
} catch (e) {
|
2016-11-27 22:13:24 +01:00
|
|
|
throw Error(i18n.str`Can't parse wire_types: ${e.message}`);
|
2016-11-19 16:33:29 +01:00
|
|
|
}
|
|
|
|
|
2017-05-29 16:27:53 +02:00
|
|
|
const suggestedExchangeUrl = query.suggested_exchange_url;
|
|
|
|
const currencyRecord = await getCurrency(amount.currency);
|
2017-02-12 04:40:28 +01:00
|
|
|
|
2017-05-29 16:27:53 +02:00
|
|
|
const args = {
|
2017-04-28 23:28:27 +02:00
|
|
|
amount,
|
2017-05-29 16:27:53 +02:00
|
|
|
callback_url,
|
2017-04-28 23:28:27 +02:00
|
|
|
currencyRecord,
|
2017-05-29 16:27:53 +02:00
|
|
|
suggestedExchangeUrl,
|
|
|
|
wt_types,
|
2016-10-07 14:34:31 +02:00
|
|
|
};
|
|
|
|
|
2016-11-13 08:16:12 +01:00
|
|
|
ReactDOM.render(<ExchangeSelection {...args} />, document.getElementById(
|
2016-10-07 14:34:31 +02:00
|
|
|
"exchange-selection")!);
|
|
|
|
|
|
|
|
} catch (e) {
|
|
|
|
// TODO: provide more context information, maybe factor it out into a
|
|
|
|
// TODO:generic error reporting function or component.
|
2016-11-27 22:13:24 +01:00
|
|
|
document.body.innerText = i18n.str`Fatal error: "${e.message}".`;
|
2016-10-07 14:34:31 +02:00
|
|
|
console.error(`got error "${e.message}"`, e);
|
|
|
|
}
|
2016-10-10 04:01:14 +02:00
|
|
|
}
|
2017-04-20 03:09:25 +02:00
|
|
|
|
|
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
|
|
main();
|
|
|
|
});
|