Merge branch 'master' into jg-signer-decoding

This commit is contained in:
Jaco Greeff 2016-12-02 15:27:56 +01:00
commit fbd3738096
28 changed files with 960 additions and 667 deletions

View File

@ -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",

View File

@ -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);
} }

View File

@ -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);
} }

View File

@ -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) {

View File

@ -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('');
} }

View File

@ -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;

View File

@ -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>
); );
} }

View File

@ -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',

View 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;
}
}

View File

@ -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(

View File

@ -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);
}); });

View File

@ -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>

View File

@ -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);

View 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';

View 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;
}

View 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>
);
}
}

View File

@ -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;

View File

@ -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 &nbsp;</span> <span
<code className={ styles.inputData }>{ text }</code> onClick={ this.toggleInputType }
</div> className={ [ styles.clickable, styles.noSelect ].join(' ') }
>
{ type === 'ascii' ? 'input' : 'data' }
</span>
<span> &nbsp; </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;

View File

@ -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 {

View File

@ -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;

View File

@ -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,

View File

@ -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 ]);
} }

View File

@ -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,

View File

@ -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;
} }

View File

@ -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;
} }

View File

@ -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 } />

View File

@ -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, {

View File

@ -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: {