diff --git a/js/src/abi/util/slice.js b/js/src/abi/util/slice.js
index 417efea54..f4bdf38e2 100644
--- a/js/src/abi/util/slice.js
+++ b/js/src/abi/util/slice.js
@@ -27,9 +27,5 @@ export function sliceData (_data) {
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);
}
diff --git a/js/src/api/contract/contract.js b/js/src/api/contract/contract.js
index c1ba8498d..28bad8a3b 100644
--- a/js/src/api/contract/contract.js
+++ b/js/src/api/contract/contract.js
@@ -240,9 +240,29 @@ export default class Contract {
return this.unsubscribe(subscriptionId);
};
+ event.getAllLogs = (options = {}) => {
+ return this.getAllLogs(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) {
const event = eventName
? this._events.find((evt) => evt.name === eventName)
@@ -256,7 +276,7 @@ export default class Contract {
return event;
}
- _createEthFilter (event = null, _options) {
+ _getFilterOptions (event = null, _options = {}) {
const optionTopics = _options.topics || [];
const signature = event && event.signature || null;
@@ -271,6 +291,11 @@ export default class Contract {
topics
});
+ return options;
+ }
+
+ _createEthFilter (event = null, _options) {
+ const options = this._getFilterOptions(event, _options);
return this._api.eth.newFilter(options);
}
diff --git a/js/src/api/rpc/eth/eth.js b/js/src/api/rpc/eth/eth.js
index 43f8025e1..8148f9385 100644
--- a/js/src/api/rpc/eth/eth.js
+++ b/js/src/api/rpc/eth/eth.js
@@ -146,7 +146,8 @@ export default class Eth {
getLogs (options) {
return this._transport
- .execute('eth_getLogs', inFilter(options));
+ .execute('eth_getLogs', inFilter(options))
+ .then((logs) => logs.map(outLog));
}
getLogsEx (options) {
diff --git a/js/src/api/util/format.js b/js/src/api/util/format.js
index 7f60357cd..d8cf74a8f 100644
--- a/js/src/api/util/format.js
+++ b/js/src/api/util/format.js
@@ -32,6 +32,10 @@ export function hex2Ascii (_hex) {
return str;
}
+export function bytesToAscii (bytes) {
+ return bytes.map((b) => String.fromCharCode(b % 512)).join('');
+}
+
export function asciiToHex (string) {
return '0x' + string.split('').map((s) => s.charCodeAt(0).toString(16)).join('');
}
diff --git a/js/src/modals/Transfer/Details/details.js b/js/src/modals/Transfer/Details/details.js
index decd69c3c..dcc786422 100644
--- a/js/src/modals/Transfer/Details/details.js
+++ b/js/src/modals/Transfer/Details/details.js
@@ -18,6 +18,8 @@ import BigNumber from 'bignumber.js';
import React, { Component, PropTypes } from 'react';
import { Checkbox, MenuItem } from 'material-ui';
+import { isEqual } from 'lodash';
+
import Form, { Input, InputAddressSelect, Select } from '../../../ui/Form';
import imageUnknown from '../../../../assets/images/contracts/unknown-64x64.png';
@@ -29,11 +31,101 @@ const CHECK_STYLE = {
left: '1em'
};
-export default class Details extends Component {
+class TokenSelect extends Component {
static contextTypes = {
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 = (
+
+
![]({)
+
+ { token.name }
+
+
+ { value } { token.tag }
+
+
+ );
+
+ return (
+
+ );
+ });
+
+ this.setState({ items });
+ }
+
+ render () {
+ const { tag, onChange } = this.props;
+ const { items } = this.state;
+
+ return (
+
+ );
+ }
+}
+
+export default class Details extends Component {
static propTypes = {
address: PropTypes.string,
balance: PropTypes.object,
@@ -115,62 +207,15 @@ export default class Details extends Component {
}
renderTokenSelect () {
- const { api } = this.context;
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 = (
-
-
![]({)
-
- { token.name }
-
-
- { value } { token.tag }
-
-
- );
-
- return (
-
- );
- });
-
return (
-
+
);
}
diff --git a/js/src/modals/Transfer/errors.js b/js/src/modals/Transfer/errors.js
index 3a6bd63ae..b06e91b5d 100644
--- a/js/src/modals/Transfer/errors.js
+++ b/js/src/modals/Transfer/errors.js
@@ -15,6 +15,7 @@
// along with Parity. If not, see .
const ERRORS = {
+ requireSender: 'a valid sender is required for the transaction',
requireRecipient: 'a recipient network address is required for the transaction',
invalidAddress: 'the supplied address is an invalid network address',
invalidAmount: 'the supplied amount should be a valid positive number',
diff --git a/js/src/modals/Transfer/store.js b/js/src/modals/Transfer/store.js
new file mode 100644
index 000000000..20dc52e5f
--- /dev/null
+++ b/js/src/modals/Transfer/store.js
@@ -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 .
+
+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;
+ }
+}
diff --git a/js/src/modals/Transfer/transfer.js b/js/src/modals/Transfer/transfer.js
index c082a6e79..529cebd10 100644
--- a/js/src/modals/Transfer/transfer.js
+++ b/js/src/modals/Transfer/transfer.js
@@ -14,88 +14,50 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see .
-import BigNumber from 'bignumber.js';
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
+import { observer } from 'mobx-react';
+
import ActionDoneAll from 'material-ui/svg-icons/action/done-all';
import ContentClear from 'material-ui/svg-icons/content/clear';
import NavigationArrowBack from 'material-ui/svg-icons/navigation/arrow-back';
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 { DEFAULT_GAS, DEFAULT_GASPRICE, MAX_GAS_ESTIMATION } from '../../util/constants';
import Details from './Details';
import Extras from './Extras';
-import ERRORS from './errors';
+
+import TransferStore from './store';
import styles from './transfer.css';
-import { ERROR_CODES } from '../../api/transport/error';
-
-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];
-
+@observer
class Transfer extends Component {
static contextTypes = {
- api: PropTypes.object.isRequired,
- store: PropTypes.object.isRequired
+ api: PropTypes.object.isRequired
}
static propTypes = {
+ newError: PropTypes.func.isRequired,
+ gasLimit: PropTypes.object.isRequired,
+ images: PropTypes.object.isRequired,
+
account: PropTypes.object,
balance: PropTypes.object,
balances: PropTypes.object,
- gasLimit: PropTypes.object.isRequired,
- images: PropTypes.object.isRequired,
onClose: PropTypes.func
}
- state = {
- 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
- }
+ store = new TransferStore(this.context.api, this.props);
componentDidMount () {
- this.getDefaults();
+ this.store.getDefaults();
}
render () {
- const { stage, extras, rejected } = this.state;
-
- const steps = [].concat(extras ? STAGES_EXTRA : STAGES_BASIC);
-
- if (rejected) {
- steps[steps.length - 1] = TITLES.rejected;
- }
+ const { stage, extras, steps } = this.store;
return (
+ recipient={ recipient }
+ recipientError={ recipientError }
+ tag={ tag }
+ total={ total }
+ totalError={ totalError }
+ value={ value }
+ valueError={ valueError }
+ onChange={ this.store.onUpdateDetails } />
);
}
renderExtrasPage () {
- if (!this.state.gasPriceHistogram) {
+ if (!this.store.gasPriceHistogram) {
return null;
}
+ const { isEth, data, dataError, gas, gasEst, gasError, gasPrice } = this.store;
+ const { gasPriceDefault, gasPriceError, gasPriceHistogram, total, totalError } = this.store;
+
return (
+ isEth={ isEth }
+ data={ data }
+ dataError={ dataError }
+ gas={ gas }
+ gasEst={ gasEst }
+ gasError={ gasError }
+ gasPrice={ gasPrice }
+ gasPriceDefault={ gasPriceDefault }
+ gasPriceError={ gasPriceError }
+ gasPriceHistogram={ gasPriceHistogram }
+ total={ total }
+ totalError={ totalError }
+ onChange={ this.store.onUpdateDetails } />
);
}
renderDialogActions () {
const { account } = this.props;
- const { extras, sending, stage } = this.state;
+ const { extras, sending, stage } = this.store;
const cancelBtn = (
}
label='Cancel'
- onClick={ this.onClose } />
+ onClick={ this.store.onClose } />
);
const nextBtn = (
}
label='Next'
- onClick={ this.onNext } />
+ onClick={ this.store.onNext } />
);
const prevBtn = (
}
label='Back'
- onClick={ this.onPrev } />
+ onClick={ this.store.onPrev } />
);
const sendBtn = (
}
label='Send'
- onClick={ this.onSend } />
+ onClick={ this.store.onSend } />
);
const doneBtn = (
}
label='Close'
- onClick={ this.onClose } />
+ onClick={ this.store.onClose } />
);
switch (stage) {
@@ -268,7 +235,7 @@ class Transfer extends Component {
}
renderWarning () {
- const { gasLimitError } = this.state;
+ const { gasLimitError } = this.store;
if (!gasLimitError) {
return null;
@@ -280,413 +247,17 @@ class Transfer extends Component {
);
}
-
- 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) {
const { gasLimit } = state.nodeStatus;
-
return { gasLimit };
}
function mapDispatchToProps (dispatch) {
- return bindActionCreators({}, dispatch);
+ return bindActionCreators({
+ newError
+ }, dispatch);
}
export default connect(
diff --git a/js/src/redux/providers/balancesActions.js b/js/src/redux/providers/balancesActions.js
index f5d602b73..2d00cf00d 100644
--- a/js/src/redux/providers/balancesActions.js
+++ b/js/src/redux/providers/balancesActions.js
@@ -256,7 +256,8 @@ export function queryTokensFilter (tokensFilter) {
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);
});
diff --git a/js/src/ui/Form/AddressSelect/addressSelect.js b/js/src/ui/Form/AddressSelect/addressSelect.js
index 97195efe7..8ffbfcf50 100644
--- a/js/src/ui/Form/AddressSelect/addressSelect.js
+++ b/js/src/ui/Form/AddressSelect/addressSelect.js
@@ -16,7 +16,6 @@
import React, { Component, PropTypes } from 'react';
import { MenuItem } from 'material-ui';
-import { isEqual } from 'lodash';
import AutoComplete from '../AutoComplete';
import IdentityIcon from '../../IdentityIcon';
@@ -64,13 +63,6 @@ export default class AddressSelect extends Component {
}
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) {
this.setState({ value: newProps.value });
}
@@ -127,31 +119,33 @@ export default class AddressSelect extends Component {
}
renderItem = (entry) => {
+ const { address, name } = entry;
+
return {
- text: entry.name && entry.name.toUpperCase() || entry.address,
- value: this.renderSelectEntry(entry),
- address: entry.address
+ text: name && name.toUpperCase() || address,
+ value: this.renderMenuItem(address),
+ address
};
}
- renderSelectEntry = (entry) => {
+ renderMenuItem (address) {
const item = (
+ address={ address } />
+ address={ address } />
);
return (
diff --git a/js/src/ui/Form/AutoComplete/autocomplete.js b/js/src/ui/Form/AutoComplete/autocomplete.js
index ada1176b6..10b035059 100644
--- a/js/src/ui/Form/AutoComplete/autocomplete.js
+++ b/js/src/ui/Form/AutoComplete/autocomplete.js
@@ -19,6 +19,8 @@ import keycode from 'keycode';
import { MenuItem, AutoComplete as MUIAutoComplete } from 'material-ui';
import { PopoverAnimationVertical } from 'material-ui/Popover';
+import { isEqual } from 'lodash';
+
export default class AutoComplete extends Component {
static propTypes = {
onChange: PropTypes.func.isRequired,
@@ -42,12 +44,28 @@ export default class AutoComplete extends Component {
lastChangedValue: undefined,
entry: null,
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 () {
const { disabled, error, hint, label, value, className, filter, onUpdateInput } = this.props;
- const { open } = this.state;
+ const { open, dataSource } = this.state;
return (
50;
+ const textToShow = expandInput || !expandable
+ ? text
+ : text.slice(0, 50) + '...';
+
return (
- with the input
- { text }
+ with the
+
+ { type === 'ascii' ? 'input' : 'data' }
+
+
+
+
+ { textToShow }
+
+
);
}
@@ -373,6 +410,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 () {
const { transaction } = this.props;
diff --git a/js/src/ui/TxList/txList.js b/js/src/ui/TxList/txList.js
index 5b47c2ca8..8ae614f2f 100644
--- a/js/src/ui/TxList/txList.js
+++ b/js/src/ui/TxList/txList.js
@@ -29,79 +29,49 @@ import Store from './store';
import styles from './txList.css';
-@observer
-class TxList extends Component {
+export class TxRow extends Component {
static contextTypes = {
api: PropTypes.object.isRequired
- }
+ };
static propTypes = {
+ tx: PropTypes.object.isRequired,
address: PropTypes.string.isRequired,
- hashes: PropTypes.oneOfType([
- PropTypes.array,
- PropTypes.object
- ]).isRequired,
- isTest: PropTypes.bool.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);
- }
+ block: PropTypes.object
+ };
render () {
+ const { tx, address, isTest } = this.props;
+
return (
-
-
- { this.renderRows() }
-
-
+
+ { this.renderBlockNumber(tx.blockNumber) }
+ { this.renderAddress(tx.from) }
+
+ { this.renderEtherValue(tx.value) }
+ ⇒
+
+ |
+ { this.renderAddress(tx.to) }
+
+
+ |
+
);
}
- renderRows () {
- const { address, isTest } = this.props;
-
- return this.store.sortedHashes.map((txhash) => {
- const tx = this.store.transactions[txhash];
-
- return (
-
- { this.renderBlockNumber(tx.blockNumber) }
- { this.renderAddress(tx.from) }
-
- { this.renderEtherValue(tx.value) }
- ⇒
-
- |
- { this.renderAddress(tx.to) }
-
-
- |
-
- );
- });
- }
-
renderAddress (address) {
const { isTest } = this.props;
@@ -148,8 +118,8 @@ class TxList extends Component {
}
renderBlockNumber (_blockNumber) {
+ const { block } = this.props;
const blockNumber = _blockNumber.toNumber();
- const block = this.store.blocks[blockNumber];
return (
@@ -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 (
+
+
+ { this.renderRows() }
+
+
+ );
+ }
+
+ 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 (
+
+ );
+ });
+ }
+}
+
function mapStateToProps (state) {
const { isTest } = state.nodeStatus;
diff --git a/js/src/util/nullable-proptype.js b/js/src/util/nullable-proptype.js
index 331be6c18..78c486cb6 100644
--- a/js/src/util/nullable-proptype.js
+++ b/js/src/util/nullable-proptype.js
@@ -16,6 +16,6 @@
import { PropTypes } from 'react';
-export default function (type) {
+export default function nullableProptype (type) {
return PropTypes.oneOfType([ PropTypes.oneOf([ null ]), type ]);
}
diff --git a/js/src/views/Account/account.js b/js/src/views/Account/account.js
index 21d2f380c..835b9358c 100644
--- a/js/src/views/Account/account.js
+++ b/js/src/views/Account/account.js
@@ -51,8 +51,6 @@ class Account extends Component {
balances: PropTypes.object
}
- propName = null
-
state = {
showDeleteDialog: false,
showEditDialog: false,
diff --git a/js/webpack/app.js b/js/webpack/app.js
index aff9b8aac..5a7d8cb5d 100644
--- a/js/webpack/app.js
+++ b/js/webpack/app.js
@@ -32,7 +32,7 @@ const isProd = ENV === 'production';
module.exports = {
cache: !isProd,
- devtool: isProd ? '#eval' : '#cheap-module-eval-source-map',
+ devtool: isProd ? '#eval' : '#eval-source-map',
context: path.join(__dirname, '../src'),
entry: Object.assign({}, Shared.dappsEntry, {
diff --git a/js/webpack/dev.server.js b/js/webpack/dev.server.js
index ca006fc98..aed7464b4 100644
--- a/js/webpack/dev.server.js
+++ b/js/webpack/dev.server.js
@@ -60,7 +60,7 @@ app.use(webpackHotMiddleware(compiler, {
app.use(webpackDevMiddleware(compiler, {
noInfo: false,
- quiet: false,
+ quiet: true,
progress: true,
publicPath: webpackConfig.output.publicPath,
stats: {
|