diff --git a/js/src/redux/providers/personalActions.js b/js/src/redux/providers/personalActions.js
index b172aaf8d..7ca7c3374 100644
--- a/js/src/redux/providers/personalActions.js
+++ b/js/src/redux/providers/personalActions.js
@@ -19,6 +19,8 @@ import { isEqual } from 'lodash';
import { fetchBalances } from './balancesActions';
import { attachWallets } from './walletActions';
+import MethodDecodingStore from '~/ui/MethodDecoding/methodDecodingStore';
+
export function personalAccountsInfo (accountsInfo) {
const accounts = {};
const contacts = {};
@@ -41,6 +43,9 @@ export function personalAccountsInfo (accountsInfo) {
}
});
+ // Load user contracts for Method Decoding
+ MethodDecodingStore.loadContracts(contracts);
+
return (dispatch) => {
const data = {
accountsInfo,
diff --git a/js/src/ui/Form/AddressSelect/addressSelect.js b/js/src/ui/Form/AddressSelect/addressSelect.js
index 3fb15cd86..c443ecaa2 100644
--- a/js/src/ui/Form/AddressSelect/addressSelect.js
+++ b/js/src/ui/Form/AddressSelect/addressSelect.js
@@ -16,6 +16,7 @@
import React, { Component, PropTypes } from 'react';
import { MenuItem } from 'material-ui';
+import { isEqual, pick } from 'lodash';
import AutoComplete from '../AutoComplete';
import IdentityIcon from '../../IdentityIcon';
@@ -31,19 +32,20 @@ export default class AddressSelect extends Component {
}
static propTypes = {
- disabled: PropTypes.bool,
+ onChange: PropTypes.func.isRequired,
+
accounts: PropTypes.object,
+ allowInput: PropTypes.bool,
+ balances: PropTypes.object,
contacts: PropTypes.object,
contracts: PropTypes.object,
- wallets: PropTypes.object,
- label: PropTypes.string,
- hint: PropTypes.string,
+ disabled: PropTypes.bool,
error: PropTypes.string,
- value: PropTypes.string,
+ hint: PropTypes.string,
+ label: PropTypes.string,
tokens: PropTypes.object,
- onChange: PropTypes.func.isRequired,
- allowInput: PropTypes.bool,
- balances: PropTypes.object
+ value: PropTypes.string,
+ wallets: PropTypes.object
}
state = {
@@ -53,6 +55,9 @@ export default class AddressSelect extends Component {
value: ''
}
+ // Cache autocomplete items
+ items = {}
+
entriesFromProps (props = this.props) {
const { accounts = {}, contacts = {}, contracts = {}, wallets = {} } = props;
@@ -76,6 +81,15 @@ export default class AddressSelect extends Component {
return { autocompleteEntries, entries };
}
+ shouldComponentUpdate (nextProps, nextState) {
+ const keys = [ 'error', 'value' ];
+
+ const prevValues = pick(this.props, keys);
+ const nextValues = pick(nextProps, keys);
+
+ return !isEqual(prevValues, nextValues);
+ }
+
componentWillMount () {
const { value } = this.props;
const { entries, autocompleteEntries } = this.entriesFromProps();
@@ -143,14 +157,21 @@ export default class AddressSelect extends Component {
renderItem = (entry) => {
const { address, name } = entry;
- return {
- text: name && name.toUpperCase() || address,
- value: this.renderMenuItem(address),
- address
- };
+ const _balance = this.getBalance(address);
+ const balance = _balance ? _balance.toNumber() : _balance;
+
+ if (!this.items[address] || this.items[address].balance !== balance) {
+ this.items[address] = {
+ text: name && name.toUpperCase() || address,
+ value: this.renderMenuItem(address),
+ address, balance
+ };
+ }
+
+ return this.items[address];
}
- renderBalance (address) {
+ getBalance (address) {
const { balances = {} } = this.props;
const balance = balances[address];
@@ -164,7 +185,12 @@ export default class AddressSelect extends Component {
return null;
}
- const value = fromWei(ethToken.value);
+ return ethToken.value;
+ }
+
+ renderBalance (address) {
+ const balance = this.getBalance(address);
+ const value = fromWei(balance);
return (
diff --git a/js/src/ui/Form/AutoComplete/autocomplete.css b/js/src/ui/Form/AutoComplete/autocomplete.css
new file mode 100644
index 000000000..9fad53edb
--- /dev/null
+++ b/js/src/ui/Form/AutoComplete/autocomplete.css
@@ -0,0 +1,24 @@
+/* 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
.
+*/
+
+.item {
+ &:last-child {
+ &.divider {
+ display: none;
+ }
+ }
+}
diff --git a/js/src/ui/Form/AutoComplete/autocomplete.js b/js/src/ui/Form/AutoComplete/autocomplete.js
index c98019009..3ebd59772 100644
--- a/js/src/ui/Form/AutoComplete/autocomplete.js
+++ b/js/src/ui/Form/AutoComplete/autocomplete.js
@@ -21,13 +21,18 @@ import { PopoverAnimationVertical } from 'material-ui/Popover';
import { isEqual } from 'lodash';
+import styles from './autocomplete.css';
+
// Hack to prevent "Unknown prop `disableFocusRipple` on
tag" error
class Divider extends Component {
static muiName = MUIDivider.muiName;
render () {
return (
-
+
);
@@ -143,11 +148,16 @@ export default class AutoComplete extends Component {
if (renderItem && typeof renderItem === 'function') {
item = renderItem(entry);
+
+ // Add the item class to the entry
+ const classNames = [ styles.item ].concat(item.value.props.className);
+ item.value = React.cloneElement(item.value, { className: classNames.join(' ') });
} else {
item = {
text: entry,
value: (
)
@@ -160,6 +170,7 @@ export default class AutoComplete extends Component {
}
item.divider = currentDivider;
+ item.entry = entry;
return item;
}).filter((item) => item !== undefined);
@@ -215,13 +226,8 @@ export default class AutoComplete extends Component {
return;
}
- const { entries } = this.props;
-
- const entriesArray = (entries instanceof Array)
- ? entries
- : Object.values(entries);
-
- const entry = entriesArray[idx];
+ const { dataSource } = this.state;
+ const { entry } = dataSource[idx];
this.handleOnChange(entry);
this.setState({ entry, open: false });
diff --git a/js/src/ui/MethodDecoding/methodDecoding.js b/js/src/ui/MethodDecoding/methodDecoding.js
index e02355ffb..fcf7f7513 100644
--- a/js/src/ui/MethodDecoding/methodDecoding.js
+++ b/js/src/ui/MethodDecoding/methodDecoding.js
@@ -17,16 +17,14 @@
import BigNumber from 'bignumber.js';
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
-import { bindActionCreators } from 'redux';
import CircularProgress from 'material-ui/CircularProgress';
-import Contracts from '~/contracts';
import { Input, InputAddress } from '../Form';
+import MethodDecodingStore from './methodDecodingStore';
import styles from './methodDecoding.css';
const ASCII_INPUT = /^[a-z0-9\s,?;.:/!()-_@'"#]+$/i;
-const CONTRACT_CREATE = '0x60606040';
const TOKEN_METHODS = {
'0xa9059cbb': 'transfer(to,value)'
};
@@ -38,19 +36,17 @@ class MethodDecoding extends Component {
static propTypes = {
address: PropTypes.string.isRequired,
- tokens: PropTypes.object,
+ token: PropTypes.object,
transaction: PropTypes.object,
historic: PropTypes.bool
}
state = {
contractAddress: null,
- method: null,
methodName: null,
methodInputs: null,
methodParams: null,
methodSignature: null,
- token: null,
isContract: false,
isDeploy: false,
isReceived: false,
@@ -59,14 +55,29 @@ class MethodDecoding extends Component {
inputType: 'auto'
}
- componentWillMount () {
- const lookupResult = this.lookup();
+ methodDecodingStore = MethodDecodingStore.get(this.context.api);
- if (typeof lookupResult === 'object' && typeof lookupResult.then === 'function') {
- lookupResult.then(() => this.setState({ isLoading: false }));
- } else {
- this.setState({ isLoading: false });
- }
+ componentWillMount () {
+ const { address, transaction } = this.props;
+
+ this
+ .methodDecodingStore
+ .lookup(address, transaction)
+ .then((lookup) => {
+ const newState = {
+ methodName: lookup.name,
+ methodInputs: lookup.inputs,
+ methodParams: lookup.params,
+ methodSignature: lookup.signature,
+
+ isContract: lookup.contract,
+ isDeploy: lookup.deploy,
+ isLoading: false,
+ isReceived: lookup.received
+ };
+
+ this.setState(newState);
+ });
}
render () {
@@ -116,7 +127,8 @@ class MethodDecoding extends Component {
}
renderAction () {
- const { methodName, methodInputs, methodSignature, token, isDeploy, isReceived, isContract } = this.state;
+ const { token } = this.props;
+ const { methodName, methodInputs, methodSignature, isDeploy, isReceived, isContract } = this.state;
if (isDeploy) {
return this.renderDeploy();
@@ -378,7 +390,7 @@ class MethodDecoding extends Component {
}
renderTokenValue (value) {
- const { token } = this.state;
+ const { token } = this.props;
return (
@@ -436,96 +448,18 @@ class MethodDecoding extends Component {
});
}
- lookup () {
- const { transaction } = this.props;
-
- if (!transaction) {
- return;
- }
-
- const { api } = this.context;
- const { address, tokens } = this.props;
-
- const isReceived = transaction.to === address;
- const contractAddress = isReceived ? transaction.from : transaction.to;
- const input = transaction.input || transaction.data;
-
- const token = (tokens || {})[contractAddress];
- this.setState({ token, isReceived, contractAddress });
-
- if (!input || input === '0x') {
- return;
- }
-
- const { signature, paramdata } = api.util.decodeCallData(input);
- this.setState({ methodSignature: signature, methodParams: paramdata });
-
- if (!signature || signature === CONTRACT_CREATE || transaction.creates) {
- this.setState({ isDeploy: true });
- return;
- }
-
- if (contractAddress === '0x') {
- return;
- }
-
- return api.eth
- .getCode(contractAddress || transaction.creates)
- .then((bytecode) => {
- const isContract = bytecode && /^(0x)?([0]*[1-9a-f]+[0]*)+$/.test(bytecode);
-
- this.setState({ isContract });
-
- if (!isContract) {
- return;
- }
-
- return Contracts.get()
- .signatureReg
- .lookup(signature)
- .then((method) => {
- let methodInputs = null;
- let methodName = null;
-
- if (method && method.length) {
- const { methodParams } = this.state;
- const abi = api.util.methodToAbi(method);
-
- methodName = abi.name;
- methodInputs = api.util
- .decodeMethodInput(abi, methodParams)
- .map((value, index) => {
- const type = abi.inputs[index].type;
-
- return { type, value };
- });
- }
-
- this.setState({
- method,
- methodName,
- methodInputs,
- bytecode
- });
- });
- })
- .catch((error) => {
- console.warn('lookup', error);
- });
- }
}
-function mapStateToProps (state) {
- const { tokens } = state.balances;
+function mapStateToProps (initState, initProps) {
+ const { tokens } = initState.balances;
+ const { address } = initProps;
- return { tokens };
+ const token = (tokens || {})[address];
+
+ return () => {
+ return { token };
+ };
}
-
-function mapDispatchToProps (dispatch) {
- return bindActionCreators({}, dispatch);
-}
-
export default connect(
- mapStateToProps,
- mapDispatchToProps
+ mapStateToProps
)(MethodDecoding);
diff --git a/js/src/ui/MethodDecoding/methodDecodingStore.js b/js/src/ui/MethodDecoding/methodDecodingStore.js
new file mode 100644
index 000000000..24433e541
--- /dev/null
+++ b/js/src/ui/MethodDecoding/methodDecodingStore.js
@@ -0,0 +1,216 @@
+// 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 Contracts from '~/contracts';
+import Abi from '~/abi';
+import * as abis from '~/contracts/abi';
+
+const CONTRACT_CREATE = '0x60606040';
+
+let instance = null;
+
+export default class MethodDecodingStore {
+
+ api = null;
+
+ _isContract = {};
+ _methods = {};
+
+ constructor (api, contracts = {}) {
+ this.api = api;
+
+ // Load the signatures from the local ABIs
+ Object.keys(abis).forEach((abiKey) => {
+ this.loadFromAbi(abis[abiKey]);
+ });
+
+ this.addContracts(contracts);
+ }
+
+ addContracts (contracts = {}) {
+ // Load the User defined contracts
+ Object.values(contracts).forEach((contract) => {
+ if (!contract || !contract.meta || !contract.meta.abi) {
+ return;
+ }
+ this.loadFromAbi(contract.meta.abi);
+ });
+ }
+
+ loadFromAbi (_abi) {
+ const abi = new Abi(_abi);
+ abi
+ .functions
+ .map((f) => ({ sign: f.signature, abi: f.abi }))
+ .forEach((mapping) => {
+ const sign = (/^0x/.test(mapping.sign) ? '' : '0x') + mapping.sign;
+ this._methods[sign] = mapping.abi;
+ });
+ }
+
+ static get (api, contracts = {}) {
+ if (!instance) {
+ instance = new MethodDecodingStore(api, contracts);
+ }
+
+ // Set API if not set yet
+ if (!instance.api) {
+ instance.api = api;
+ }
+
+ return instance;
+ }
+
+ static loadContracts (contracts = {}) {
+ if (!instance) {
+ // Just create the instance with null API
+ MethodDecodingStore.get(null, contracts);
+ } else {
+ instance.addContracts(contracts);
+ }
+ }
+
+ /**
+ * Looks up a transaction in the context of the given
+ * address
+ *
+ * @param {String} address The address contract
+ * @param {Object} transaction The transaction to lookup
+ * @return {Promise} The result of the lookup. Resolves with:
+ * {
+ * contract: Boolean,
+ * deploy: Boolean,
+ * inputs: Array,
+ * name: String,
+ * params: Array,
+ * received: Boolean,
+ * signature: String
+ * }
+ */
+ lookup (address, transaction) {
+ const result = {};
+
+ if (!transaction) {
+ return Promise.resolve(result);
+ }
+
+ const isReceived = transaction.to === address;
+ const contractAddress = isReceived ? transaction.from : transaction.to;
+ const input = transaction.input || transaction.data;
+
+ result.received = isReceived;
+
+ // No input, should be a ETH transfer
+ if (!input || input === '0x') {
+ return Promise.resolve(result);
+ }
+
+ const { signature, paramdata } = this.api.util.decodeCallData(input);
+ result.signature = signature;
+ result.params = paramdata;
+
+ // Contract deployment
+ if (!signature || signature === CONTRACT_CREATE || transaction.creates) {
+ return Promise.resolve({ ...result, deploy: true });
+ }
+
+ return this
+ .isContract(contractAddress || transaction.creates)
+ .then((isContract) => {
+ result.contract = isContract;
+
+ if (!isContract) {
+ return result;
+ }
+
+ return this
+ .fetchMethodAbi(signature)
+ .then((abi) => {
+ let methodName = null;
+ let methodInputs = null;
+
+ if (abi) {
+ methodName = abi.name;
+ methodInputs = this.api.util
+ .decodeMethodInput(abi, paramdata)
+ .map((value, index) => {
+ const type = abi.inputs[index].type;
+ return { type, value };
+ });
+ }
+
+ return {
+ ...result,
+ name: methodName,
+ inputs: methodInputs
+ };
+ });
+ })
+ .catch((error) => {
+ console.warn('lookup', error);
+ });
+ }
+
+ fetchMethodAbi (signature) {
+ if (this._methods[signature] !== undefined) {
+ return Promise.resolve(this._methods[signature]);
+ }
+
+ this._methods[signature] = Contracts.get()
+ .signatureReg
+ .lookup(signature)
+ .then((method) => {
+ let abi = null;
+
+ if (method && method.length) {
+ abi = this.api.util.methodToAbi(method);
+ }
+
+ this._methods[signature] = abi;
+ return this._methods[signature];
+ });
+
+ return Promise.resolve(this._methods[signature]);
+ }
+
+ /**
+ * Checks (and caches) if the given address is a
+ * Contract or not, from its fetched bytecode
+ */
+ isContract (contractAddress) {
+ // If zero address, it isn't a contract
+ if (/^(0x)?0*$/.test(contractAddress)) {
+ return Promise.resolve(false);
+ }
+
+ if (this._isContract[contractAddress]) {
+ return Promise.resolve(this._isContract[contractAddress]);
+ }
+
+ this._isContract[contractAddress] = this.api.eth
+ .getCode(contractAddress)
+ .then((bytecode) => {
+ // Is a contract if the address contains *valid* bytecode
+ const _isContract = bytecode && /^(0x)?([0]*[1-9a-f]+[0]*)+$/.test(bytecode);
+
+ this._isContract[contractAddress] = _isContract;
+ return this._isContract[contractAddress];
+ });
+
+ return Promise.resolve(this._isContract[contractAddress]);
+ }
+
+}
diff --git a/js/src/views/Account/Transactions/transactions.js b/js/src/views/Account/Transactions/transactions.js
index b8b28208f..eb11e8def 100644
--- a/js/src/views/Account/Transactions/transactions.js
+++ b/js/src/views/Account/Transactions/transactions.js
@@ -19,7 +19,7 @@ import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import etherscan from '~/3rdparty/etherscan';
-import { Container, TxList } from '~/ui';
+import { Container, TxList, Loading } from '~/ui';
import styles from './transactions.css';
@@ -60,19 +60,32 @@ class Transactions extends Component {
}
render () {
- const { address } = this.props;
- const { hashes } = this.state;
-
return (
-
+ { this.renderTransactionList() }
{ this.renderEtherscanFooter() }
);
}
+ renderTransactionList () {
+ const { address } = this.props;
+ const { hashes, loading } = this.state;
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+ }
+
renderEtherscanFooter () {
const { traceMode } = this.props;
diff --git a/js/src/views/WriteContract/writeContract.css b/js/src/views/WriteContract/writeContract.css
index ca47e7332..2502c4060 100644
--- a/js/src/views/WriteContract/writeContract.css
+++ b/js/src/views/WriteContract/writeContract.css
@@ -32,6 +32,10 @@
flex: 1;
flex-direction: row;
+ // Fallback for browsers not supporting `calc`
+ min-height: 90vh;
+ min-height: calc(100vh - 8em);
+
> * {
margin: 0;