Merge branch 'master' into jg-signer-decoding
This commit is contained in:
commit
fbd3738096
@ -107,9 +107,8 @@
|
|||||||
"postcss-simple-vars": "~3.0.0",
|
"postcss-simple-vars": "~3.0.0",
|
||||||
"progress": "~1.1.8",
|
"progress": "~1.1.8",
|
||||||
"raw-loader": "~0.5.1",
|
"raw-loader": "~0.5.1",
|
||||||
"react-addons-perf": "~15.3.2",
|
"react-addons-perf": "~15.4.1",
|
||||||
"react-addons-test-utils": "~15.3.2",
|
"react-addons-test-utils": "~15.4.1",
|
||||||
"react-dom": "~15.3.2",
|
|
||||||
"react-hot-loader": "~3.0.0-beta.6",
|
"react-hot-loader": "~3.0.0-beta.6",
|
||||||
"rucksack-css": "~0.8.6",
|
"rucksack-css": "~0.8.6",
|
||||||
"sinon": "~1.17.4",
|
"sinon": "~1.17.4",
|
||||||
@ -141,25 +140,25 @@
|
|||||||
"js-sha3": "~0.5.2",
|
"js-sha3": "~0.5.2",
|
||||||
"lodash": "~4.11.1",
|
"lodash": "~4.11.1",
|
||||||
"marked": "~0.3.6",
|
"marked": "~0.3.6",
|
||||||
"material-ui": "0.16.1",
|
"material-ui": "~0.16.4",
|
||||||
"material-ui-chip-input": "~0.8.0",
|
"material-ui-chip-input": "~0.11.1",
|
||||||
"mobx": "~2.6.1",
|
"mobx": "~2.6.1",
|
||||||
"mobx-react": "~3.5.8",
|
"mobx-react": "~3.5.8",
|
||||||
"mobx-react-devtools": "~4.2.9",
|
"mobx-react-devtools": "~4.2.9",
|
||||||
"moment": "~2.14.1",
|
"moment": "~2.14.1",
|
||||||
"phoneformat.js": "~1.0.3",
|
"phoneformat.js": "~1.0.3",
|
||||||
"qs": "~6.3.0",
|
"qs": "~6.3.0",
|
||||||
"react": "~15.3.2",
|
"react": "~15.4.1",
|
||||||
"react-ace": "~4.0.0",
|
"react-ace": "~4.0.0",
|
||||||
"react-addons-css-transition-group": "~15.3.2",
|
"react-addons-css-transition-group": "~15.4.1",
|
||||||
"react-chartjs-2": "~1.5.0",
|
"react-chartjs-2": "~1.5.0",
|
||||||
"react-copy-to-clipboard": "~4.2.3",
|
"react-copy-to-clipboard": "~4.2.3",
|
||||||
"react-dom": "~15.3.2",
|
"react-dom": "~15.4.1",
|
||||||
"react-dropzone": "~3.7.3",
|
"react-dropzone": "~3.7.3",
|
||||||
"react-redux": "~4.4.5",
|
"react-redux": "~4.4.5",
|
||||||
"react-router": "~2.6.1",
|
"react-router": "~2.6.1",
|
||||||
"react-router-redux": "~4.0.5",
|
"react-router-redux": "~4.0.5",
|
||||||
"react-tap-event-plugin": "~1.0.0",
|
"react-tap-event-plugin": "~2.0.1",
|
||||||
"react-tooltip": "~2.0.3",
|
"react-tooltip": "~2.0.3",
|
||||||
"recharts": "~0.15.2",
|
"recharts": "~0.15.2",
|
||||||
"redux": "~3.5.2",
|
"redux": "~3.5.2",
|
||||||
|
@ -27,9 +27,5 @@ export function sliceData (_data) {
|
|||||||
data = padAddress('');
|
data = padAddress('');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.length % 64) {
|
|
||||||
throw new Error(`Invalid data length (not mod 64) passed to sliceData, ${data}, % 64 == ${data.length % 64}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return data.match(/.{1,64}/g);
|
return data.match(/.{1,64}/g);
|
||||||
}
|
}
|
||||||
|
@ -240,9 +240,29 @@ export default class Contract {
|
|||||||
return this.unsubscribe(subscriptionId);
|
return this.unsubscribe(subscriptionId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
event.getAllLogs = (options = {}) => {
|
||||||
|
return this.getAllLogs(event);
|
||||||
|
};
|
||||||
|
|
||||||
return event;
|
return event;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAllLogs (event, _options) {
|
||||||
|
// Options as first parameter
|
||||||
|
if (!_options && event && event.topics) {
|
||||||
|
return this.getAllLogs(null, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = this._getFilterOptions(event, _options);
|
||||||
|
return this._api.eth
|
||||||
|
.getLogs({
|
||||||
|
fromBlock: 0,
|
||||||
|
toBlock: 'latest',
|
||||||
|
...options
|
||||||
|
})
|
||||||
|
.then((logs) => this.parseEventLogs(logs));
|
||||||
|
}
|
||||||
|
|
||||||
_findEvent (eventName = null) {
|
_findEvent (eventName = null) {
|
||||||
const event = eventName
|
const event = eventName
|
||||||
? this._events.find((evt) => evt.name === eventName)
|
? this._events.find((evt) => evt.name === eventName)
|
||||||
@ -256,7 +276,7 @@ export default class Contract {
|
|||||||
return event;
|
return event;
|
||||||
}
|
}
|
||||||
|
|
||||||
_createEthFilter (event = null, _options) {
|
_getFilterOptions (event = null, _options = {}) {
|
||||||
const optionTopics = _options.topics || [];
|
const optionTopics = _options.topics || [];
|
||||||
const signature = event && event.signature || null;
|
const signature = event && event.signature || null;
|
||||||
|
|
||||||
@ -271,6 +291,11 @@ export default class Contract {
|
|||||||
topics
|
topics
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
_createEthFilter (event = null, _options) {
|
||||||
|
const options = this._getFilterOptions(event, _options);
|
||||||
return this._api.eth.newFilter(options);
|
return this._api.eth.newFilter(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -146,7 +146,8 @@ export default class Eth {
|
|||||||
|
|
||||||
getLogs (options) {
|
getLogs (options) {
|
||||||
return this._transport
|
return this._transport
|
||||||
.execute('eth_getLogs', inFilter(options));
|
.execute('eth_getLogs', inFilter(options))
|
||||||
|
.then((logs) => logs.map(outLog));
|
||||||
}
|
}
|
||||||
|
|
||||||
getLogsEx (options) {
|
getLogsEx (options) {
|
||||||
|
@ -32,6 +32,10 @@ export function hex2Ascii (_hex) {
|
|||||||
return str;
|
return str;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function bytesToAscii (bytes) {
|
||||||
|
return bytes.map((b) => String.fromCharCode(b % 512)).join('');
|
||||||
|
}
|
||||||
|
|
||||||
export function asciiToHex (string) {
|
export function asciiToHex (string) {
|
||||||
return '0x' + string.split('').map((s) => s.charCodeAt(0).toString(16)).join('');
|
return '0x' + string.split('').map((s) => s.charCodeAt(0).toString(16)).join('');
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
<style>
|
<style>
|
||||||
html, body, #container {
|
html, body, #container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
min-height: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background: white;
|
background: white;
|
||||||
|
@ -18,6 +18,8 @@ import BigNumber from 'bignumber.js';
|
|||||||
import React, { Component, PropTypes } from 'react';
|
import React, { Component, PropTypes } from 'react';
|
||||||
import { Checkbox, MenuItem } from 'material-ui';
|
import { Checkbox, MenuItem } from 'material-ui';
|
||||||
|
|
||||||
|
import { isEqual } from 'lodash';
|
||||||
|
|
||||||
import Form, { Input, InputAddressSelect, Select } from '../../../ui/Form';
|
import Form, { Input, InputAddressSelect, Select } from '../../../ui/Form';
|
||||||
|
|
||||||
import imageUnknown from '../../../../assets/images/contracts/unknown-64x64.png';
|
import imageUnknown from '../../../../assets/images/contracts/unknown-64x64.png';
|
||||||
@ -29,11 +31,101 @@ const CHECK_STYLE = {
|
|||||||
left: '1em'
|
left: '1em'
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class Details extends Component {
|
class TokenSelect extends Component {
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
api: PropTypes.object
|
api: PropTypes.object
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
balance: PropTypes.object.isRequired,
|
||||||
|
images: PropTypes.object.isRequired,
|
||||||
|
tag: PropTypes.string.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
componentWillMount () {
|
||||||
|
this.computeTokens();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps (nextProps) {
|
||||||
|
const prevTokens = this.props.balance.tokens.map((t) => `${t.token.tag}_${t.value.toNumber()}`);
|
||||||
|
const nextTokens = nextProps.balance.tokens.map((t) => `${t.token.tag}_${t.value.toNumber()}`);
|
||||||
|
|
||||||
|
if (!isEqual(prevTokens, nextTokens)) {
|
||||||
|
this.computeTokens(nextProps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
computeTokens (props = this.props) {
|
||||||
|
const { api } = this.context;
|
||||||
|
const { balance, images } = this.props;
|
||||||
|
|
||||||
|
const items = balance.tokens
|
||||||
|
.filter((token, index) => !index || token.value.gt(0))
|
||||||
|
.map((balance, index) => {
|
||||||
|
const token = balance.token;
|
||||||
|
const isEth = index === 0;
|
||||||
|
let imagesrc = token.image;
|
||||||
|
if (!imagesrc) {
|
||||||
|
imagesrc =
|
||||||
|
images[token.address]
|
||||||
|
? `${api.dappsUrl}${images[token.address]}`
|
||||||
|
: imageUnknown;
|
||||||
|
}
|
||||||
|
let value = 0;
|
||||||
|
|
||||||
|
if (isEth) {
|
||||||
|
value = api.util.fromWei(balance.value).toFormat(3);
|
||||||
|
} else {
|
||||||
|
const format = balance.token.format || 1;
|
||||||
|
const decimals = format === 1 ? 0 : Math.min(3, Math.floor(format / 10));
|
||||||
|
value = new BigNumber(balance.value).div(format).toFormat(decimals);
|
||||||
|
}
|
||||||
|
|
||||||
|
const label = (
|
||||||
|
<div className={ styles.token }>
|
||||||
|
<img src={ imagesrc } />
|
||||||
|
<div className={ styles.tokenname }>
|
||||||
|
{ token.name }
|
||||||
|
</div>
|
||||||
|
<div className={ styles.tokenbalance }>
|
||||||
|
{ value }<small> { token.tag }</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuItem
|
||||||
|
key={ token.tag }
|
||||||
|
value={ token.tag }
|
||||||
|
label={ label }>
|
||||||
|
{ label }
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.setState({ items });
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { tag, onChange } = this.props;
|
||||||
|
const { items } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
className={ styles.tokenSelect }
|
||||||
|
label='type of token transfer'
|
||||||
|
hint='type of token to transfer'
|
||||||
|
value={ tag }
|
||||||
|
onChange={ onChange }
|
||||||
|
>
|
||||||
|
{ items }
|
||||||
|
</Select>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class Details extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
address: PropTypes.string,
|
address: PropTypes.string,
|
||||||
balance: PropTypes.object,
|
balance: PropTypes.object,
|
||||||
@ -115,62 +207,15 @@ export default class Details extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderTokenSelect () {
|
renderTokenSelect () {
|
||||||
const { api } = this.context;
|
|
||||||
const { balance, images, tag } = this.props;
|
const { balance, images, tag } = this.props;
|
||||||
|
|
||||||
const items = balance.tokens
|
|
||||||
.filter((token, index) => !index || token.value.gt(0))
|
|
||||||
.map((balance, index) => {
|
|
||||||
const token = balance.token;
|
|
||||||
const isEth = index === 0;
|
|
||||||
let imagesrc = token.image;
|
|
||||||
if (!imagesrc) {
|
|
||||||
imagesrc =
|
|
||||||
images[token.address]
|
|
||||||
? `${api.dappsUrl}${images[token.address]}`
|
|
||||||
: imageUnknown;
|
|
||||||
}
|
|
||||||
let value = 0;
|
|
||||||
|
|
||||||
if (isEth) {
|
|
||||||
value = api.util.fromWei(balance.value).toFormat(3);
|
|
||||||
} else {
|
|
||||||
const format = balance.token.format || 1;
|
|
||||||
const decimals = format === 1 ? 0 : Math.min(3, Math.floor(format / 10));
|
|
||||||
value = new BigNumber(balance.value).div(format).toFormat(decimals);
|
|
||||||
}
|
|
||||||
|
|
||||||
const label = (
|
|
||||||
<div className={ styles.token }>
|
|
||||||
<img src={ imagesrc } />
|
|
||||||
<div className={ styles.tokenname }>
|
|
||||||
{ token.name }
|
|
||||||
</div>
|
|
||||||
<div className={ styles.tokenbalance }>
|
|
||||||
{ value }<small> { token.tag }</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<MenuItem
|
|
||||||
key={ token.tag }
|
|
||||||
value={ token.tag }
|
|
||||||
label={ label }>
|
|
||||||
{ label }
|
|
||||||
</MenuItem>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<TokenSelect
|
||||||
className={ styles.tokenSelect }
|
balance={ balance }
|
||||||
label='type of token transfer'
|
images={ images }
|
||||||
hint='type of token to transfer'
|
tag={ tag }
|
||||||
value={ tag }
|
onChange={ this.onChangeToken }
|
||||||
onChange={ this.onChangeToken }>
|
/>
|
||||||
{ items }
|
|
||||||
</Select>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
const ERRORS = {
|
const ERRORS = {
|
||||||
|
requireSender: 'a valid sender is required for the transaction',
|
||||||
requireRecipient: 'a recipient network address is required for the transaction',
|
requireRecipient: 'a recipient network address is required for the transaction',
|
||||||
invalidAddress: 'the supplied address is an invalid network address',
|
invalidAddress: 'the supplied address is an invalid network address',
|
||||||
invalidAmount: 'the supplied amount should be a valid positive number',
|
invalidAmount: 'the supplied amount should be a valid positive number',
|
||||||
|
463
js/src/modals/Transfer/store.js
Normal file
463
js/src/modals/Transfer/store.js
Normal file
@ -0,0 +1,463 @@
|
|||||||
|
// Copyright 2015, 2016 Ethcore (UK) Ltd.
|
||||||
|
// This file is part of Parity.
|
||||||
|
|
||||||
|
// Parity 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 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
|
||||||
|
// Parity 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 Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import { observable, computed, action, transaction } from 'mobx';
|
||||||
|
import BigNumber from 'bignumber.js';
|
||||||
|
|
||||||
|
import ERRORS from './errors';
|
||||||
|
import { ERROR_CODES } from '../../api/transport/error';
|
||||||
|
import { DEFAULT_GAS, DEFAULT_GASPRICE, MAX_GAS_ESTIMATION } from '../../util/constants';
|
||||||
|
|
||||||
|
const TITLES = {
|
||||||
|
transfer: 'transfer details',
|
||||||
|
sending: 'sending',
|
||||||
|
complete: 'complete',
|
||||||
|
extras: 'extra information',
|
||||||
|
rejected: 'rejected'
|
||||||
|
};
|
||||||
|
const STAGES_BASIC = [TITLES.transfer, TITLES.sending, TITLES.complete];
|
||||||
|
const STAGES_EXTRA = [TITLES.transfer, TITLES.extras, TITLES.sending, TITLES.complete];
|
||||||
|
|
||||||
|
export default class TransferStore {
|
||||||
|
@observable stage = 0;
|
||||||
|
@observable data = '';
|
||||||
|
@observable dataError = null;
|
||||||
|
@observable extras = false;
|
||||||
|
@observable gas = DEFAULT_GAS;
|
||||||
|
@observable gasEst = '0';
|
||||||
|
@observable gasError = null;
|
||||||
|
@observable gasLimitError = null;
|
||||||
|
@observable gasPrice = DEFAULT_GASPRICE;
|
||||||
|
@observable gasPriceError = null;
|
||||||
|
@observable recipient = '';
|
||||||
|
@observable recipientError = ERRORS.requireRecipient;
|
||||||
|
@observable sending = false;
|
||||||
|
@observable tag = 'ETH';
|
||||||
|
@observable total = '0.0';
|
||||||
|
@observable totalError = null;
|
||||||
|
@observable value = '0.0';
|
||||||
|
@observable valueAll = false;
|
||||||
|
@observable valueError = null;
|
||||||
|
@observable isEth = true;
|
||||||
|
@observable busyState = null;
|
||||||
|
@observable rejected = false;
|
||||||
|
|
||||||
|
gasPriceHistogram = {};
|
||||||
|
|
||||||
|
account = null;
|
||||||
|
balance = null;
|
||||||
|
gasLimit = null;
|
||||||
|
onClose = null;
|
||||||
|
|
||||||
|
@computed get steps () {
|
||||||
|
const steps = [].concat(this.extras ? STAGES_EXTRA : STAGES_BASIC);
|
||||||
|
|
||||||
|
if (this.rejected) {
|
||||||
|
steps[steps.length - 1] = TITLES.rejected;
|
||||||
|
}
|
||||||
|
|
||||||
|
return steps;
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed get isValid () {
|
||||||
|
const detailsValid = !this.recipientError && !this.valueError && !this.totalError;
|
||||||
|
const extrasValid = !this.gasError && !this.gasPriceError && !this.totalError;
|
||||||
|
const verifyValid = !this.passwordError;
|
||||||
|
|
||||||
|
switch (this.stage) {
|
||||||
|
case 0:
|
||||||
|
return detailsValid;
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
return this.extras ? extrasValid : verifyValid;
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
return verifyValid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor (api, props) {
|
||||||
|
this.api = api;
|
||||||
|
|
||||||
|
const { account, balance, gasLimit, onClose } = props;
|
||||||
|
|
||||||
|
this.account = account;
|
||||||
|
this.balance = balance;
|
||||||
|
this.gasLimit = gasLimit;
|
||||||
|
this.onClose = onClose;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action onNext = () => {
|
||||||
|
this.stage += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action onPrev = () => {
|
||||||
|
this.stage -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action onClose = () => {
|
||||||
|
this.onClose && this.onClose();
|
||||||
|
this.stage = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action onUpdateDetails = (type, value) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'all':
|
||||||
|
return this._onUpdateAll(value);
|
||||||
|
|
||||||
|
case 'extras':
|
||||||
|
return this._onUpdateExtras(value);
|
||||||
|
|
||||||
|
case 'data':
|
||||||
|
return this._onUpdateData(value);
|
||||||
|
|
||||||
|
case 'gas':
|
||||||
|
return this._onUpdateGas(value);
|
||||||
|
|
||||||
|
case 'gasPrice':
|
||||||
|
return this._onUpdateGasPrice(value);
|
||||||
|
|
||||||
|
case 'recipient':
|
||||||
|
return this._onUpdateRecipient(value);
|
||||||
|
|
||||||
|
case 'tag':
|
||||||
|
return this._onUpdateTag(value);
|
||||||
|
|
||||||
|
case 'value':
|
||||||
|
return this._onUpdateValue(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action getDefaults = () => {
|
||||||
|
Promise
|
||||||
|
.all([
|
||||||
|
this.api.parity.gasPriceHistogram(),
|
||||||
|
this.api.eth.gasPrice()
|
||||||
|
])
|
||||||
|
.then(([gasPriceHistogram, gasPrice]) => {
|
||||||
|
transaction(() => {
|
||||||
|
this.gasPrice = gasPrice.toString();
|
||||||
|
this.gasPriceDefault = gasPrice.toFormat();
|
||||||
|
this.gasPriceHistogram = gasPriceHistogram;
|
||||||
|
|
||||||
|
this.recalculate();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.warn('getDefaults', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@action onSend = () => {
|
||||||
|
this.onNext();
|
||||||
|
this.sending = true;
|
||||||
|
|
||||||
|
const promise = this.isEth ? this._sendEth() : this._sendToken();
|
||||||
|
|
||||||
|
promise
|
||||||
|
.then((requestId) => {
|
||||||
|
this.busyState = 'Waiting for authorization in the Parity Signer';
|
||||||
|
|
||||||
|
return this.api
|
||||||
|
.pollMethod('parity_checkRequest', requestId)
|
||||||
|
.catch((e) => {
|
||||||
|
if (e.code === ERROR_CODES.REQUEST_REJECTED) {
|
||||||
|
this.rejected = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then((txhash) => {
|
||||||
|
transaction(() => {
|
||||||
|
this.onNext();
|
||||||
|
|
||||||
|
this.sending = false;
|
||||||
|
this.txhash = txhash;
|
||||||
|
this.busyState = 'Your transaction has been posted to the network';
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
this.sending = false;
|
||||||
|
this.newError(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@action _onUpdateAll = (valueAll) => {
|
||||||
|
this.valueAll = valueAll;
|
||||||
|
this.recalculateGas();
|
||||||
|
}
|
||||||
|
|
||||||
|
@action _onUpdateExtras = (extras) => {
|
||||||
|
this.extras = extras;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action _onUpdateData = (data) => {
|
||||||
|
this.data = data;
|
||||||
|
this.recalculateGas();
|
||||||
|
}
|
||||||
|
|
||||||
|
@action _onUpdateGas = (gas) => {
|
||||||
|
const gasError = this._validatePositiveNumber(gas);
|
||||||
|
|
||||||
|
transaction(() => {
|
||||||
|
this.gas = gas;
|
||||||
|
this.gasError = gasError;
|
||||||
|
|
||||||
|
this.recalculate();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@action _onUpdateGasPrice = (gasPrice) => {
|
||||||
|
const gasPriceError = this._validatePositiveNumber(gasPrice);
|
||||||
|
|
||||||
|
transaction(() => {
|
||||||
|
this.gasPrice = gasPrice;
|
||||||
|
this.gasPriceError = gasPriceError;
|
||||||
|
|
||||||
|
this.recalculate();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@action _onUpdateRecipient = (recipient) => {
|
||||||
|
let recipientError = null;
|
||||||
|
|
||||||
|
if (!recipient || !recipient.length) {
|
||||||
|
recipientError = ERRORS.requireRecipient;
|
||||||
|
} else if (!this.api.util.isAddressValid(recipient)) {
|
||||||
|
recipientError = ERRORS.invalidAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction(() => {
|
||||||
|
this.recipient = recipient;
|
||||||
|
this.recipientError = recipientError;
|
||||||
|
|
||||||
|
this.recalculateGas();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@action _onUpdateTag = (tag) => {
|
||||||
|
transaction(() => {
|
||||||
|
this.tag = tag;
|
||||||
|
this.isEth = tag.toLowerCase().trim() === 'eth';
|
||||||
|
|
||||||
|
this.recalculateGas();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@action _onUpdateValue = (value) => {
|
||||||
|
let valueError = this._validatePositiveNumber(value);
|
||||||
|
|
||||||
|
if (!valueError) {
|
||||||
|
valueError = this._validateDecimals(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction(() => {
|
||||||
|
this.value = value;
|
||||||
|
this.valueError = valueError;
|
||||||
|
|
||||||
|
this.recalculateGas();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@action recalculateGas = () => {
|
||||||
|
if (!this.isValid) {
|
||||||
|
this.gas = 0;
|
||||||
|
return this.recalculate();
|
||||||
|
}
|
||||||
|
|
||||||
|
const promise = this.isEth ? this._estimateGasEth() : this._estimateGasToken();
|
||||||
|
|
||||||
|
promise
|
||||||
|
.then((gasEst) => {
|
||||||
|
let gas = gasEst;
|
||||||
|
let gasLimitError = null;
|
||||||
|
|
||||||
|
if (gas.gt(DEFAULT_GAS)) {
|
||||||
|
gas = gas.mul(1.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gas.gte(MAX_GAS_ESTIMATION)) {
|
||||||
|
gasLimitError = ERRORS.gasException;
|
||||||
|
} else if (gas.gt(this.gasLimit)) {
|
||||||
|
gasLimitError = ERRORS.gasBlockLimit;
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction(() => {
|
||||||
|
this.gas = gas.toFixed(0);
|
||||||
|
this.gasEst = gasEst.toFormat();
|
||||||
|
this.gasLimitError = gasLimitError;
|
||||||
|
|
||||||
|
this.recalculate();
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('etimateGas', error);
|
||||||
|
this.recalculate();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@action recalculate = () => {
|
||||||
|
const { account, balance } = this;
|
||||||
|
|
||||||
|
if (!account || !balance) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { gas, gasPrice, tag, valueAll, isEth } = this;
|
||||||
|
|
||||||
|
const gasTotal = new BigNumber(gasPrice || 0).mul(new BigNumber(gas || 0));
|
||||||
|
const balance_ = balance.tokens.find((b) => tag === b.token.tag);
|
||||||
|
const availableEth = new BigNumber(balance.tokens[0].value);
|
||||||
|
const available = new BigNumber(balance_.value);
|
||||||
|
const format = new BigNumber(balance_.token.format || 1);
|
||||||
|
|
||||||
|
let { value, valueError } = this;
|
||||||
|
let totalEth = gasTotal;
|
||||||
|
let totalError = null;
|
||||||
|
|
||||||
|
if (valueAll) {
|
||||||
|
if (isEth) {
|
||||||
|
const bn = this.api.util.fromWei(availableEth.minus(gasTotal));
|
||||||
|
value = (bn.lt(0) ? new BigNumber(0.0) : bn).toString();
|
||||||
|
} else {
|
||||||
|
value = available.div(format).toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEth) {
|
||||||
|
totalEth = totalEth.plus(this.api.util.toWei(value || 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (new BigNumber(value || 0).gt(available.div(format))) {
|
||||||
|
valueError = ERRORS.largeAmount;
|
||||||
|
} else if (valueError === ERRORS.largeAmount) {
|
||||||
|
valueError = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalEth.gt(availableEth)) {
|
||||||
|
totalError = ERRORS.largeAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction(() => {
|
||||||
|
this.total = this.api.util.fromWei(totalEth).toString();
|
||||||
|
this.totalError = totalError;
|
||||||
|
this.value = value;
|
||||||
|
this.valueError = valueError;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_sendEth () {
|
||||||
|
const { account, data, gas, gasPrice, recipient, value } = this;
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
from: account.address,
|
||||||
|
to: recipient,
|
||||||
|
gas,
|
||||||
|
gasPrice,
|
||||||
|
value: this.api.util.toWei(value || 0)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (data && data.length) {
|
||||||
|
options.data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.api.parity.postTransaction(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
_sendToken () {
|
||||||
|
const { account, balance } = this;
|
||||||
|
const { gas, gasPrice, recipient, value, tag } = this;
|
||||||
|
|
||||||
|
const token = balance.tokens.find((balance) => balance.token.tag === tag).token;
|
||||||
|
|
||||||
|
return token.contract.instance.transfer
|
||||||
|
.postTransaction({
|
||||||
|
from: account.address,
|
||||||
|
to: token.address,
|
||||||
|
gas,
|
||||||
|
gasPrice
|
||||||
|
}, [
|
||||||
|
recipient,
|
||||||
|
new BigNumber(value).mul(token.format).toFixed(0)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
_estimateGasToken () {
|
||||||
|
const { account, balance } = this;
|
||||||
|
const { recipient, value, tag } = this;
|
||||||
|
|
||||||
|
const token = balance.tokens.find((balance) => balance.token.tag === tag).token;
|
||||||
|
|
||||||
|
return token.contract.instance.transfer
|
||||||
|
.estimateGas({
|
||||||
|
gas: MAX_GAS_ESTIMATION,
|
||||||
|
from: account.address,
|
||||||
|
to: token.address
|
||||||
|
}, [
|
||||||
|
recipient,
|
||||||
|
new BigNumber(value || 0).mul(token.format).toFixed(0)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
_estimateGasEth () {
|
||||||
|
const { account, data, recipient, value } = this;
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
gas: MAX_GAS_ESTIMATION,
|
||||||
|
from: account.address,
|
||||||
|
to: recipient,
|
||||||
|
value: this.api.util.toWei(value || 0)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (data && data.length) {
|
||||||
|
options.data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.api.eth.estimateGas(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
_validatePositiveNumber (num) {
|
||||||
|
try {
|
||||||
|
const v = new BigNumber(num);
|
||||||
|
if (v.lt(0)) {
|
||||||
|
return ERRORS.invalidAmount;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return ERRORS.invalidAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_validateDecimals (num) {
|
||||||
|
const { balance } = this;
|
||||||
|
|
||||||
|
if (this.tag === 'ETH') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = balance.tokens.find((balance) => balance.token.tag === this.tag).token;
|
||||||
|
const s = new BigNumber(num).mul(token.format || 1).toFixed();
|
||||||
|
|
||||||
|
if (s.indexOf('.') !== -1) {
|
||||||
|
return ERRORS.invalidDecimals;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
@ -14,88 +14,50 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import BigNumber from 'bignumber.js';
|
|
||||||
import React, { Component, PropTypes } from 'react';
|
import React, { Component, PropTypes } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
|
|
||||||
import ActionDoneAll from 'material-ui/svg-icons/action/done-all';
|
import ActionDoneAll from 'material-ui/svg-icons/action/done-all';
|
||||||
import ContentClear from 'material-ui/svg-icons/content/clear';
|
import ContentClear from 'material-ui/svg-icons/content/clear';
|
||||||
import NavigationArrowBack from 'material-ui/svg-icons/navigation/arrow-back';
|
import NavigationArrowBack from 'material-ui/svg-icons/navigation/arrow-back';
|
||||||
import NavigationArrowForward from 'material-ui/svg-icons/navigation/arrow-forward';
|
import NavigationArrowForward from 'material-ui/svg-icons/navigation/arrow-forward';
|
||||||
|
|
||||||
|
import { newError } from '../../ui/Errors/actions';
|
||||||
import { BusyStep, CompletedStep, Button, IdentityIcon, Modal, TxHash } from '../../ui';
|
import { BusyStep, CompletedStep, Button, IdentityIcon, Modal, TxHash } from '../../ui';
|
||||||
import { DEFAULT_GAS, DEFAULT_GASPRICE, MAX_GAS_ESTIMATION } from '../../util/constants';
|
|
||||||
|
|
||||||
import Details from './Details';
|
import Details from './Details';
|
||||||
import Extras from './Extras';
|
import Extras from './Extras';
|
||||||
import ERRORS from './errors';
|
|
||||||
|
import TransferStore from './store';
|
||||||
import styles from './transfer.css';
|
import styles from './transfer.css';
|
||||||
|
|
||||||
import { ERROR_CODES } from '../../api/transport/error';
|
@observer
|
||||||
|
|
||||||
const TITLES = {
|
|
||||||
transfer: 'transfer details',
|
|
||||||
sending: 'sending',
|
|
||||||
complete: 'complete',
|
|
||||||
extras: 'extra information',
|
|
||||||
rejected: 'rejected'
|
|
||||||
};
|
|
||||||
const STAGES_BASIC = [TITLES.transfer, TITLES.sending, TITLES.complete];
|
|
||||||
const STAGES_EXTRA = [TITLES.transfer, TITLES.extras, TITLES.sending, TITLES.complete];
|
|
||||||
|
|
||||||
class Transfer extends Component {
|
class Transfer extends Component {
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
api: PropTypes.object.isRequired,
|
api: PropTypes.object.isRequired
|
||||||
store: PropTypes.object.isRequired
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
newError: PropTypes.func.isRequired,
|
||||||
|
gasLimit: PropTypes.object.isRequired,
|
||||||
|
images: PropTypes.object.isRequired,
|
||||||
|
|
||||||
account: PropTypes.object,
|
account: PropTypes.object,
|
||||||
balance: PropTypes.object,
|
balance: PropTypes.object,
|
||||||
balances: PropTypes.object,
|
balances: PropTypes.object,
|
||||||
gasLimit: PropTypes.object.isRequired,
|
|
||||||
images: PropTypes.object.isRequired,
|
|
||||||
onClose: PropTypes.func
|
onClose: PropTypes.func
|
||||||
}
|
}
|
||||||
|
|
||||||
state = {
|
store = new TransferStore(this.context.api, this.props);
|
||||||
stage: 0,
|
|
||||||
data: '',
|
|
||||||
dataError: null,
|
|
||||||
extras: false,
|
|
||||||
gas: DEFAULT_GAS,
|
|
||||||
gasEst: '0',
|
|
||||||
gasError: null,
|
|
||||||
gasLimitError: null,
|
|
||||||
gasPrice: DEFAULT_GASPRICE,
|
|
||||||
gasPriceHistogram: {},
|
|
||||||
gasPriceError: null,
|
|
||||||
recipient: '',
|
|
||||||
recipientError: ERRORS.requireRecipient,
|
|
||||||
sending: false,
|
|
||||||
tag: 'ETH',
|
|
||||||
total: '0.0',
|
|
||||||
totalError: null,
|
|
||||||
value: '0.0',
|
|
||||||
valueAll: false,
|
|
||||||
valueError: null,
|
|
||||||
isEth: true,
|
|
||||||
busyState: null,
|
|
||||||
rejected: false
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
this.getDefaults();
|
this.store.getDefaults();
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { stage, extras, rejected } = this.state;
|
const { stage, extras, steps } = this.store;
|
||||||
|
|
||||||
const steps = [].concat(extras ? STAGES_EXTRA : STAGES_BASIC);
|
|
||||||
|
|
||||||
if (rejected) {
|
|
||||||
steps[steps.length - 1] = TITLES.rejected;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
@ -134,7 +96,7 @@ class Transfer extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderPage () {
|
renderPage () {
|
||||||
const { extras, stage } = this.state;
|
const { extras, stage } = this.store;
|
||||||
|
|
||||||
if (stage === 0) {
|
if (stage === 0) {
|
||||||
return this.renderDetailsPage();
|
return this.renderDetailsPage();
|
||||||
@ -146,7 +108,7 @@ class Transfer extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderCompletePage () {
|
renderCompletePage () {
|
||||||
const { sending, txhash, busyState, rejected } = this.state;
|
const { sending, txhash, busyState, rejected } = this.store;
|
||||||
|
|
||||||
if (rejected) {
|
if (rejected) {
|
||||||
return (
|
return (
|
||||||
@ -174,83 +136,88 @@ class Transfer extends Component {
|
|||||||
|
|
||||||
renderDetailsPage () {
|
renderDetailsPage () {
|
||||||
const { account, balance, images } = this.props;
|
const { account, balance, images } = this.props;
|
||||||
|
const { valueAll, extras, recipient, recipientError, tag } = this.store;
|
||||||
|
const { total, totalError, value, valueError } = this.store;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Details
|
<Details
|
||||||
address={ account.address }
|
address={ account.address }
|
||||||
all={ this.state.valueAll }
|
all={ valueAll }
|
||||||
balance={ balance }
|
balance={ balance }
|
||||||
extras={ this.state.extras }
|
extras={ extras }
|
||||||
images={ images }
|
images={ images }
|
||||||
recipient={ this.state.recipient }
|
recipient={ recipient }
|
||||||
recipientError={ this.state.recipientError }
|
recipientError={ recipientError }
|
||||||
tag={ this.state.tag }
|
tag={ tag }
|
||||||
total={ this.state.total }
|
total={ total }
|
||||||
totalError={ this.state.totalError }
|
totalError={ totalError }
|
||||||
value={ this.state.value }
|
value={ value }
|
||||||
valueError={ this.state.valueError }
|
valueError={ valueError }
|
||||||
onChange={ this.onUpdateDetails } />
|
onChange={ this.store.onUpdateDetails } />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderExtrasPage () {
|
renderExtrasPage () {
|
||||||
if (!this.state.gasPriceHistogram) {
|
if (!this.store.gasPriceHistogram) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { isEth, data, dataError, gas, gasEst, gasError, gasPrice } = this.store;
|
||||||
|
const { gasPriceDefault, gasPriceError, gasPriceHistogram, total, totalError } = this.store;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Extras
|
<Extras
|
||||||
isEth={ this.state.isEth }
|
isEth={ isEth }
|
||||||
data={ this.state.data }
|
data={ data }
|
||||||
dataError={ this.state.dataError }
|
dataError={ dataError }
|
||||||
gas={ this.state.gas }
|
gas={ gas }
|
||||||
gasEst={ this.state.gasEst }
|
gasEst={ gasEst }
|
||||||
gasError={ this.state.gasError }
|
gasError={ gasError }
|
||||||
gasPrice={ this.state.gasPrice }
|
gasPrice={ gasPrice }
|
||||||
gasPriceDefault={ this.state.gasPriceDefault }
|
gasPriceDefault={ gasPriceDefault }
|
||||||
gasPriceError={ this.state.gasPriceError }
|
gasPriceError={ gasPriceError }
|
||||||
gasPriceHistogram={ this.state.gasPriceHistogram }
|
gasPriceHistogram={ gasPriceHistogram }
|
||||||
total={ this.state.total }
|
total={ total }
|
||||||
totalError={ this.state.totalError }
|
totalError={ totalError }
|
||||||
onChange={ this.onUpdateDetails } />
|
onChange={ this.store.onUpdateDetails } />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderDialogActions () {
|
renderDialogActions () {
|
||||||
const { account } = this.props;
|
const { account } = this.props;
|
||||||
const { extras, sending, stage } = this.state;
|
const { extras, sending, stage } = this.store;
|
||||||
|
|
||||||
const cancelBtn = (
|
const cancelBtn = (
|
||||||
<Button
|
<Button
|
||||||
icon={ <ContentClear /> }
|
icon={ <ContentClear /> }
|
||||||
label='Cancel'
|
label='Cancel'
|
||||||
onClick={ this.onClose } />
|
onClick={ this.store.onClose } />
|
||||||
);
|
);
|
||||||
const nextBtn = (
|
const nextBtn = (
|
||||||
<Button
|
<Button
|
||||||
disabled={ !this.isValid() }
|
disabled={ !this.store.isValid }
|
||||||
icon={ <NavigationArrowForward /> }
|
icon={ <NavigationArrowForward /> }
|
||||||
label='Next'
|
label='Next'
|
||||||
onClick={ this.onNext } />
|
onClick={ this.store.onNext } />
|
||||||
);
|
);
|
||||||
const prevBtn = (
|
const prevBtn = (
|
||||||
<Button
|
<Button
|
||||||
icon={ <NavigationArrowBack /> }
|
icon={ <NavigationArrowBack /> }
|
||||||
label='Back'
|
label='Back'
|
||||||
onClick={ this.onPrev } />
|
onClick={ this.store.onPrev } />
|
||||||
);
|
);
|
||||||
const sendBtn = (
|
const sendBtn = (
|
||||||
<Button
|
<Button
|
||||||
disabled={ !this.isValid() || sending }
|
disabled={ !this.store.isValid || sending }
|
||||||
icon={ <IdentityIcon address={ account.address } button /> }
|
icon={ <IdentityIcon address={ account.address } button /> }
|
||||||
label='Send'
|
label='Send'
|
||||||
onClick={ this.onSend } />
|
onClick={ this.store.onSend } />
|
||||||
);
|
);
|
||||||
const doneBtn = (
|
const doneBtn = (
|
||||||
<Button
|
<Button
|
||||||
icon={ <ActionDoneAll /> }
|
icon={ <ActionDoneAll /> }
|
||||||
label='Close'
|
label='Close'
|
||||||
onClick={ this.onClose } />
|
onClick={ this.store.onClose } />
|
||||||
);
|
);
|
||||||
|
|
||||||
switch (stage) {
|
switch (stage) {
|
||||||
@ -268,7 +235,7 @@ class Transfer extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderWarning () {
|
renderWarning () {
|
||||||
const { gasLimitError } = this.state;
|
const { gasLimitError } = this.store;
|
||||||
|
|
||||||
if (!gasLimitError) {
|
if (!gasLimitError) {
|
||||||
return null;
|
return null;
|
||||||
@ -280,413 +247,17 @@ class Transfer extends Component {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
isValid () {
|
|
||||||
const detailsValid = !this.state.recipientError && !this.state.valueError && !this.state.totalError;
|
|
||||||
const extrasValid = !this.state.gasError && !this.state.gasPriceError && !this.state.totalError;
|
|
||||||
const verifyValid = !this.state.passwordError;
|
|
||||||
|
|
||||||
switch (this.state.stage) {
|
|
||||||
case 0:
|
|
||||||
return detailsValid;
|
|
||||||
|
|
||||||
case 1:
|
|
||||||
return this.state.extras ? extrasValid : verifyValid;
|
|
||||||
|
|
||||||
case 2:
|
|
||||||
return verifyValid;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onNext = () => {
|
|
||||||
this.setState({
|
|
||||||
stage: this.state.stage + 1
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onPrev = () => {
|
|
||||||
this.setState({
|
|
||||||
stage: this.state.stage - 1
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_onUpdateAll (valueAll) {
|
|
||||||
this.setState({
|
|
||||||
valueAll
|
|
||||||
}, this.recalculateGas);
|
|
||||||
}
|
|
||||||
|
|
||||||
_onUpdateExtras (extras) {
|
|
||||||
this.setState({
|
|
||||||
extras
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_onUpdateData (data) {
|
|
||||||
this.setState({
|
|
||||||
data
|
|
||||||
}, this.recalculateGas);
|
|
||||||
}
|
|
||||||
|
|
||||||
validatePositiveNumber (num) {
|
|
||||||
try {
|
|
||||||
const v = new BigNumber(num);
|
|
||||||
if (v.lt(0)) {
|
|
||||||
return ERRORS.invalidAmount;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
return ERRORS.invalidAmount;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
validateDecimals (num) {
|
|
||||||
const { balance } = this.props;
|
|
||||||
const { tag } = this.state;
|
|
||||||
|
|
||||||
if (tag === 'ETH') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = balance.tokens.find((balance) => balance.token.tag === tag).token;
|
|
||||||
const s = new BigNumber(num).mul(token.format || 1).toFixed();
|
|
||||||
|
|
||||||
if (s.indexOf('.') !== -1) {
|
|
||||||
return ERRORS.invalidDecimals;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
_onUpdateGas (gas) {
|
|
||||||
const gasError = this.validatePositiveNumber(gas);
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
gas,
|
|
||||||
gasError
|
|
||||||
}, this.recalculate);
|
|
||||||
}
|
|
||||||
|
|
||||||
_onUpdateGasPrice (gasPrice) {
|
|
||||||
const gasPriceError = this.validatePositiveNumber(gasPrice);
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
gasPrice,
|
|
||||||
gasPriceError
|
|
||||||
}, this.recalculate);
|
|
||||||
}
|
|
||||||
|
|
||||||
_onUpdateRecipient (recipient) {
|
|
||||||
const { api } = this.context;
|
|
||||||
let recipientError = null;
|
|
||||||
|
|
||||||
if (!recipient || !recipient.length) {
|
|
||||||
recipientError = ERRORS.requireRecipient;
|
|
||||||
} else if (!api.util.isAddressValid(recipient)) {
|
|
||||||
recipientError = ERRORS.invalidAddress;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
recipient,
|
|
||||||
recipientError
|
|
||||||
}, this.recalculateGas);
|
|
||||||
}
|
|
||||||
|
|
||||||
_onUpdateTag (tag) {
|
|
||||||
const { balance } = this.props;
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
tag,
|
|
||||||
isEth: tag === balance.tokens[0].token.tag
|
|
||||||
}, this.recalculateGas);
|
|
||||||
}
|
|
||||||
|
|
||||||
_onUpdateValue (value) {
|
|
||||||
let valueError = this.validatePositiveNumber(value);
|
|
||||||
|
|
||||||
if (!valueError) {
|
|
||||||
valueError = this.validateDecimals(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
value,
|
|
||||||
valueError
|
|
||||||
}, this.recalculateGas);
|
|
||||||
}
|
|
||||||
|
|
||||||
onUpdateDetails = (type, value) => {
|
|
||||||
switch (type) {
|
|
||||||
case 'all':
|
|
||||||
return this._onUpdateAll(value);
|
|
||||||
|
|
||||||
case 'extras':
|
|
||||||
return this._onUpdateExtras(value);
|
|
||||||
|
|
||||||
case 'data':
|
|
||||||
return this._onUpdateData(value);
|
|
||||||
|
|
||||||
case 'gas':
|
|
||||||
return this._onUpdateGas(value);
|
|
||||||
|
|
||||||
case 'gasPrice':
|
|
||||||
return this._onUpdateGasPrice(value);
|
|
||||||
|
|
||||||
case 'recipient':
|
|
||||||
return this._onUpdateRecipient(value);
|
|
||||||
|
|
||||||
case 'tag':
|
|
||||||
return this._onUpdateTag(value);
|
|
||||||
|
|
||||||
case 'value':
|
|
||||||
return this._onUpdateValue(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_sendEth () {
|
|
||||||
const { api } = this.context;
|
|
||||||
const { account } = this.props;
|
|
||||||
const { data, gas, gasPrice, recipient, value } = this.state;
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
from: account.address,
|
|
||||||
to: recipient,
|
|
||||||
gas,
|
|
||||||
gasPrice,
|
|
||||||
value: api.util.toWei(value || 0)
|
|
||||||
};
|
|
||||||
|
|
||||||
if (data && data.length) {
|
|
||||||
options.data = data;
|
|
||||||
}
|
|
||||||
|
|
||||||
return api.parity.postTransaction(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
_sendToken () {
|
|
||||||
const { account, balance } = this.props;
|
|
||||||
const { gas, gasPrice, recipient, value, tag } = this.state;
|
|
||||||
const token = balance.tokens.find((balance) => balance.token.tag === tag).token;
|
|
||||||
|
|
||||||
return token.contract.instance.transfer
|
|
||||||
.postTransaction({
|
|
||||||
from: account.address,
|
|
||||||
to: token.address,
|
|
||||||
gas,
|
|
||||||
gasPrice
|
|
||||||
}, [
|
|
||||||
recipient,
|
|
||||||
new BigNumber(value).mul(token.format).toFixed(0)
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
onSend = () => {
|
|
||||||
const { api } = this.context;
|
|
||||||
|
|
||||||
this.onNext();
|
|
||||||
|
|
||||||
this.setState({ sending: true }, () => {
|
|
||||||
(this.state.isEth
|
|
||||||
? this._sendEth()
|
|
||||||
: this._sendToken()
|
|
||||||
).then((requestId) => {
|
|
||||||
this.setState({ busyState: 'Waiting for authorization in the Parity Signer' });
|
|
||||||
|
|
||||||
return api
|
|
||||||
.pollMethod('parity_checkRequest', requestId)
|
|
||||||
.catch((e) => {
|
|
||||||
if (e.code === ERROR_CODES.REQUEST_REJECTED) {
|
|
||||||
this.setState({ rejected: true });
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw e;
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.then((txhash) => {
|
|
||||||
this.onNext();
|
|
||||||
this.setState({
|
|
||||||
sending: false,
|
|
||||||
txhash,
|
|
||||||
busyState: 'Your transaction has been posted to the network'
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.log('send', error);
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
sending: false
|
|
||||||
});
|
|
||||||
|
|
||||||
this.newError(error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onClose = () => {
|
|
||||||
this.setState({ stage: 0 }, () => {
|
|
||||||
this.props.onClose && this.props.onClose();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_estimateGasToken () {
|
|
||||||
const { account, balance } = this.props;
|
|
||||||
const { recipient, value, tag } = this.state;
|
|
||||||
const token = balance.tokens.find((balance) => balance.token.tag === tag).token;
|
|
||||||
|
|
||||||
return token.contract.instance.transfer
|
|
||||||
.estimateGas({
|
|
||||||
gas: MAX_GAS_ESTIMATION,
|
|
||||||
from: account.address,
|
|
||||||
to: token.address
|
|
||||||
}, [
|
|
||||||
recipient,
|
|
||||||
new BigNumber(value || 0).mul(token.format).toFixed(0)
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
_estimateGasEth () {
|
|
||||||
const { api } = this.context;
|
|
||||||
const { account } = this.props;
|
|
||||||
const { data, recipient, value } = this.state;
|
|
||||||
const options = {
|
|
||||||
gas: MAX_GAS_ESTIMATION,
|
|
||||||
from: account.address,
|
|
||||||
to: recipient,
|
|
||||||
value: api.util.toWei(value || 0)
|
|
||||||
};
|
|
||||||
|
|
||||||
if (data && data.length) {
|
|
||||||
options.data = data;
|
|
||||||
}
|
|
||||||
|
|
||||||
return api.eth.estimateGas(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
recalculateGas = () => {
|
|
||||||
if (!this.isValid()) {
|
|
||||||
this.setState({
|
|
||||||
gas: '0'
|
|
||||||
}, this.recalculate);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { gasLimit } = this.props;
|
|
||||||
|
|
||||||
(this.state.isEth
|
|
||||||
? this._estimateGasEth()
|
|
||||||
: this._estimateGasToken()
|
|
||||||
).then((gasEst) => {
|
|
||||||
let gas = gasEst;
|
|
||||||
let gasLimitError = null;
|
|
||||||
|
|
||||||
if (gas.gt(DEFAULT_GAS)) {
|
|
||||||
gas = gas.mul(1.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (gas.gte(MAX_GAS_ESTIMATION)) {
|
|
||||||
gasLimitError = ERRORS.gasException;
|
|
||||||
} else if (gas.gt(gasLimit)) {
|
|
||||||
gasLimitError = ERRORS.gasBlockLimit;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
gas: gas.toFixed(0),
|
|
||||||
gasEst: gasEst.toFormat(),
|
|
||||||
gasLimitError
|
|
||||||
}, this.recalculate);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('etimateGas', error);
|
|
||||||
this.recalculate();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
recalculate = () => {
|
|
||||||
const { api } = this.context;
|
|
||||||
const { account, balance } = this.props;
|
|
||||||
|
|
||||||
if (!account || !balance) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { gas, gasPrice, tag, valueAll, isEth } = this.state;
|
|
||||||
const gasTotal = new BigNumber(gasPrice || 0).mul(new BigNumber(gas || 0));
|
|
||||||
const balance_ = balance.tokens.find((b) => tag === b.token.tag);
|
|
||||||
const availableEth = new BigNumber(balance.tokens[0].value);
|
|
||||||
const available = new BigNumber(balance_.value);
|
|
||||||
const format = new BigNumber(balance_.token.format || 1);
|
|
||||||
|
|
||||||
let { value, valueError } = this.state;
|
|
||||||
let totalEth = gasTotal;
|
|
||||||
let totalError = null;
|
|
||||||
|
|
||||||
if (valueAll) {
|
|
||||||
if (isEth) {
|
|
||||||
const bn = api.util.fromWei(availableEth.minus(gasTotal));
|
|
||||||
value = (bn.lt(0) ? new BigNumber(0.0) : bn).toString();
|
|
||||||
} else {
|
|
||||||
value = available.div(format).toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isEth) {
|
|
||||||
totalEth = totalEth.plus(api.util.toWei(value || 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (new BigNumber(value || 0).gt(available.div(format))) {
|
|
||||||
valueError = ERRORS.largeAmount;
|
|
||||||
} else if (valueError === ERRORS.largeAmount) {
|
|
||||||
valueError = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (totalEth.gt(availableEth)) {
|
|
||||||
totalError = ERRORS.largeAmount;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
total: api.util.fromWei(totalEth).toString(),
|
|
||||||
totalError,
|
|
||||||
value,
|
|
||||||
valueError
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getDefaults = () => {
|
|
||||||
const { api } = this.context;
|
|
||||||
|
|
||||||
Promise
|
|
||||||
.all([
|
|
||||||
api.parity.gasPriceHistogram(),
|
|
||||||
api.eth.gasPrice()
|
|
||||||
])
|
|
||||||
.then(([gasPriceHistogram, gasPrice]) => {
|
|
||||||
this.setState({
|
|
||||||
gasPrice: gasPrice.toString(),
|
|
||||||
gasPriceDefault: gasPrice.toFormat(),
|
|
||||||
gasPriceHistogram
|
|
||||||
}, this.recalculate);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.warn('getDefaults', error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
newError = (error) => {
|
|
||||||
const { store } = this.context;
|
|
||||||
|
|
||||||
store.dispatch({ type: 'newError', error });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapStateToProps (state) {
|
function mapStateToProps (state) {
|
||||||
const { gasLimit } = state.nodeStatus;
|
const { gasLimit } = state.nodeStatus;
|
||||||
|
|
||||||
return { gasLimit };
|
return { gasLimit };
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapDispatchToProps (dispatch) {
|
function mapDispatchToProps (dispatch) {
|
||||||
return bindActionCreators({}, dispatch);
|
return bindActionCreators({
|
||||||
|
newError
|
||||||
|
}, dispatch);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
|
@ -256,7 +256,8 @@ export function queryTokensFilter (tokensFilter) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tokens = balances.tokens.filter((t) => tokenAddresses.includes(t.address));
|
const tokens = Object.values(balances.tokens)
|
||||||
|
.filter((t) => tokenAddresses.includes(t.address));
|
||||||
|
|
||||||
fetchTokensBalances(uniq(addresses), tokens)(dispatch, getState);
|
fetchTokensBalances(uniq(addresses), tokens)(dispatch, getState);
|
||||||
});
|
});
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
|
|
||||||
import React, { Component, PropTypes } from 'react';
|
import React, { Component, PropTypes } from 'react';
|
||||||
import { MenuItem } from 'material-ui';
|
import { MenuItem } from 'material-ui';
|
||||||
import { isEqual } from 'lodash';
|
|
||||||
|
|
||||||
import AutoComplete from '../AutoComplete';
|
import AutoComplete from '../AutoComplete';
|
||||||
import IdentityIcon from '../../IdentityIcon';
|
import IdentityIcon from '../../IdentityIcon';
|
||||||
@ -64,13 +63,6 @@ export default class AddressSelect extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps (newProps) {
|
componentWillReceiveProps (newProps) {
|
||||||
const entries = this.entriesFromProps();
|
|
||||||
const addresses = Object.keys(entries).sort();
|
|
||||||
|
|
||||||
if (!isEqual(addresses, this.state.addresses)) {
|
|
||||||
this.setState({ entries, addresses });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newProps.value !== this.props.value) {
|
if (newProps.value !== this.props.value) {
|
||||||
this.setState({ value: newProps.value });
|
this.setState({ value: newProps.value });
|
||||||
}
|
}
|
||||||
@ -127,31 +119,33 @@ export default class AddressSelect extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderItem = (entry) => {
|
renderItem = (entry) => {
|
||||||
|
const { address, name } = entry;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
text: entry.name && entry.name.toUpperCase() || entry.address,
|
text: name && name.toUpperCase() || address,
|
||||||
value: this.renderSelectEntry(entry),
|
value: this.renderMenuItem(address),
|
||||||
address: entry.address
|
address
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
renderSelectEntry = (entry) => {
|
renderMenuItem (address) {
|
||||||
const item = (
|
const item = (
|
||||||
<div className={ styles.account }>
|
<div className={ styles.account }>
|
||||||
<IdentityIcon
|
<IdentityIcon
|
||||||
className={ styles.image }
|
className={ styles.image }
|
||||||
inline center
|
inline center
|
||||||
address={ entry.address } />
|
address={ address } />
|
||||||
<IdentityName
|
<IdentityName
|
||||||
className={ styles.name }
|
className={ styles.name }
|
||||||
address={ entry.address } />
|
address={ address } />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
className={ styles.menuItem }
|
className={ styles.menuItem }
|
||||||
key={ entry.address }
|
key={ address }
|
||||||
value={ entry.address }
|
value={ address }
|
||||||
label={ item }>
|
label={ item }>
|
||||||
{ item }
|
{ item }
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
@ -19,6 +19,8 @@ import keycode from 'keycode';
|
|||||||
import { MenuItem, AutoComplete as MUIAutoComplete } from 'material-ui';
|
import { MenuItem, AutoComplete as MUIAutoComplete } from 'material-ui';
|
||||||
import { PopoverAnimationVertical } from 'material-ui/Popover';
|
import { PopoverAnimationVertical } from 'material-ui/Popover';
|
||||||
|
|
||||||
|
import { isEqual } from 'lodash';
|
||||||
|
|
||||||
export default class AutoComplete extends Component {
|
export default class AutoComplete extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
@ -42,12 +44,28 @@ export default class AutoComplete extends Component {
|
|||||||
lastChangedValue: undefined,
|
lastChangedValue: undefined,
|
||||||
entry: null,
|
entry: null,
|
||||||
open: false,
|
open: false,
|
||||||
fakeBlur: false
|
fakeBlur: false,
|
||||||
|
dataSource: []
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillMount () {
|
||||||
|
const dataSource = this.getDataSource();
|
||||||
|
this.setState({ dataSource });
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps (nextProps) {
|
||||||
|
const prevEntries = Object.keys(this.props.entries || {}).sort();
|
||||||
|
const nextEntries = Object.keys(nextProps.entries || {}).sort();
|
||||||
|
|
||||||
|
if (!isEqual(prevEntries, nextEntries)) {
|
||||||
|
const dataSource = this.getDataSource(nextProps);
|
||||||
|
this.setState({ dataSource });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { disabled, error, hint, label, value, className, filter, onUpdateInput } = this.props;
|
const { disabled, error, hint, label, value, className, filter, onUpdateInput } = this.props;
|
||||||
const { open } = this.state;
|
const { open, dataSource } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MUIAutoComplete
|
<MUIAutoComplete
|
||||||
@ -68,7 +86,7 @@ export default class AutoComplete extends Component {
|
|||||||
menuCloseDelay={ 0 }
|
menuCloseDelay={ 0 }
|
||||||
fullWidth
|
fullWidth
|
||||||
floatingLabelFixed
|
floatingLabelFixed
|
||||||
dataSource={ this.getDataSource() }
|
dataSource={ dataSource }
|
||||||
menuProps={ { maxHeight: 400 } }
|
menuProps={ { maxHeight: 400 } }
|
||||||
ref='muiAutocomplete'
|
ref='muiAutocomplete'
|
||||||
onKeyDown={ this.onKeyDown }
|
onKeyDown={ this.onKeyDown }
|
||||||
@ -76,8 +94,8 @@ export default class AutoComplete extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getDataSource () {
|
getDataSource (props = this.props) {
|
||||||
const { renderItem, entries } = this.props;
|
const { renderItem, entries } = props;
|
||||||
const entriesArray = (entries instanceof Array)
|
const entriesArray = (entries instanceof Array)
|
||||||
? entries
|
? entries
|
||||||
: Object.values(entries);
|
: Object.values(entries);
|
||||||
|
17
js/src/ui/Loading/index.js
Normal file
17
js/src/ui/Loading/index.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
// Copyright 2015, 2016 Ethcore (UK) Ltd.
|
||||||
|
// This file is part of Parity.
|
||||||
|
|
||||||
|
// Parity 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 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
|
||||||
|
// Parity 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 Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
export default from './loading';
|
23
js/src/ui/Loading/loading.css
Normal file
23
js/src/ui/Loading/loading.css
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
/* Copyright 2015, 2016 Ethcore (UK) Ltd.
|
||||||
|
/* This file is part of Parity.
|
||||||
|
/*
|
||||||
|
/* Parity 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 of the License, or
|
||||||
|
/* (at your option) any later version.
|
||||||
|
/*
|
||||||
|
/* Parity 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 Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
36
js/src/ui/Loading/loading.js
Normal file
36
js/src/ui/Loading/loading.js
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
// Copyright 2015, 2016 Ethcore (UK) Ltd.
|
||||||
|
// This file is part of Parity.
|
||||||
|
|
||||||
|
// Parity 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 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
|
||||||
|
// Parity 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 Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import React, { Component, PropTypes } from 'react';
|
||||||
|
import CircularProgress from 'material-ui/CircularProgress';
|
||||||
|
|
||||||
|
import styles from './loading.css';
|
||||||
|
|
||||||
|
export default class Loading extends Component {
|
||||||
|
static propTypes = {
|
||||||
|
size: PropTypes.number
|
||||||
|
};
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const size = (this.props.size || 2) * 60;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={ styles.loading }>
|
||||||
|
<CircularProgress size={ size } />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -18,6 +18,20 @@
|
|||||||
.container {
|
.container {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.clickable {
|
||||||
|
border: 1px dashed rgba(255, 255, 255, 0.4);
|
||||||
|
padding: 0.1em 0.3em;
|
||||||
|
margin: 0.1em 0.1em;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.noSelect {
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.loading {
|
.loading {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -54,7 +54,9 @@ class MethodDecoding extends Component {
|
|||||||
isContract: false,
|
isContract: false,
|
||||||
isDeploy: false,
|
isDeploy: false,
|
||||||
isReceived: false,
|
isReceived: false,
|
||||||
isLoading: true
|
isLoading: true,
|
||||||
|
expandInput: false,
|
||||||
|
inputType: 'auto'
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillMount () {
|
componentWillMount () {
|
||||||
@ -94,6 +96,11 @@ class MethodDecoding extends Component {
|
|||||||
renderGas () {
|
renderGas () {
|
||||||
const { historic, transaction } = this.props;
|
const { historic, transaction } = this.props;
|
||||||
const { gas, gasPrice } = transaction;
|
const { gas, gasPrice } = transaction;
|
||||||
|
|
||||||
|
if (!gas || !gasPrice) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const gasValue = gas.mul(gasPrice);
|
const gasValue = gas.mul(gasPrice);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -132,26 +139,55 @@ class MethodDecoding extends Component {
|
|||||||
: this.renderValueTransfer();
|
: this.renderValueTransfer();
|
||||||
}
|
}
|
||||||
|
|
||||||
renderInputValue () {
|
getAscii () {
|
||||||
const { api } = this.context;
|
const { api } = this.context;
|
||||||
const { transaction } = this.props;
|
const { transaction } = this.props;
|
||||||
|
const ascii = api.util.hex2Ascii(transaction.input || transaction.data);
|
||||||
|
|
||||||
|
return { value: ascii, valid: ASCII_INPUT.test(ascii) };
|
||||||
|
}
|
||||||
|
|
||||||
|
renderInputValue () {
|
||||||
|
const { transaction } = this.props;
|
||||||
|
const { expandInput, inputType } = this.state;
|
||||||
const input = transaction.input || transaction.data;
|
const input = transaction.input || transaction.data;
|
||||||
|
|
||||||
if (!/^(0x)?([0]*[1-9a-f]+[0]*)+$/.test(input)) {
|
if (!/^(0x)?([0]*[1-9a-f]+[0]*)+$/.test(input)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ascii = api.util.hex2Ascii(input);
|
const ascii = this.getAscii();
|
||||||
const text = ASCII_INPUT.test(ascii)
|
const type = inputType === 'auto'
|
||||||
? ascii
|
? (ascii.valid ? 'ascii' : 'raw')
|
||||||
|
: inputType;
|
||||||
|
|
||||||
|
const text = type === 'ascii'
|
||||||
|
? ascii.value
|
||||||
: input;
|
: input;
|
||||||
|
|
||||||
|
const expandable = text.length > 50;
|
||||||
|
const textToShow = expandInput || !expandable
|
||||||
|
? text
|
||||||
|
: text.slice(0, 50) + '...';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={ styles.description }>
|
<div>
|
||||||
<div>
|
<span>with the </span>
|
||||||
<span>with the input </span>
|
<span
|
||||||
<code className={ styles.inputData }>{ text }</code>
|
onClick={ this.toggleInputType }
|
||||||
</div>
|
className={ [ styles.clickable, styles.noSelect ].join(' ') }
|
||||||
|
>
|
||||||
|
{ type === 'ascii' ? 'input' : 'data' }
|
||||||
|
</span>
|
||||||
|
<span> </span>
|
||||||
|
<span
|
||||||
|
onClick={ this.toggleInputExpand }
|
||||||
|
className={ expandable ? styles.clickable : '' }
|
||||||
|
>
|
||||||
|
<code className={ styles.inputData }>
|
||||||
|
{ textToShow }
|
||||||
|
</code>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -375,6 +411,31 @@ class MethodDecoding extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleInputExpand = () => {
|
||||||
|
if (window.getSelection && window.getSelection().type === 'Range') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
expandInput: !this.state.expandInput
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleInputType = () => {
|
||||||
|
const { inputType } = this.state;
|
||||||
|
|
||||||
|
if (inputType !== 'auto') {
|
||||||
|
return this.setState({
|
||||||
|
inputType: this.state.inputType === 'raw' ? 'ascii' : 'raw'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const ascii = this.getAscii();
|
||||||
|
return this.setState({
|
||||||
|
inputType: ascii.valid ? 'raw' : 'ascii'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
lookup () {
|
lookup () {
|
||||||
const { transaction } = this.props;
|
const { transaction } = this.props;
|
||||||
|
|
||||||
|
@ -14,8 +14,9 @@
|
|||||||
/* You should have received a copy of the GNU General Public License
|
/* You should have received a copy of the GNU General Public License
|
||||||
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.layout {
|
.layout {
|
||||||
padding: 0.25em;
|
padding: 0.25em 0.25em 1em 0.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.layout>div {
|
.layout>div {
|
||||||
|
@ -29,79 +29,49 @@ import Store from './store';
|
|||||||
|
|
||||||
import styles from './txList.css';
|
import styles from './txList.css';
|
||||||
|
|
||||||
@observer
|
export class TxRow extends Component {
|
||||||
class TxList extends Component {
|
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
api: PropTypes.object.isRequired
|
api: PropTypes.object.isRequired
|
||||||
}
|
};
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
tx: PropTypes.object.isRequired,
|
||||||
address: PropTypes.string.isRequired,
|
address: PropTypes.string.isRequired,
|
||||||
hashes: PropTypes.oneOfType([
|
isTest: PropTypes.bool.isRequired,
|
||||||
PropTypes.array,
|
|
||||||
PropTypes.object
|
|
||||||
]).isRequired,
|
|
||||||
isTest: PropTypes.bool.isRequired
|
|
||||||
}
|
|
||||||
|
|
||||||
store = new Store(this.context.api);
|
block: PropTypes.object
|
||||||
|
};
|
||||||
componentWillMount () {
|
|
||||||
this.store.loadTransactions(this.props.hashes);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount () {
|
|
||||||
this.store.unsubscribe();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillReceiveProps (newProps) {
|
|
||||||
this.store.loadTransactions(newProps.hashes);
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
|
const { tx, address, isTest } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<table className={ styles.transactions }>
|
<tr>
|
||||||
<tbody>
|
{ this.renderBlockNumber(tx.blockNumber) }
|
||||||
{ this.renderRows() }
|
{ this.renderAddress(tx.from) }
|
||||||
</tbody>
|
<td className={ styles.transaction }>
|
||||||
</table>
|
{ this.renderEtherValue(tx.value) }
|
||||||
|
<div>⇒</div>
|
||||||
|
<div>
|
||||||
|
<a
|
||||||
|
className={ styles.link }
|
||||||
|
href={ txLink(tx.hash, isTest) }
|
||||||
|
target='_blank'>
|
||||||
|
{ `${tx.hash.substr(2, 6)}...${tx.hash.slice(-6)}` }
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
{ this.renderAddress(tx.to) }
|
||||||
|
<td className={ styles.method }>
|
||||||
|
<MethodDecoding
|
||||||
|
historic
|
||||||
|
address={ address }
|
||||||
|
transaction={ tx } />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderRows () {
|
|
||||||
const { address, isTest } = this.props;
|
|
||||||
|
|
||||||
return this.store.sortedHashes.map((txhash) => {
|
|
||||||
const tx = this.store.transactions[txhash];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<tr key={ tx.hash }>
|
|
||||||
{ this.renderBlockNumber(tx.blockNumber) }
|
|
||||||
{ this.renderAddress(tx.from) }
|
|
||||||
<td className={ styles.transaction }>
|
|
||||||
{ this.renderEtherValue(tx.value) }
|
|
||||||
<div>⇒</div>
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
className={ styles.link }
|
|
||||||
href={ txLink(tx.hash, isTest) }
|
|
||||||
target='_blank'>
|
|
||||||
{ `${tx.hash.substr(2, 6)}...${tx.hash.slice(-6)}` }
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
{ this.renderAddress(tx.to) }
|
|
||||||
<td className={ styles.method }>
|
|
||||||
<MethodDecoding
|
|
||||||
historic
|
|
||||||
address={ address }
|
|
||||||
transaction={ tx } />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
renderAddress (address) {
|
renderAddress (address) {
|
||||||
const { isTest } = this.props;
|
const { isTest } = this.props;
|
||||||
|
|
||||||
@ -148,8 +118,8 @@ class TxList extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderBlockNumber (_blockNumber) {
|
renderBlockNumber (_blockNumber) {
|
||||||
|
const { block } = this.props;
|
||||||
const blockNumber = _blockNumber.toNumber();
|
const blockNumber = _blockNumber.toNumber();
|
||||||
const block = this.store.blocks[blockNumber];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<td className={ styles.timestamp }>
|
<td className={ styles.timestamp }>
|
||||||
@ -160,6 +130,66 @@ class TxList extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@observer
|
||||||
|
class TxList extends Component {
|
||||||
|
static contextTypes = {
|
||||||
|
api: PropTypes.object.isRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
address: PropTypes.string.isRequired,
|
||||||
|
hashes: PropTypes.oneOfType([
|
||||||
|
PropTypes.array,
|
||||||
|
PropTypes.object
|
||||||
|
]).isRequired,
|
||||||
|
isTest: PropTypes.bool.isRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
store = new Store(this.context.api);
|
||||||
|
|
||||||
|
componentWillMount () {
|
||||||
|
this.store.loadTransactions(this.props.hashes);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount () {
|
||||||
|
this.store.unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps (newProps) {
|
||||||
|
this.store.loadTransactions(newProps.hashes);
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
return (
|
||||||
|
<table className={ styles.transactions }>
|
||||||
|
<tbody>
|
||||||
|
{ this.renderRows() }
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderRows () {
|
||||||
|
const { address, isTest } = this.props;
|
||||||
|
|
||||||
|
return this.store.sortedHashes.map((txhash) => {
|
||||||
|
const tx = this.store.transactions[txhash];
|
||||||
|
const blockNumber = tx.blockNumber.toNumber();
|
||||||
|
const block = this.store.blocks[blockNumber];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TxRow
|
||||||
|
key={ tx.hash }
|
||||||
|
tx={ tx }
|
||||||
|
block={ block }
|
||||||
|
address={ address }
|
||||||
|
isTest={ isTest }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function mapStateToProps (state) {
|
function mapStateToProps (state) {
|
||||||
const { isTest } = state.nodeStatus;
|
const { isTest } = state.nodeStatus;
|
||||||
|
|
||||||
|
@ -33,6 +33,7 @@ import Errors from './Errors';
|
|||||||
import Form, { AddressSelect, FormWrap, TypedInput, Input, InputAddress, InputAddressSelect, InputChip, InputInline, Select, RadioButtons } from './Form';
|
import Form, { AddressSelect, FormWrap, TypedInput, Input, InputAddress, InputAddressSelect, InputChip, InputInline, Select, RadioButtons } from './Form';
|
||||||
import IdentityIcon from './IdentityIcon';
|
import IdentityIcon from './IdentityIcon';
|
||||||
import IdentityName from './IdentityName';
|
import IdentityName from './IdentityName';
|
||||||
|
import Loading from './Loading';
|
||||||
import MethodDecoding from './MethodDecoding';
|
import MethodDecoding from './MethodDecoding';
|
||||||
import Modal, { Busy as BusyStep, Completed as CompletedStep } from './Modal';
|
import Modal, { Busy as BusyStep, Completed as CompletedStep } from './Modal';
|
||||||
import muiTheme from './Theme';
|
import muiTheme from './Theme';
|
||||||
@ -72,6 +73,7 @@ export {
|
|||||||
InputAddressSelect,
|
InputAddressSelect,
|
||||||
InputChip,
|
InputChip,
|
||||||
InputInline,
|
InputInline,
|
||||||
|
Loading,
|
||||||
Select,
|
Select,
|
||||||
IdentityIcon,
|
IdentityIcon,
|
||||||
IdentityName,
|
IdentityName,
|
||||||
|
@ -16,6 +16,6 @@
|
|||||||
|
|
||||||
import { PropTypes } from 'react';
|
import { PropTypes } from 'react';
|
||||||
|
|
||||||
export default function (type) {
|
export default function nullableProptype (type) {
|
||||||
return PropTypes.oneOfType([ PropTypes.oneOf([ null ]), type ]);
|
return PropTypes.oneOfType([ PropTypes.oneOf([ null ]), type ]);
|
||||||
}
|
}
|
||||||
|
@ -51,8 +51,6 @@ class Account extends Component {
|
|||||||
balances: PropTypes.object
|
balances: PropTypes.object
|
||||||
}
|
}
|
||||||
|
|
||||||
propName = null
|
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
showDeleteDialog: false,
|
showDeleteDialog: false,
|
||||||
showEditDialog: false,
|
showEditDialog: false,
|
||||||
|
@ -65,7 +65,7 @@ button.tabactive:hover {
|
|||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
margin: 0 0 0 -24px;
|
margin: 0 0 0 -24px;
|
||||||
padding: 22px 24px 0 24px;
|
padding: 20px 24px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,6 +84,6 @@ button.tabactive:hover {
|
|||||||
|
|
||||||
.last {
|
.last {
|
||||||
margin: 0 -24px 0 0;
|
margin: 0 -24px 0 0;
|
||||||
padding: 22px 12px 0 12px;
|
padding: 36px 12px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
@ -15,13 +15,8 @@
|
|||||||
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.outer,
|
|
||||||
.container {
|
.container {
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
min-height: 100vh;
|
||||||
|
|
||||||
.container {
|
|
||||||
padding-bottom: 1.6em;
|
|
||||||
}
|
}
|
||||||
|
@ -30,8 +30,6 @@ import Status from './Status';
|
|||||||
import Store from './store';
|
import Store from './store';
|
||||||
import TabBar from './TabBar';
|
import TabBar from './TabBar';
|
||||||
|
|
||||||
import styles from './application.css';
|
|
||||||
|
|
||||||
const inFrame = window.parent !== window && window.parent.frames.length !== 0;
|
const inFrame = window.parent !== window && window.parent.frames.length !== 0;
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
@ -62,7 +60,7 @@ class Application extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={ styles.outer }>
|
<div>
|
||||||
{ isDapp ? this.renderDapp() : this.renderApp() }
|
{ isDapp ? this.renderDapp() : this.renderApp() }
|
||||||
<Connection />
|
<Connection />
|
||||||
<ParityBar dapp={ isDapp } />
|
<ParityBar dapp={ isDapp } />
|
||||||
|
@ -32,7 +32,7 @@ const isProd = ENV === 'production';
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
cache: !isProd,
|
cache: !isProd,
|
||||||
devtool: isProd ? '#eval' : '#cheap-module-eval-source-map',
|
devtool: isProd ? '#eval' : '#eval-source-map',
|
||||||
|
|
||||||
context: path.join(__dirname, '../src'),
|
context: path.join(__dirname, '../src'),
|
||||||
entry: Object.assign({}, Shared.dappsEntry, {
|
entry: Object.assign({}, Shared.dappsEntry, {
|
||||||
|
@ -60,7 +60,7 @@ app.use(webpackHotMiddleware(compiler, {
|
|||||||
|
|
||||||
app.use(webpackDevMiddleware(compiler, {
|
app.use(webpackDevMiddleware(compiler, {
|
||||||
noInfo: false,
|
noInfo: false,
|
||||||
quiet: false,
|
quiet: true,
|
||||||
progress: true,
|
progress: true,
|
||||||
publicPath: webpackConfig.output.publicPath,
|
publicPath: webpackConfig.output.publicPath,
|
||||||
stats: {
|
stats: {
|
||||||
|
Loading…
Reference in New Issue
Block a user