Refactoring of Tokens & Balances (#5372)

* Remove ETH filter

* Remove unused Blockchain reducer+actions

* Simpler Token updates and fetching

* Cleanup use of balances

* Cleanup of balances

* Cleanup of Balances

* Linting

* Update List Component

* Separate tokens from balances

* Refactoring balance fetchin and storing - Part I

* Linting

* Better ETH token description and use

* Working Transfer with new logic

* Add debugging

* Querying the tokens filter on new block

* Fixing the tests - PART I

* Fix txCount
This commit is contained in:
Nicolas Gotchac 2017-04-19 18:00:05 +02:00 committed by Jaco Greeff
parent fc18299869
commit 37690cfde2
49 changed files with 725 additions and 1027 deletions

View File

@ -59,7 +59,8 @@ export default class Api extends EventEmitter {
} }
return null; return null;
}); })
.catch(() => null);
transport.addMiddleware(middleware); transport.addMiddleware(middleware);
} }

View File

@ -27,7 +27,7 @@ export default class TokenReg {
} }
getInstance () { getInstance () {
return this.getContract().instance; return this.getContract().then((contract) => contract.instance);
} }
tokenCount () { tokenCount () {

View File

@ -16,8 +16,8 @@
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import imagesEthereum from '~/../assets/images/contracts/ethereum-black-64x64.png';
import { AccountCard } from '~/ui'; import { AccountCard } from '~/ui';
import { ETH_TOKEN } from '~/util/tokens';
export default class GethCard extends Component { export default class GethCard extends Component {
static propTypes = { static propTypes = {
@ -36,14 +36,7 @@ export default class GethCard extends Component {
name name
} } } }
balance={ { balance={ {
tokens: [ { [ETH_TOKEN.id]: balance
value: balance,
token: {
image: imagesEthereum,
native: true,
tag: 'ETH'
}
} ]
} } } }
/> />
); );

View File

@ -17,14 +17,12 @@
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { AccountCard, Portal, SelectionList } from '~/ui'; import { AccountCard, Portal, SelectionList } from '~/ui';
@observer @observer
class DappPermissions extends Component { export default class DappPermissions extends Component {
static propTypes = { static propTypes = {
balances: PropTypes.object,
permissionStore: PropTypes.object.isRequired permissionStore: PropTypes.object.isRequired
}; };
@ -66,27 +64,10 @@ class DappPermissions extends Component {
} }
renderAccount = (account) => { renderAccount = (account) => {
const { balances } = this.props;
const balance = balances[account.address];
return ( return (
<AccountCard <AccountCard
account={ account } account={ account }
balance={ balance }
/> />
); );
} }
} }
function mapStateToProps (state) {
const { balances } = state.balances;
return {
balances
};
}
export default connect(
mapStateToProps,
null
)(DappPermissions);

View File

@ -16,38 +16,15 @@
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import React from 'react'; import React from 'react';
import sinon from 'sinon';
import DappPermissions from './'; import DappPermissions from './';
let component; let component;
let store;
function createRedux () {
store = {
dispatch: sinon.stub(),
subscribe: sinon.stub(),
getState: () => {
return {
balances: {
balances: {}
}
};
}
};
return store;
}
function renderShallow (permissionStore = {}) { function renderShallow (permissionStore = {}) {
component = shallow( component = shallow(
<DappPermissions permissionStore={ permissionStore } />, <DappPermissions permissionStore={ permissionStore } />
{ );
context: {
store: createRedux()
}
}
).find('DappPermissions').shallow();
return component; return component;
} }

View File

@ -47,7 +47,6 @@ export default class DetailsStep extends Component {
abiError: PropTypes.string, abiError: PropTypes.string,
amount: PropTypes.string, amount: PropTypes.string,
amountError: PropTypes.string, amountError: PropTypes.string,
balances: PropTypes.object,
code: PropTypes.string, code: PropTypes.string,
codeError: PropTypes.string, codeError: PropTypes.string,
description: PropTypes.string, description: PropTypes.string,
@ -87,7 +86,6 @@ export default class DetailsStep extends Component {
render () { render () {
const { const {
accounts, accounts,
balances,
readOnly, readOnly,
fromAddress, fromAddressError, fromAddress, fromAddressError,
@ -141,7 +139,6 @@ export default class DetailsStep extends Component {
<AddressSelect <AddressSelect
accounts={ accounts } accounts={ accounts }
balances={ balances }
error={ fromAddressError } error={ fromAddressError }
hint={ hint={
<FormattedMessage <FormattedMessage

View File

@ -15,7 +15,6 @@
// 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 BigNumber from 'bignumber.js';
import { pick } from 'lodash';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
@ -69,7 +68,6 @@ class DeployContract extends Component {
static propTypes = { static propTypes = {
accounts: PropTypes.object.isRequired, accounts: PropTypes.object.isRequired,
abi: PropTypes.string, abi: PropTypes.string,
balances: PropTypes.object,
code: PropTypes.string, code: PropTypes.string,
gasLimit: PropTypes.object.isRequired, gasLimit: PropTypes.object.isRequired,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
@ -282,7 +280,7 @@ class DeployContract extends Component {
} }
renderStep () { renderStep () {
const { accounts, readOnly, balances } = this.props; const { accounts, readOnly } = this.props;
const { step } = this.state; const { step } = this.state;
switch (step) { switch (step) {
@ -291,7 +289,6 @@ class DeployContract extends Component {
<DetailsStep <DetailsStep
{ ...this.state } { ...this.state }
accounts={ accounts } accounts={ accounts }
balances={ balances }
onAmountChange={ this.onAmountChange } onAmountChange={ this.onAmountChange }
onExtrasChange={ this.onExtrasChange } onExtrasChange={ this.onExtrasChange }
onFromAddressChange={ this.onFromAddressChange } onFromAddressChange={ this.onFromAddressChange }
@ -484,20 +481,11 @@ class DeployContract extends Component {
} }
} }
function mapStateToProps (initState, initProps) { function mapStateToProps (state) {
const { accounts } = initProps; const { gasLimit } = state.nodeStatus;
const fromAddresses = Object.keys(accounts); return {
gasLimit
return (state) => {
const balances = pick(state.balances.balances, fromAddresses);
const { gasLimit } = state.nodeStatus;
return {
accounts,
balances,
gasLimit
};
}; };
} }

View File

@ -34,7 +34,6 @@ export default class DetailsStep extends Component {
accounts: PropTypes.object.isRequired, accounts: PropTypes.object.isRequired,
amount: PropTypes.string, amount: PropTypes.string,
amountError: PropTypes.string, amountError: PropTypes.string,
balances: PropTypes.object,
contract: PropTypes.object.isRequired, contract: PropTypes.object.isRequired,
fromAddress: PropTypes.string, fromAddress: PropTypes.string,
fromAddressError: PropTypes.string, fromAddressError: PropTypes.string,
@ -51,13 +50,12 @@ export default class DetailsStep extends Component {
} }
render () { render () {
const { accounts, advancedOptions, amount, amountError, balances, fromAddress, fromAddressError, onAdvancedClick, onAmountChange, onFromAddressChange } = this.props; const { accounts, advancedOptions, amount, amountError, fromAddress, fromAddressError, onAdvancedClick, onAmountChange, onFromAddressChange } = this.props;
return ( return (
<Form> <Form>
<AddressSelect <AddressSelect
accounts={ accounts } accounts={ accounts }
balances={ balances }
error={ fromAddressError } error={ fromAddressError }
hint={ hint={
<FormattedMessage <FormattedMessage

View File

@ -14,7 +14,6 @@
// 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 { pick } from 'lodash';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
@ -58,7 +57,6 @@ class ExecuteContract extends Component {
static propTypes = { static propTypes = {
accounts: PropTypes.object, accounts: PropTypes.object,
balances: PropTypes.object,
contract: PropTypes.object.isRequired, contract: PropTypes.object.isRequired,
fromAddress: PropTypes.string, fromAddress: PropTypes.string,
gasLimit: PropTypes.object.isRequired, gasLimit: PropTypes.object.isRequired,
@ -199,14 +197,16 @@ class ExecuteContract extends Component {
} }
renderStep () { renderStep () {
const { onFromAddressChange } = this.props; const { accounts, contract, fromAddress, onFromAddressChange } = this.props;
const { step } = this.state; const { step } = this.state;
if (step === STEP_DETAILS) { if (step === STEP_DETAILS) {
return ( return (
<DetailsStep <DetailsStep
{ ...this.props }
{ ...this.state } { ...this.state }
accounts={ accounts }
contract={ contract }
fromAddress={ fromAddress }
onAmountChange={ this.onAmountChange } onAmountChange={ this.onAmountChange }
onFromAddressChange={ onFromAddressChange } onFromAddressChange={ onFromAddressChange }
onFuncChange={ this.onFuncChange } onFuncChange={ this.onFuncChange }
@ -334,15 +334,10 @@ class ExecuteContract extends Component {
} }
} }
function mapStateToProps (initState, initProps) { function mapStateToProps (state) {
const fromAddresses = Object.keys(initProps.accounts); const { gasLimit } = state.nodeStatus;
return (state) => { return { gasLimit };
const balances = pick(state.balances.balances, fromAddresses);
const { gasLimit } = state.nodeStatus;
return { gasLimit, balances };
};
} }
export default connect( export default connect(

View File

@ -38,10 +38,9 @@ export default class Details extends Component {
extras: PropTypes.bool, extras: PropTypes.bool,
sender: PropTypes.string, sender: PropTypes.string,
senderError: PropTypes.string, senderError: PropTypes.string,
sendersBalances: PropTypes.object,
recipient: PropTypes.string, recipient: PropTypes.string,
recipientError: PropTypes.string, recipientError: PropTypes.string,
tag: PropTypes.string, token: PropTypes.object,
total: PropTypes.string, total: PropTypes.string,
totalError: PropTypes.string, totalError: PropTypes.string,
value: PropTypes.string, value: PropTypes.string,
@ -57,13 +56,13 @@ export default class Details extends Component {
}; };
render () { render () {
const { all, extras, tag, total, totalError, value, valueError } = this.props; const { all, extras, token, total, totalError, value, valueError } = this.props;
const label = ( const label = (
<FormattedMessage <FormattedMessage
id='transfer.details.amount.label' id='transfer.details.amount.label'
defaultMessage='amount to transfer (in {tag})' defaultMessage='amount to transfer (in {tag})'
values={ { values={ {
tag tag: token.tag
} } } }
/> />
); );
@ -140,7 +139,7 @@ export default class Details extends Component {
} }
renderFromAddress () { renderFromAddress () {
const { sender, senderError, senders, sendersBalances } = this.props; const { sender, senderError, senders } = this.props;
if (!senders) { if (!senders) {
return null; return null;
@ -165,7 +164,6 @@ export default class Details extends Component {
} }
value={ sender } value={ sender }
onChange={ this.onEditSender } onChange={ this.onEditSender }
balances={ sendersBalances }
/> />
</div> </div>
); );
@ -198,19 +196,19 @@ export default class Details extends Component {
} }
renderTokenSelect () { renderTokenSelect () {
const { balance, tag } = this.props; const { balance, token } = this.props;
return ( return (
<TokenSelect <TokenSelect
balance={ balance } balance={ balance }
tag={ tag }
onChange={ this.onChangeToken } onChange={ this.onChangeToken }
value={ token.id }
/> />
); );
} }
onChangeToken = (event, index, tag) => { onChangeToken = (event, index, tokenId) => {
this.props.onChange('tag', tag); this.props.onChange('token', tokenId);
} }
onEditSender = (event, sender) => { onEditSender = (event, sender) => {

View File

@ -16,6 +16,7 @@
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { MenuItem } from 'material-ui'; import { MenuItem } from 'material-ui';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
@ -24,7 +25,7 @@ import TokenImage from '~/ui/TokenImage';
import styles from '../transfer.css'; import styles from '../transfer.css';
export default class TokenSelect extends Component { class TokenSelect extends Component {
static contextTypes = { static contextTypes = {
api: PropTypes.object api: PropTypes.object
}; };
@ -32,7 +33,8 @@ export default class TokenSelect extends Component {
static propTypes = { static propTypes = {
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
balance: PropTypes.object.isRequired, balance: PropTypes.object.isRequired,
tag: PropTypes.string.isRequired tokens: PropTypes.object.isRequired,
value: PropTypes.string.isRequired
}; };
componentWillMount () { componentWillMount () {
@ -40,8 +42,10 @@ export default class TokenSelect extends Component {
} }
componentWillReceiveProps (nextProps) { componentWillReceiveProps (nextProps) {
const prevTokens = this.props.balance.tokens.map((t) => `${t.token.tag}_${t.value.toNumber()}`); const prevTokens = Object.keys(this.props.balance)
const nextTokens = nextProps.balance.tokens.map((t) => `${t.token.tag}_${t.value.toNumber()}`); .map((tokenId) => `${tokenId}_${this.props.balance[tokenId].toNumber()}`);
const nextTokens = Object.keys(nextProps.balance)
.map((tokenId) => `${tokenId}_${nextProps.balance[tokenId].toNumber()}`);
if (!isEqual(prevTokens, nextTokens)) { if (!isEqual(prevTokens, nextTokens)) {
this.computeTokens(nextProps); this.computeTokens(nextProps);
@ -50,23 +54,27 @@ export default class TokenSelect extends Component {
computeTokens (props = this.props) { computeTokens (props = this.props) {
const { api } = this.context; const { api } = this.context;
const { balance } = this.props; const { balance, tokens } = this.props;
const items = balance.tokens const items = Object.keys(balance)
.filter((token, index) => !index || token.value.gt(0)) .map((tokenId) => {
.map((balance, index) => { const token = tokens[tokenId];
const token = balance.token; const tokenValue = balance[tokenId];
const isEth = index === 0; const isEth = token.native;
if (!isEth && tokenValue.eq(0)) {
return null;
}
let value = 0; let value = 0;
if (isEth) { if (isEth) {
value = api.util.fromWei(balance.value).toFormat(3); value = api.util.fromWei(tokenValue).toFormat(3);
} else { } else {
const format = balance.token.format || 1; const format = token.format || 1;
const decimals = format === 1 ? 0 : Math.min(3, Math.floor(format / 10)); const decimals = format === 1 ? 0 : Math.min(3, Math.floor(format / 10));
value = new BigNumber(balance.value).div(format).toFormat(decimals); value = new BigNumber(tokenValue).div(format).toFormat(decimals);
} }
const label = ( const label = (
@ -83,20 +91,21 @@ export default class TokenSelect extends Component {
return ( return (
<MenuItem <MenuItem
key={ `${index}_${token.tag}` } key={ tokenId }
value={ token.tag } value={ token.id }
label={ label } label={ label }
> >
{ label } { label }
</MenuItem> </MenuItem>
); );
}); })
.filter((node) => node);
this.setState({ items }); this.setState({ items });
} }
render () { render () {
const { tag, onChange } = this.props; const { onChange, value } = this.props;
const { items } = this.state; const { items } = this.state;
return ( return (
@ -104,7 +113,7 @@ export default class TokenSelect extends Component {
className={ styles.tokenSelect } className={ styles.tokenSelect }
label='type of token transfer' label='type of token transfer'
hint='type of token to transfer' hint='type of token to transfer'
value={ tag } value={ value }
onChange={ onChange } onChange={ onChange }
> >
{ items } { items }
@ -112,3 +121,11 @@ export default class TokenSelect extends Component {
); );
} }
} }
function mapStateToProps (state) {
const { tokens } = state;
return { tokens };
}
export default connect(mapStateToProps)(TokenSelect);

View File

@ -18,11 +18,12 @@ import { noop } from 'lodash';
import { observable, computed, action, transaction } from 'mobx'; import { observable, computed, action, transaction } from 'mobx';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { wallet as walletAbi } from '~/contracts/abi'; import { eip20 as tokenAbi, wallet as walletAbi } from '~/contracts/abi';
import { fromWei } from '~/api/util/wei'; import { fromWei } from '~/api/util/wei';
import Contract from '~/api/contract'; import Contract from '~/api/contract';
import ERRORS from './errors'; import ERRORS from './errors';
import { DEFAULT_GAS, DEFAULT_GASPRICE, MAX_GAS_ESTIMATION } from '~/util/constants'; import { DEFAULT_GAS, DEFAULT_GASPRICE, MAX_GAS_ESTIMATION } from '~/util/constants';
import { ETH_TOKEN } from '~/util/tokens';
import GasPriceStore from '~/ui/GasPriceEditor/store'; import GasPriceStore from '~/ui/GasPriceEditor/store';
import { getLogger, LOG_KEYS } from '~/config'; import { getLogger, LOG_KEYS } from '~/config';
@ -40,10 +41,10 @@ export const WALLET_WARNING_SPENT_TODAY_LIMIT = 'WALLET_WARNING_SPENT_TODAY_LIMI
export default class TransferStore { export default class TransferStore {
@observable stage = 0; @observable stage = 0;
@observable extras = false; @observable extras = false;
@observable isEth = true;
@observable valueAll = false; @observable valueAll = false;
@observable sending = false; @observable sending = false;
@observable tag = 'ETH'; @observable token = ETH_TOKEN;
@observable isEth = true;
@observable data = ''; @observable data = '';
@observable dataError = null; @observable dataError = null;
@ -69,6 +70,8 @@ export default class TransferStore {
onClose = noop; onClose = noop;
senders = null; senders = null;
isWallet = false; isWallet = false;
tokenContract = null;
tokens = {};
wallet = null; wallet = null;
gasStore = null; gasStore = null;
@ -76,14 +79,16 @@ export default class TransferStore {
constructor (api, props) { constructor (api, props) {
this.api = api; this.api = api;
const { account, balance, gasLimit, onClose, senders, newError, sendersBalances } = props; const { account, balance, gasLimit, onClose, senders, newError, sendersBalances, tokens } = props;
this.account = account; this.account = account;
this.balance = balance; this.balance = balance;
this.isWallet = account && account.wallet; this.isWallet = account && account.wallet;
this.newError = newError; this.newError = newError;
this.tokens = tokens;
this.gasStore = new GasPriceStore(api, { gasLimit }); this.gasStore = new GasPriceStore(api, { gasLimit });
this.tokenContract = api.newContract(tokenAbi, '');
if (this.isWallet) { if (this.isWallet) {
this.wallet = props.wallet; this.wallet = props.wallet;
@ -126,10 +131,6 @@ export default class TransferStore {
} }
} }
get token () {
return this.balance.tokens.find((balance) => balance.token.tag === this.tag).token;
}
@action onNext = () => { @action onNext = () => {
this.stage += 1; this.stage += 1;
} }
@ -166,8 +167,8 @@ export default class TransferStore {
case 'sender': case 'sender':
return this._onUpdateSender(value); return this._onUpdateSender(value);
case 'tag': case 'token':
return this._onUpdateTag(value); return this._onUpdateToken(value);
case 'value': case 'value':
return this._onUpdateValue(value); return this._onUpdateValue(value);
@ -244,10 +245,10 @@ export default class TransferStore {
}); });
} }
@action _onUpdateTag = (tag) => { @action _onUpdateToken = (tokenId) => {
transaction(() => { transaction(() => {
this.tag = tag; this.token = { ...this.tokens[tokenId] };
this.isEth = tag.toLowerCase().trim() === 'eth'; this.isEth = this.token.native;
this.recalculateGas(); this.recalculateGas();
}); });
@ -323,46 +324,15 @@ export default class TransferStore {
return balance; return balance;
} }
getToken (tag = this.tag, forceSender = false) {
const balance = this.getBalance(forceSender);
if (!balance) {
return null;
}
const _tag = tag.toLowerCase();
const token = balance.tokens.find((b) => b.token.tag.toLowerCase() === _tag);
return token;
}
/** /**
* Return the balance of the selected token * Return the balance of the selected token
* (in WEI for ETH, without formating for other tokens) * (in WEI for ETH, without formating for other tokens)
*/ */
getTokenBalance (tag = this.tag, forceSender = false) { getTokenBalance (token = this.token, forceSender = false) {
const token = this.getToken(tag, forceSender); return new BigNumber(this.balance[token.id] || 0);
if (!token) {
return new BigNumber(0);
}
const value = new BigNumber(token.value || 0);
return value;
} }
getTokenValue (tag = this.tag, value = this.value, inverse = false) { getTokenValue (token = this.token, value = this.value, inverse = false) {
const token = this.getToken(tag);
if (!token) {
return new BigNumber(0);
}
const format = token.token
? new BigNumber(token.token.format || 1)
: new BigNumber(1);
let _value; let _value;
try { try {
@ -371,19 +341,11 @@ export default class TransferStore {
_value = new BigNumber(0); _value = new BigNumber(0);
} }
if (token.token && token.token.tag.toLowerCase() === 'eth') {
if (inverse) {
return this.api.util.fromWei(_value);
}
return this.api.util.toWei(_value);
}
if (inverse) { if (inverse) {
return _value.div(format); return _value.div(token.format);
} }
return _value.mul(format); return _value.mul(token.format);
} }
getValues (_gasTotal) { getValues (_gasTotal) {
@ -425,7 +387,7 @@ export default class TransferStore {
} }
// Otherwise, substract the gas estimate // Otherwise, substract the gas estimate
const availableEth = this.getTokenBalance('ETH'); const availableEth = this.getTokenBalance(ETH_TOKEN);
const totalEthValue = availableEth.gt(gasTotal) const totalEthValue = availableEth.gt(gasTotal)
? availableEth.minus(gasTotal) ? availableEth.minus(gasTotal)
: new BigNumber(0); : new BigNumber(0);
@ -437,15 +399,7 @@ export default class TransferStore {
} }
getFormattedTokenValue (tokenValue) { getFormattedTokenValue (tokenValue) {
const token = this.getToken(); return this.getTokenValue(this.token, tokenValue, true);
if (!token) {
return new BigNumber(0);
}
const tag = token.token && token.token.tag || '';
return this.getTokenValue(tag, tokenValue, true);
} }
@action recalculate = (redo = false) => { @action recalculate = (redo = false) => {
@ -463,7 +417,7 @@ export default class TransferStore {
const gasTotal = new BigNumber(this.gasStore.price || 0).mul(new BigNumber(this.gasStore.gas || 0)); const gasTotal = new BigNumber(this.gasStore.price || 0).mul(new BigNumber(this.gasStore.gas || 0));
const ethBalance = this.getTokenBalance('ETH', true); const ethBalance = this.getTokenBalance(ETH_TOKEN, true);
const tokenBalance = this.getTokenBalance(); const tokenBalance = this.getTokenBalance();
const { eth, token } = this.getValues(gasTotal); const { eth, token } = this.getValues(gasTotal);
@ -545,7 +499,7 @@ export default class TransferStore {
return this.wallet.instance.execute; return this.wallet.instance.execute;
} }
return this.token.contract.instance.transfer; return this.tokenContract.at(this.token.address).instance.transfer;
} }
_getData (gas = false) { _getData (gas = false) {
@ -558,7 +512,7 @@ export default class TransferStore {
const func = this._getTransferMethod(gas, true); const func = this._getTransferMethod(gas, true);
const { options, values } = this._getTransferParams(gas, true); const { options, values } = this._getTransferParams(gas, true);
return this.token.contract.getCallData(func, options, values); return this.tokenContract.at(this.token.address).getCallData(func, options, values);
} }
_getTransferParams (gas = false, forceToken = false) { _getTransferParams (gas = false, forceToken = false) {
@ -587,7 +541,7 @@ export default class TransferStore {
} }
if (isWallet && !forceToken) { if (isWallet && !forceToken) {
const to = isEth ? this.recipient : this.token.contract.address; const to = isEth ? this.recipient : this.token.address;
const value = isEth ? token : new BigNumber(0); const value = isEth ? token : new BigNumber(0);
const values = [ const values = [
@ -621,14 +575,7 @@ export default class TransferStore {
} }
_validateDecimals (num) { _validateDecimals (num) {
const { balance } = this; const s = new BigNumber(num).mul(this.token.format || 1).toFixed();
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) { if (s.indexOf('.') !== -1) {
return ERRORS.invalidDecimals; return ERRORS.invalidDecimals;

View File

@ -45,12 +45,13 @@ class Transfer extends Component {
newError: PropTypes.func.isRequired, newError: PropTypes.func.isRequired,
gasLimit: PropTypes.object.isRequired, gasLimit: PropTypes.object.isRequired,
senders: nullableProptype(PropTypes.object),
sendersBalances: nullableProptype(PropTypes.object),
account: PropTypes.object, account: PropTypes.object,
balance: PropTypes.object, balance: PropTypes.object,
wallet: PropTypes.object, onClose: PropTypes.func,
onClose: PropTypes.func senders: nullableProptype(PropTypes.object),
sendersBalances: nullableProptype(PropTypes.object),
tokens: PropTypes.object,
wallet: PropTypes.object
} }
store = new TransferStore(this.context.api, this.props); store = new TransferStore(this.context.api, this.props);
@ -144,8 +145,8 @@ class Transfer extends Component {
renderDetailsPage () { renderDetailsPage () {
const { account, balance, senders } = this.props; const { account, balance, senders } = this.props;
const { recipient, recipientError, sender, senderError, sendersBalances } = this.store; const { recipient, recipientError, sender, senderError } = this.store;
const { valueAll, extras, tag, total, totalError, value, valueError } = this.store; const { valueAll, extras, token, total, totalError, value, valueError } = this.store;
return ( return (
<Details <Details
@ -159,8 +160,7 @@ class Transfer extends Component {
sender={ sender } sender={ sender }
senderError={ senderError } senderError={ senderError }
senders={ senders } senders={ senders }
sendersBalances={ sendersBalances } token={ token }
tag={ tag }
total={ total } total={ total }
totalError={ totalError } totalError={ totalError }
value={ value } value={ value }
@ -272,6 +272,7 @@ class Transfer extends Component {
} }
function mapStateToProps (initState, initProps) { function mapStateToProps (initState, initProps) {
const { tokens } = initState;
const { address } = initProps.account; const { address } = initProps.account;
const isWallet = initProps.account && initProps.account.wallet; const isWallet = initProps.account && initProps.account.wallet;
@ -291,9 +292,12 @@ function mapStateToProps (initState, initProps) {
return (state) => { return (state) => {
const { gasLimit } = state.nodeStatus; const { gasLimit } = state.nodeStatus;
const sendersBalances = senders ? pick(state.balances.balances, Object.keys(senders)) : null; const { balances } = state;
return { gasLimit, wallet, senders, sendersBalances }; const balance = balances[address];
const sendersBalances = senders ? pick(balances, Object.keys(senders)) : null;
return { balance, gasLimit, senders, sendersBalances, tokens, wallet };
}; };
} }

View File

@ -33,7 +33,6 @@ class VaultAccounts extends Component {
static propTypes = { static propTypes = {
accounts: PropTypes.object.isRequired, accounts: PropTypes.object.isRequired,
balances: PropTypes.object.isRequired,
newError: PropTypes.func.isRequired, newError: PropTypes.func.isRequired,
personalAccountsInfo: PropTypes.func.isRequired, personalAccountsInfo: PropTypes.func.isRequired,
vaultStore: PropTypes.object.isRequired vaultStore: PropTypes.object.isRequired
@ -108,13 +107,9 @@ class VaultAccounts extends Component {
} }
renderAccount = (account) => { renderAccount = (account) => {
const { balances } = this.props;
const balance = balances[account.address];
return ( return (
<AccountCard <AccountCard
account={ account } account={ account }
balance={ balance }
/> />
); );
} }
@ -171,12 +166,10 @@ class VaultAccounts extends Component {
} }
function mapStateToProps (state) { function mapStateToProps (state) {
const { balances } = state.balances;
const { accounts } = state.personal; const { accounts } = state.personal;
return { return {
accounts, accounts
balances
}; };
} }

View File

@ -16,7 +16,8 @@
import { throttle } from 'lodash'; import { throttle } from 'lodash';
import { loadTokens, setTokenReg, fetchBalances, fetchTokens, fetchTokensBalances } from './balancesActions'; import { fetchBalances, fetchTokensBalances, queryTokensFilter } from './balancesActions';
import { loadTokens, fetchTokens } from './tokensActions';
import { padRight } from '~/api/util/format'; import { padRight } from '~/api/util/format';
import Contracts from '~/contracts'; import Contracts from '~/contracts';
@ -193,6 +194,7 @@ export default class Balances {
return console.warn('_subscribeBlockNumber', error); return console.warn('_subscribeBlockNumber', error);
} }
this._store.dispatch(queryTokensFilter());
return this.fetchAllBalances(); return this.fetchAllBalances();
}) })
.then((blockNumberSID) => { .then((blockNumberSID) => {
@ -276,7 +278,6 @@ export default class Balances {
.then((tokenreg) => { .then((tokenreg) => {
this._tokenreg = tokenreg; this._tokenreg = tokenreg;
this._store.dispatch(setTokenReg(tokenreg));
this._store.dispatch(loadTokens(options)); this._store.dispatch(loadTokens(options));
return this.attachToTokens(tokenreg); return this.attachToTokens(tokenreg);

View File

@ -14,119 +14,19 @@
// 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 { range, uniq, isEqual } from 'lodash'; import { uniq, isEqual } from 'lodash';
import BigNumber from 'bignumber.js';
import { push } from 'react-router-redux'; import { push } from 'react-router-redux';
import { hashToImageUrl } from './imagesReducer';
import { setAddressImage } from './imagesActions';
import * as ABIS from '~/contracts/abi';
import { notifyTransaction } from '~/util/notifications'; import { notifyTransaction } from '~/util/notifications';
import { ETH_TOKEN, fetchAccountsBalances } from '~/util/tokens';
import { LOG_KEYS, getLogger } from '~/config'; import { LOG_KEYS, getLogger } from '~/config';
import imagesEthereum from '~/../assets/images/contracts/ethereum-black-64x64.png'; import { sha3 } from '~/api/util/sha3';
const TRANSFER_SIGNATURE = sha3('Transfer(address,address,uint256)');
const log = getLogger(LOG_KEYS.Balances); const log = getLogger(LOG_KEYS.Balances);
const ETH = { let tokensFilter = {};
name: 'Ethereum',
tag: 'ETH',
image: imagesEthereum,
native: true
};
function setBalances (_balances, skipNotifications = false) {
return (dispatch, getState) => {
const state = getState();
const currentTokens = Object.values(state.balances.tokens || {});
const tokensAddresses = currentTokens
.map((token) => token.address)
.filter((address) => address);
const accounts = state.personal.accounts;
const nextBalances = _balances;
const prevBalances = state.balances.balances;
const balances = { ...prevBalances };
Object.keys(nextBalances).forEach((address) => {
if (!balances[address]) {
balances[address] = Object.assign({}, nextBalances[address]);
return;
}
const balance = Object.assign({}, balances[address]);
const { tokens, txCount = balance.txCount } = nextBalances[address];
const prevTokens = balance.tokens.slice();
const nextTokens = [];
const handleToken = (prevToken, nextToken) => {
// If the given token is not in the current tokens, skip
if (!nextToken && !prevToken) {
return false;
}
// No updates
if (!nextToken) {
return nextTokens.push(prevToken);
}
const { token, value } = nextToken;
// If it's a new token, push it
if (!prevToken) {
return nextTokens.push({
token, value
});
}
// Otherwise, update the value
const prevValue = prevToken.value;
// If received a token/eth (old value < new value), notify
if (prevValue.lt(value) && accounts[address] && !skipNotifications) {
const account = accounts[address];
const txValue = value.minus(prevValue);
const redirectToAccount = () => {
const basePath = account.wallet
? 'wallet'
: 'accounts';
const route = `/${basePath}/${account.address}`;
dispatch(push(route));
};
notifyTransaction(account, token, txValue, redirectToAccount);
}
return nextTokens.push({
...prevToken,
value
});
};
const prevEthToken = prevTokens.find((tok) => tok.token.native);
const nextEthToken = tokens.find((tok) => tok.token.native);
handleToken(prevEthToken, nextEthToken);
tokensAddresses
.forEach((address) => {
const prevToken = prevTokens.find((tok) => tok.token.address === address);
const nextToken = tokens.find((tok) => tok.token.address === address);
handleToken(prevToken, nextToken);
});
balances[address] = { txCount: txCount || new BigNumber(0), tokens: nextTokens };
});
dispatch(_setBalances(balances));
};
}
function _setBalances (balances) { function _setBalances (balances) {
return { return {
@ -135,129 +35,88 @@ function _setBalances (balances) {
}; };
} }
export function setTokens (tokens) { /**
return { * @param {Object} _balances - In the shape:
type: 'setTokens', * {
tokens * [ who ]: { [ tokenId ]: BigNumber } // The balances of `who`
}; * }
} * @param {Boolean} skipNotifications [description]
*/
export function setTokenReg (tokenreg) { function setBalances (updates, skipNotifications = false) {
return {
type: 'setTokenReg',
tokenreg
};
}
export function setTokensFilter (tokensFilter) {
return {
type: 'setTokensFilter',
tokensFilter
};
}
export function setTokenImage (tokenAddress, image) {
return {
type: 'setTokenImage',
tokenAddress, image
};
}
export function loadTokens (options = {}) {
log.debug('loading tokens', Object.keys(options).length ? options : '');
return (dispatch, getState) => { return (dispatch, getState) => {
const { tokenreg } = getState().balances; const { tokens, balances } = getState();
return tokenreg.instance.tokenCount const prevBalances = balances;
.call() const nextBalances = { ...prevBalances };
.then((numTokens) => {
const tokenIds = range(numTokens.toNumber());
dispatch(fetchTokens(tokenIds, options)); Object.keys(updates)
}) .forEach((who) => {
.catch((error) => { const accountUpdates = updates[who];
console.warn('balances::loadTokens', error);
});
};
}
export function fetchTokens (_tokenIds, options = {}) { Object.keys(accountUpdates)
const tokenIds = uniq(_tokenIds || []); .forEach((tokenId) => {
const token = tokens[tokenId];
const prevTokenValue = (prevBalances[who] || {})[tokenId];
const nextTokenValue = accountUpdates[tokenId];
return (dispatch, getState) => { if (prevTokenValue && prevTokenValue.lt(nextTokenValue)) {
const { api, images, balances } = getState(); dispatch(notifyBalanceChange(who, prevTokenValue, nextTokenValue, token));
const { tokenreg } = balances;
return Promise
.all(tokenIds.map((id) => fetchTokenInfo(tokenreg, id, api)))
// FIXME ; shouldn't have to filter out tokens...
.then((tokens) => tokens.filter((token) => token.tag && token.tag.toLowerCase() !== 'eth'))
.then((tokens) => {
// dispatch only the changed images
tokens
.forEach((token) => {
const { image, address } = token;
if (images[address] === image) {
return;
} }
dispatch(setTokenImage(address, image)); // Add the token if it's native ETH or if it has a value
dispatch(setAddressImage(address, image, true)); if (token.native || nextTokenValue.gt(0)) {
nextBalances[who] = {
...(nextBalances[who] || {}),
[tokenId]: nextTokenValue
};
}
}); });
log.debug('fetched token', tokens);
dispatch(setTokens(tokens));
dispatch(updateTokensFilter(null, null, options));
})
.catch((error) => {
console.warn('balances::fetchTokens', error);
}); });
return dispatch(_setBalances(nextBalances));
}; };
} }
export function fetchBalances (_addresses, skipNotifications = false) { function notifyBalanceChange (who, fromValue, toValue, token) {
return (dispatch, getState) => { return (dispatch, getState) => {
const { api, personal } = getState(); const account = getState().personal.accounts[who];
const { visibleAccounts, accounts } = personal;
const addresses = uniq(_addresses || visibleAccounts || []); if (account) {
const txValue = toValue.minus(fromValue);
// With only a single account, more info will be displayed. const redirectToAccount = () => {
const fullFetch = addresses.length === 1; const basePath = account.wallet
? 'wallet'
: 'accounts';
// Add accounts addresses (for notifications, accounts selection, etc.) const route = `/${basePath}/${account.address}`;
const addressesToFetch = uniq(addresses.concat(Object.keys(accounts)));
return Promise dispatch(push(route));
.all(addressesToFetch.map((addr) => fetchAccount(addr, api, fullFetch))) };
.then((accountsBalances) => {
const balances = {};
addressesToFetch.forEach((addr, idx) => { notifyTransaction(account, token, txValue, redirectToAccount);
balances[addr] = accountsBalances[idx]; }
});
dispatch(setBalances(balances, skipNotifications));
})
.catch((error) => {
console.warn('balances::fetchBalances', error);
});
}; };
} }
// TODO: fetch txCount when needed
export function fetchBalances (_addresses, skipNotifications = false) {
return fetchTokensBalances(_addresses, [ ETH_TOKEN ], skipNotifications);
}
export function updateTokensFilter (_addresses, _tokens, options = {}) { export function updateTokensFilter (_addresses, _tokens, options = {}) {
return (dispatch, getState) => { return (dispatch, getState) => {
const { api, balances, personal } = getState(); const { api, personal, tokens } = getState();
const { visibleAccounts, accounts } = personal; const { visibleAccounts, accounts } = personal;
const { tokensFilter } = balances;
const addressesToFetch = uniq(visibleAccounts.concat(Object.keys(accounts))); const addressesToFetch = uniq(visibleAccounts.concat(Object.keys(accounts)));
const addresses = uniq(_addresses || addressesToFetch || []).sort(); const addresses = uniq(_addresses || addressesToFetch || []).sort();
const tokens = _tokens || Object.values(balances.tokens) || []; const tokensToUpdate = _tokens || Object.values(tokens);
const tokenAddresses = tokens.map((t) => t.address).sort(); const tokenAddresses = tokensToUpdate
.map((t) => t.address)
.filter((address) => address)
.sort();
if (tokensFilter.filterFromId || tokensFilter.filterToId) { if (tokensFilter.filterFromId || tokensFilter.filterToId) {
// Has the tokens addresses changed (eg. a network change) // Has the tokens addresses changed (eg. a network change)
@ -287,23 +146,22 @@ export function updateTokensFilter (_addresses, _tokens, options = {}) {
const promise = Promise.all(promises); const promise = Promise.all(promises);
const TRANSFER_SIGNATURE = api.util.sha3('Transfer(address,address,uint256)');
const topicsFrom = [ TRANSFER_SIGNATURE, addresses, null ]; const topicsFrom = [ TRANSFER_SIGNATURE, addresses, null ];
const topicsTo = [ TRANSFER_SIGNATURE, null, addresses ]; const topicsTo = [ TRANSFER_SIGNATURE, null, addresses ];
const options = { const filterOptions = {
fromBlock: 0, fromBlock: 0,
toBlock: 'pending', toBlock: 'pending',
address: tokenAddresses address: tokenAddresses
}; };
const optionsFrom = { const optionsFrom = {
...options, ...filterOptions,
topics: topicsFrom topics: topicsFrom
}; };
const optionsTo = { const optionsTo = {
...options, ...filterOptions,
topics: topicsTo topics: topicsTo
}; };
@ -322,8 +180,8 @@ export function updateTokensFilter (_addresses, _tokens, options = {}) {
const { skipNotifications } = options; const { skipNotifications } = options;
dispatch(setTokensFilter(nextTokensFilter)); tokensFilter = nextTokensFilter;
fetchTokensBalances(addresses, tokens, skipNotifications)(dispatch, getState); fetchTokensBalances(addresses, tokensToUpdate, skipNotifications)(dispatch, getState);
}) })
.catch((error) => { .catch((error) => {
console.warn('balances::updateTokensFilter', error); console.warn('balances::updateTokensFilter', error);
@ -331,13 +189,14 @@ export function updateTokensFilter (_addresses, _tokens, options = {}) {
}; };
} }
export function queryTokensFilter (tokensFilter) { export function queryTokensFilter () {
return (dispatch, getState) => { return (dispatch, getState) => {
const { api, personal, balances } = getState(); const { api, personal, tokens } = getState();
const { visibleAccounts, accounts } = personal; const { visibleAccounts, accounts } = personal;
const visibleAddresses = visibleAccounts.map((a) => a.toLowerCase()); const allAddresses = visibleAccounts.concat(Object.keys(accounts));
const addressesToFetch = uniq(visibleAddresses.concat(Object.keys(accounts))); const addressesToFetch = uniq(allAddresses);
const lcAddresses = addressesToFetch.map((a) => a.toLowerCase());
Promise Promise
.all([ .all([
@ -347,21 +206,28 @@ export function queryTokensFilter (tokensFilter) {
.then(([ logsFrom, logsTo ]) => { .then(([ logsFrom, logsTo ]) => {
const addresses = []; const addresses = [];
const tokenAddresses = []; const tokenAddresses = [];
const logs = logsFrom.concat(logsTo);
logsFrom if (logs.length > 0) {
.concat(logsTo) log.debug('got tokens filter logs', logs);
}
logs
.forEach((log) => { .forEach((log) => {
const tokenAddress = log.address; const tokenAddress = log.address;
const fromAddress = '0x' + log.topics[1].slice(-40); const fromAddress = '0x' + log.topics[1].slice(-40);
const toAddress = '0x' + log.topics[2].slice(-40); const toAddress = '0x' + log.topics[2].slice(-40);
if (addressesToFetch.includes(fromAddress)) { const fromAddressIndex = lcAddresses.indexOf(fromAddress);
addresses.push(fromAddress); const toAddressIndex = lcAddresses.indexOf(toAddress);
if (fromAddressIndex > -1) {
addresses.push(addressesToFetch[fromAddressIndex]);
} }
if (addressesToFetch.includes(toAddress)) { if (toAddressIndex > -1) {
addresses.push(toAddress); addresses.push(addressesToFetch[toAddressIndex]);
} }
tokenAddresses.push(tokenAddress); tokenAddresses.push(tokenAddress);
@ -371,36 +237,35 @@ export function queryTokensFilter (tokensFilter) {
return; return;
} }
const tokens = Object.values(balances.tokens) const tokensToUpdate = Object.values(tokens)
.filter((t) => tokenAddresses.includes(t.address)); .filter((t) => tokenAddresses.includes(t.address));
fetchTokensBalances(uniq(addresses), tokens)(dispatch, getState); fetchTokensBalances(uniq(addresses), tokensToUpdate)(dispatch, getState);
}); });
}; };
} }
export function fetchTokensBalances (_addresses = null, _tokens = null, skipNotifications = false) { export function fetchTokensBalances (_addresses = null, _tokens = null, skipNotifications = false) {
return (dispatch, getState) => { return (dispatch, getState) => {
const { api, personal, balances } = getState(); const { api, personal, tokens } = getState();
const { visibleAccounts, accounts } = personal; const { visibleAccounts, accounts } = personal;
const allTokens = Object.values(tokens);
const addressesToFetch = uniq(visibleAccounts.concat(Object.keys(accounts))); const addressesToFetch = uniq(visibleAccounts.concat(Object.keys(accounts)));
const addresses = _addresses || addressesToFetch; const addresses = _addresses || addressesToFetch;
const tokens = _tokens || Object.values(balances.tokens); const tokensToUpdate = _tokens || allTokens;
if (addresses.length === 0) { if (addresses.length === 0) {
return Promise.resolve(); return Promise.resolve();
} }
return Promise const updates = addresses.reduce((updates, who) => {
.all(addresses.map((addr) => fetchTokensBalance(addr, tokens, api))) updates[who] = tokensToUpdate.map((token) => token.id);
.then((tokensBalances) => { return updates;
const balances = {}; }, {});
addresses.forEach((addr, idx) => {
balances[addr] = tokensBalances[idx];
});
return fetchAccountsBalances(api, allTokens, updates)
.then((balances) => {
dispatch(setBalances(balances, skipNotifications)); dispatch(setBalances(balances, skipNotifications));
}) })
.catch((error) => { .catch((error) => {
@ -408,78 +273,3 @@ export function fetchTokensBalances (_addresses = null, _tokens = null, skipNoti
}); });
}; };
} }
function fetchAccount (address, api, full = false) {
const promises = [ api.eth.getBalance(address) ];
if (full) {
promises.push(api.eth.getTransactionCount(address));
}
return Promise
.all(promises)
.then(([ ethBalance, txCount ]) => {
const tokens = [ { token: ETH, value: ethBalance } ];
const balance = { tokens };
if (full) {
balance.txCount = txCount;
}
return balance;
})
.catch((error) => {
console.warn('balances::fetchAccountBalance', `couldn't fetch balance for account #${address}`, error);
});
}
function fetchTokensBalance (address, _tokens, api) {
const tokensPromises = _tokens
.map((token) => {
return token.contract.instance.balanceOf.call({}, [ address ]);
});
return Promise
.all(tokensPromises)
.then((tokensBalance) => {
const tokens = _tokens
.map((token, index) => ({
token,
value: tokensBalance[index]
}));
const balance = { tokens };
return balance;
})
.catch((error) => {
console.warn('balances::fetchTokensBalance', `couldn't fetch tokens balance for account #${address}`, error);
});
}
function fetchTokenInfo (tokenreg, tokenId, api, dispatch) {
return Promise
.all([
tokenreg.instance.token.call({}, [tokenId]),
tokenreg.instance.meta.call({}, [tokenId, 'IMG'])
])
.then(([ tokenData, image ]) => {
const [ address, tag, format, name ] = tokenData;
const contract = api.newContract(ABIS.eip20, address);
const token = {
format: format.toString(),
id: tokenId,
image: hashToImageUrl(image),
address,
tag,
name,
contract
};
return token;
})
.catch((error) => {
console.warn('balances::fetchTokenInfo', `couldn't fetch token #${tokenId}`, error);
});
}

View File

@ -16,72 +16,12 @@
import { handleActions } from 'redux-actions'; import { handleActions } from 'redux-actions';
const initialState = { const initialState = {};
balances: {},
tokens: {},
tokenreg: null,
tokensFilter: {}
};
export default handleActions({ export default handleActions({
setBalances (state, action) { setBalances (state, action) {
const { balances } = action; const { balances } = action;
return Object.assign({}, state, { balances }); return Object.assign({}, state, balances);
},
setTokens (state, action) {
const { tokens } = action;
if (Array.isArray(tokens)) {
const objTokens = tokens.reduce((_tokens, token) => {
_tokens[token.address] = token;
return _tokens;
}, {});
return Object.assign({}, state, { tokens: objTokens });
}
return Object.assign({}, state, { tokens });
},
setTokenImage (state, action) {
const { tokenAddress, image } = action;
const { balances } = state;
const nextBalances = {};
Object.keys(balances).forEach((address) => {
const tokenIndex = balances[address].tokens.findIndex((t) => t.token.address === tokenAddress);
if (tokenIndex === -1 || balances[address].tokens[tokenIndex].value.equals(0)) {
return;
}
const tokens = [].concat(balances[address].tokens);
tokens[tokenIndex].token = {
...tokens[tokenIndex].token,
image
};
nextBalances[address] = {
...balances[address],
tokens
};
});
return Object.assign({}, state, { balance: { ...balances, nextBalances } });
},
setTokenReg (state, action) {
const { tokenreg } = action;
return Object.assign({}, state, { tokenreg });
},
setTokensFilter (state, action) {
const { tokensFilter } = action;
return Object.assign({}, state, { tokensFilter });
} }
}, initialState); }, initialState);

View File

@ -1,128 +0,0 @@
// Copyright 2015-2017 Parity Technologies (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 Contracts from '~/contracts';
export function setBlock (blockNumber, block) {
return {
type: 'setBlock',
blockNumber, block
};
}
export function setTransaction (txHash, info) {
return {
type: 'setTransaction',
txHash, info
};
}
export function setBytecode (address, bytecode) {
return {
type: 'setBytecode',
address, bytecode
};
}
export function setMethod (signature, method) {
return {
type: 'setMethod',
signature, method
};
}
export function fetchBlock (blockNumber) {
return (dispatch, getState) => {
const { blocks } = getState().blockchain;
if (blocks[blockNumber.toString()]) {
return;
}
const { api } = getState();
api.eth
.getBlockByNumber(blockNumber)
.then(block => {
dispatch(setBlock(blockNumber, block));
})
.catch(e => {
console.error('blockchain::fetchBlock', e);
});
};
}
export function fetchTransaction (txHash) {
return (dispatch, getState) => {
const { transactions } = getState().blockchain;
if (transactions[txHash]) {
return;
}
const { api } = getState();
api.eth
.getTransactionByHash(txHash)
.then(info => {
dispatch(setTransaction(txHash, info));
})
.catch(e => {
console.error('blockchain::fetchTransaction', e);
});
};
}
export function fetchBytecode (address) {
return (dispatch, getState) => {
const { bytecodes } = getState().blockchain;
if (bytecodes[address]) {
return;
}
const { api } = getState();
api.eth
.getCode(address)
.then(code => {
dispatch(setBytecode(address, code));
})
.catch(e => {
console.error('blockchain::fetchBytecode', e);
});
};
}
export function fetchMethod (signature) {
return (dispatch, getState) => {
const { methods } = getState().blockchain;
if (methods[signature]) {
return;
}
Contracts
.get()
.signatureReg.lookup(signature)
.then(method => {
dispatch(setMethod(signature, method));
})
.catch(e => {
console.error('blockchain::fetchMethod', e);
});
};
}

View File

@ -1,66 +0,0 @@
// Copyright 2015-2017 Parity Technologies (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 { handleActions } from 'redux-actions';
const initialState = {
blocks: {},
transactions: {},
bytecodes: {},
methods: {}
};
export default handleActions({
setBlock (state, action) {
const { blockNumber, block } = action;
const blocks = Object.assign({}, state.blocks, {
[blockNumber.toString()]: block
});
return Object.assign({}, state, { blocks });
},
setTransaction (state, action) {
const { txHash, info } = action;
const transactions = Object.assign({}, state.transactions, {
[txHash]: info
});
return Object.assign({}, state, { transactions });
},
setBytecode (state, action) {
const { address, bytecode } = action;
const bytecodes = Object.assign({}, state.bytecodes, {
[address]: bytecode
});
return Object.assign({}, state, { bytecodes });
},
setMethod (state, action) {
const { signature, method } = action;
const methods = Object.assign({}, state.methods, {
[signature]: method
});
return Object.assign({}, state, { methods });
}
}, initialState);

View File

@ -21,7 +21,6 @@ export Status from './status';
export apiReducer from './apiReducer'; export apiReducer from './apiReducer';
export balancesReducer from './balancesReducer'; export balancesReducer from './balancesReducer';
export blockchainReducer from './blockchainReducer';
export workerReducer from './workerReducer'; export workerReducer from './workerReducer';
export imagesReducer from './imagesReducer'; export imagesReducer from './imagesReducer';
export personalReducer from './personalReducer'; export personalReducer from './personalReducer';
@ -29,4 +28,5 @@ export requestsReducer from './requestsReducer';
export signerReducer from './signerReducer'; export signerReducer from './signerReducer';
export snackbarReducer from './snackbarReducer'; export snackbarReducer from './snackbarReducer';
export statusReducer from './statusReducer'; export statusReducer from './statusReducer';
export tokensReducer from './tokensReducer';
export walletReducer from './walletReducer'; export walletReducer from './walletReducer';

View File

@ -35,6 +35,12 @@ export default class Personal {
return; return;
} }
// Add the address to each accounts
Object.keys(accountsInfo)
.forEach((address) => {
accountsInfo[address].address = address;
});
this._store.dispatch(personalAccountsInfo(accountsInfo)); this._store.dispatch(personalAccountsInfo(accountsInfo));
}); });
} }

View File

@ -0,0 +1,88 @@
// Copyright 2015-2017 Parity Technologies (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 { uniq } from 'lodash';
import Contracts from '~/contracts';
import { LOG_KEYS, getLogger } from '~/config';
import { fetchTokenIds, fetchTokenInfo } from '~/util/tokens';
import { updateTokensFilter } from './balancesActions';
import { setAddressImage } from './imagesActions';
const log = getLogger(LOG_KEYS.Balances);
export function setTokens (tokens) {
return {
type: 'setTokens',
tokens
};
}
export function loadTokens (options = {}) {
log.debug('loading tokens', Object.keys(options).length ? options : '');
return (dispatch, getState) => {
const { tokenReg } = Contracts.get();
tokenReg.getInstance()
.then((tokenRegInstance) => {
return fetchTokenIds(tokenRegInstance);
})
.then((tokenIndexes) => dispatch(fetchTokens(tokenIndexes, options)))
.catch((error) => {
console.warn('tokens::loadTokens', error);
});
};
}
export function fetchTokens (_tokenIndexes, options = {}) {
const tokenIndexes = uniq(_tokenIndexes || []);
return (dispatch, getState) => {
const { api, images } = getState();
const { tokenReg } = Contracts.get();
return tokenReg.getInstance()
.then((tokenRegInstance) => {
const promises = tokenIndexes.map((id) => fetchTokenInfo(api, tokenRegInstance, id));
return Promise.all(promises);
})
.then((results) => {
const tokens = results
.reduce((tokens, token) => {
const { id, image, address } = token;
// dispatch only the changed images
if (images[address] !== image) {
dispatch(setAddressImage(address, image, true));
}
tokens[id] = token;
return tokens;
}, {});
log.debug('fetched token', tokens);
dispatch(setTokens(tokens));
dispatch(updateTokensFilter(null, null, options));
})
.catch((error) => {
console.warn('tokens::fetchTokens', error);
});
};
}

View File

@ -0,0 +1,34 @@
// Copyright 2015-2017 Parity Technologies (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 { handleActions } from 'redux-actions';
import { ETH_TOKEN } from '~/util/tokens';
const initialState = {
[ ETH_TOKEN.id ]: ETH_TOKEN
};
export default handleActions({
setTokens (state, action) {
const { tokens } = action;
return {
...state,
...tokens
};
}
}, initialState);

View File

@ -18,10 +18,10 @@ import { combineReducers } from 'redux';
import { routerReducer } from 'react-router-redux'; import { routerReducer } from 'react-router-redux';
import { import {
apiReducer, balancesReducer, blockchainReducer, apiReducer, balancesReducer,
workerReducer, imagesReducer, personalReducer, requestsReducer, workerReducer, imagesReducer, personalReducer, requestsReducer,
signerReducer, statusReducer as nodeStatusReducer, signerReducer, statusReducer as nodeStatusReducer,
snackbarReducer, walletReducer snackbarReducer, tokensReducer, walletReducer
} from './providers'; } from './providers';
import certificationsReducer from './providers/certifications/reducer'; import certificationsReducer from './providers/certifications/reducer';
import registryReducer from './providers/registry/reducer'; import registryReducer from './providers/registry/reducer';
@ -40,7 +40,6 @@ export default function () {
balances: balancesReducer, balances: balancesReducer,
certifications: certificationsReducer, certifications: certificationsReducer,
blockchain: blockchainReducer,
images: imagesReducer, images: imagesReducer,
nodeStatus: nodeStatusReducer, nodeStatus: nodeStatusReducer,
personal: personalReducer, personal: personalReducer,
@ -48,6 +47,7 @@ export default function () {
requests: requestsReducer, requests: requestsReducer,
signer: signerReducer, signer: signerReducer,
snackbar: snackbarReducer, snackbar: snackbarReducer,
tokens: tokensReducer,
wallet: walletReducer, wallet: walletReducer,
worker: workerReducer worker: workerReducer
}); });

View File

@ -84,6 +84,7 @@ export default class AccountCard extends Component {
</div> </div>
<Balance <Balance
address={ address }
balance={ balance } balance={ balance }
className={ styles.balance } className={ styles.balance }
showOnlyEth showOnlyEth

View File

@ -14,10 +14,13 @@
// 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 { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import React from 'react'; import React from 'react';
import sinon from 'sinon'; import sinon from 'sinon';
import { ETH_TOKEN } from '~/util/tokens';
import AccountCard from './'; import AccountCard from './';
const TEST_ADDRESS = '0x1234567890123456789012345678901234567890'; const TEST_ADDRESS = '0x1234567890123456789012345678901234567890';
@ -27,6 +30,21 @@ let component;
let onClick; let onClick;
let onFocus; let onFocus;
function reduxStore () {
const getState = () => ({
balances: {},
tokens: {
[ETH_TOKEN.id]: ETH_TOKEN
}
});
return {
getState,
dispatch: () => null,
subscribe: () => null
};
}
function render (props = {}) { function render (props = {}) {
if (!props.account) { if (!props.account) {
props.account = { props.account = {
@ -39,6 +57,12 @@ function render (props = {}) {
}; };
} }
if (!props.balance) {
props.balance = {
[ETH_TOKEN.id]: new BigNumber(10)
};
}
onClick = sinon.stub(); onClick = sinon.stub();
onFocus = sinon.stub(); onFocus = sinon.stub();
@ -67,7 +91,9 @@ describe('ui/AccountCard', () => {
let balance; let balance;
beforeEach(() => { beforeEach(() => {
balance = component.find('Balance'); balance = component.find('Connect(Balance)').shallow({
context: { store: reduxStore() }
});
}); });
it('renders the balance', () => { it('renders the balance', () => {

View File

@ -17,18 +17,21 @@
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import React, { Component, PropTypes } from 'react'; import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import TokenImage from '~/ui/TokenImage'; import TokenImage from '~/ui/TokenImage';
import styles from './balance.css'; import styles from './balance.css';
export default class Balance extends Component { export class Balance extends Component {
static contextTypes = { static contextTypes = {
api: PropTypes.object api: PropTypes.object
}; };
static propTypes = { static propTypes = {
balance: PropTypes.object, balance: PropTypes.object.isRequired,
tokens: PropTypes.object.isRequired,
address: PropTypes.string,
className: PropTypes.string, className: PropTypes.string,
showOnlyEth: PropTypes.bool, showOnlyEth: PropTypes.bool,
showZeroValues: PropTypes.bool showZeroValues: PropTypes.bool
@ -40,44 +43,38 @@ export default class Balance extends Component {
}; };
render () { render () {
const { api } = this.context; const { balance, className, showOnlyEth, tokens } = this.props;
const { balance, className, showOnlyEth } = this.props;
if (!balance || !balance.tokens) { if (Object.keys(balance).length === 0) {
return null; return null;
} }
let body = balance.tokens let body = Object.keys(balance)
.filter((balance) => { .map((tokenId) => {
const isEthToken = (balance.token.tag || '').toLowerCase() === 'eth'; const token = tokens[tokenId];
const hasBalance = new BigNumber(balance.value).gt(0); const balanceValue = balance[tokenId];
return hasBalance || isEthToken; const isEthToken = token.native;
}) const isFullToken = !showOnlyEth || isEthToken;
.map((balance, index) => { const hasBalance = balanceValue.gt(0);
const isFullToken = !showOnlyEth || (balance.token.tag || '').toLowerCase() === 'eth';
const token = balance.token;
let value; if (!hasBalance && !isEthToken) {
return null;
if (token.format) {
const bnf = new BigNumber(token.format);
let decimals = 0;
if (bnf.gte(1000)) {
decimals = 3;
} else if (bnf.gte(100)) {
decimals = 2;
} else if (bnf.gte(10)) {
decimals = 1;
}
value = new BigNumber(balance.value).div(bnf).toFormat(decimals);
} else {
value = api.util.fromWei(balance.value).toFormat(3);
} }
const bnf = new BigNumber(token.format || 1);
let decimals = 0;
if (bnf.gte(1000)) {
decimals = 3;
} else if (bnf.gte(100)) {
decimals = 2;
} else if (bnf.gte(10)) {
decimals = 1;
}
const value = new BigNumber(balanceValue).div(bnf).toFormat(decimals);
const classNames = [styles.balance]; const classNames = [styles.balance];
let details = null; let details = null;
@ -104,13 +101,14 @@ export default class Balance extends Component {
return ( return (
<div <div
className={ classNames.join(' ') } className={ classNames.join(' ') }
key={ `${index}_${token.tag}` } key={ tokenId }
> >
<TokenImage token={ token } /> <TokenImage token={ token } />
{ details } { details }
</div> </div>
); );
}); })
.filter((node) => node);
if (!body.length) { if (!body.length) {
body = ( body = (
@ -140,3 +138,15 @@ export default class Balance extends Component {
); );
} }
} }
function mapStateToProps (state, props) {
const { balances, tokens } = state;
const { address } = props;
return {
balance: balances[address] || props.balance || {},
tokens
};
}
export default connect(mapStateToProps)(Balance);

View File

@ -14,19 +14,24 @@
// 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 { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import React from 'react'; import React from 'react';
import apiutil from '~/api/util'; import apiutil from '~/api/util';
import Balance from './'; import { Balance } from './balance';
const TOKENS = {
'eth': { tag: 'ETH' },
'gav': { tag: 'GAV', format: 1 },
'tst': { tag: 'TST', format: 1 }
};
const BALANCE = { const BALANCE = {
tokens: [ 'eth': new BigNumber(122),
{ value: '122', token: { tag: 'ETH' } }, 'gav': new BigNumber(345),
{ value: '345', token: { tag: 'GAV', format: 1 } }, 'tst': new BigNumber(0)
{ value: '0', token: { tag: 'TST', format: 1 } }
]
}; };
let api; let api;
@ -46,6 +51,10 @@ function render (props = {}) {
props.balance = BALANCE; props.balance = BALANCE;
} }
if (!props.tokens) {
props.tokens = TOKENS;
}
const api = createApi(); const api = createApi();
component = shallow( component = shallow(

View File

@ -32,6 +32,7 @@ $transitionAll: all 0.75s cubic-bezier(0.23, 1, 0.32, 1);
.hoverOverlay { .hoverOverlay {
background: $background; background: $background;
display: none;
left: 0; left: 0;
margin-top: -1.5em; margin-top: -1.5em;
margin-bottom: 3em; margin-bottom: 3em;
@ -52,6 +53,7 @@ $transitionAll: all 0.75s cubic-bezier(0.23, 1, 0.32, 1);
.hoverOverlay { .hoverOverlay {
background: $backgroundHover; background: $backgroundHover;
display: block;
/*transform: scale(1, 1);*/ /*transform: scale(1, 1);*/
opacity: 1; opacity: 1;
} }

View File

@ -55,7 +55,6 @@ class AddressSelect extends Component {
// Redux props // Redux props
accountsInfo: PropTypes.object, accountsInfo: PropTypes.object,
accounts: PropTypes.object, accounts: PropTypes.object,
balances: PropTypes.object,
contacts: PropTypes.object, contacts: PropTypes.object,
contracts: PropTypes.object, contracts: PropTypes.object,
tokens: PropTypes.object, tokens: PropTypes.object,
@ -356,10 +355,9 @@ class AddressSelect extends Component {
} }
renderAccountCard (_account) { renderAccountCard (_account) {
const { balances, accountsInfo } = this.props; const { accountsInfo } = this.props;
const { address, index = null } = _account; const { address, index = null } = _account;
const balance = balances[address];
const account = { const account = {
...accountsInfo[address], ...accountsInfo[address],
..._account ..._account
@ -368,7 +366,6 @@ class AddressSelect extends Component {
return ( return (
<AccountCard <AccountCard
account={ account } account={ account }
balance={ balance }
className={ styles.account } className={ styles.account }
key={ `account_${index}` } key={ `account_${index}` }
onClick={ this.handleClick } onClick={ this.handleClick }
@ -650,12 +647,10 @@ class AddressSelect extends Component {
function mapStateToProps (state) { function mapStateToProps (state) {
const { accountsInfo } = state.personal; const { accountsInfo } = state.personal;
const { balances } = state.balances;
const { reverse } = state.registry; const { reverse } = state.registry;
return { return {
accountsInfo, accountsInfo,
balances,
reverse reverse
}; };
} }

View File

@ -175,14 +175,12 @@ function mapStateToProps (state, props) {
const lcValue = value.toLowerCase(); const lcValue = value.toLowerCase();
const { accountsInfo } = state.personal; const { accountsInfo } = state.personal;
const { tokens } = state.balances; const { tokens } = state;
const accountsInfoAddress = Object.keys(accountsInfo).find((address) => address.toLowerCase() === lcValue); const accountInfo = Object.values(accountsInfo).find((account) => account.address.toLowerCase() === lcValue);
const tokensAddress = Object.keys(tokens).find((address) => address.toLowerCase() === lcValue); const token = Object.values(tokens).find((token) => token.address.toLowerCase() === lcValue);
const account = (accountsInfoAddress && accountsInfo[accountsInfoAddress]) || const account = accountInfo || token || null;
(tokensAddress && tokens[tokensAddress]) ||
null;
return { return {
account account

View File

@ -34,21 +34,19 @@ const defaultNameNull = (
/> />
); );
class IdentityName extends Component { export class IdentityName extends Component {
static propTypes = { static propTypes = {
accountsInfo: PropTypes.object, account: PropTypes.object,
address: PropTypes.string, address: PropTypes.string,
className: PropTypes.string, className: PropTypes.string,
empty: PropTypes.bool, empty: PropTypes.bool,
name: PropTypes.string, name: PropTypes.string,
shorten: PropTypes.bool, shorten: PropTypes.bool,
tokens: PropTypes.object,
unknown: PropTypes.bool unknown: PropTypes.bool
} }
render () { render () {
const { address, accountsInfo, className, empty, name, shorten, tokens, unknown } = this.props; const { account, address, className, empty, name, shorten, unknown } = this.props;
const account = accountsInfo[address] || tokens[address];
if (!account && empty) { if (!account && empty) {
return null; return null;
@ -76,13 +74,14 @@ class IdentityName extends Component {
} }
} }
function mapStateToProps (state) { function mapStateToProps (state, props) {
const { accountsInfo } = state.personal; const { address } = props;
const { tokens } = state.balances; const { personal, tokens } = state;
const account = personal.accountsInfo[address] || Object.values(tokens).find((token) => token.address === address);
return { return {
accountsInfo, account
tokens
}; };
} }

View File

@ -17,40 +17,19 @@
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import React from 'react'; import React from 'react';
import sinon from 'sinon'; import { IdentityName } from './identityName';
import IdentityName from './identityName';
const ADDR_A = '0x123456789abcdef0123456789A'; const ADDR_A = '0x123456789abcdef0123456789A';
const ADDR_B = '0x123456789abcdef0123456789B';
const ADDR_C = '0x123456789abcdef0123456789C'; const ADDR_C = '0x123456789abcdef0123456789C';
const ADDR_NULL = '0x0000000000000000000000000000000000000000'; const ADDR_NULL = '0x0000000000000000000000000000000000000000';
const NAME_JIMMY = 'Jimmy Test'; const NAME_JIMMY = 'Jimmy Test';
const STORE = {
dispatch: sinon.stub(),
subscribe: sinon.stub(),
getState: () => {
return {
balances: {
tokens: {}
},
personal: {
accountsInfo: {
[ADDR_A]: { name: 'testing' },
[ADDR_B]: {}
}
}
};
}
};
function render (props) { function render (props) {
return shallow( return shallow(
<IdentityName <IdentityName
store={ STORE }
{ ...props } { ...props }
/> />
).find('IdentityName').shallow(); );
} }
describe('ui/IdentityName', () => { describe('ui/IdentityName', () => {

View File

@ -653,10 +653,10 @@ class MethodDecoding extends Component {
} }
function mapStateToProps (initState, initProps) { function mapStateToProps (initState, initProps) {
const { tokens } = initState.balances; const { tokens } = initState;
const { transaction } = initProps; const { transaction } = initProps;
const token = (tokens || {})[transaction.to]; const token = Object.values(tokens).find((token) => token.address === transaction.to);
return () => { return () => {
return { token }; return { token };

View File

@ -17,20 +17,12 @@
import Push from 'push.js'; import Push from 'push.js';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { fromWei } from '~/api/util/wei';
import ethereumIcon from '~/../assets/images/contracts/ethereum-black-64x64.png';
import unkownIcon from '~/../assets/images/contracts/unknown-64x64.png'; import unkownIcon from '~/../assets/images/contracts/unknown-64x64.png';
export function notifyTransaction (account, token, _value, onClick) { export function notifyTransaction (account, token, _value, onClick) {
const name = account.name || account.address; const name = account.name || account.address;
const value = token.tag.toLowerCase() === 'eth' const value = _value.div(new BigNumber(token.format || 1));
? fromWei(_value) const icon = token.image || unkownIcon;
: _value.div(new BigNumber(token.format || 1));
const icon = token.tag.toLowerCase() === 'eth'
? ethereumIcon
: (token.image || unkownIcon);
let _notification = null; let _notification = null;

133
js/src/util/tokens.js Normal file
View File

@ -0,0 +1,133 @@
// Copyright 2015-2017 Parity Technologies (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 { range } from 'lodash';
import BigNumber from 'bignumber.js';
import { hashToImageUrl } from '~/redux/util';
import { sha3 } from '~/api/util/sha3';
import imagesEthereum from '~/../assets/images/contracts/ethereum-black-64x64.png';
const BALANCEOF_SIGNATURE = sha3('balanceOf(address)');
const ADDRESS_PADDING = range(24).map(() => '0').join('');
export const ETH_TOKEN = {
address: '',
format: new BigNumber(10).pow(18),
id: sha3('eth_native_token').slice(0, 10),
image: imagesEthereum,
name: 'Ethereum',
native: true,
tag: 'ETH'
};
export function fetchTokenIds (tokenregInstance) {
return tokenregInstance.tokenCount
.call()
.then((numTokens) => {
const tokenIndexes = range(numTokens.toNumber());
return tokenIndexes;
});
}
export function fetchTokenInfo (api, tokenregInstace, tokenIndex) {
return Promise
.all([
tokenregInstace.token.call({}, [tokenIndex]),
tokenregInstace.meta.call({}, [tokenIndex, 'IMG'])
])
.then(([ tokenData, image ]) => {
const [ address, tag, format, name ] = tokenData;
const token = {
format: format.toString(),
index: tokenIndex,
image: hashToImageUrl(image),
id: sha3(address + tokenIndex).slice(0, 10),
address,
name,
tag
};
return token;
});
}
/**
* `updates` should be in the shape:
* {
* [ who ]: [ tokenId ] // Array of tokens to updates
* }
*
* Returns a Promise resolved witht the balances in the shape:
* {
* [ who ]: { [ tokenId ]: BigNumber } // The balances of `who`
* }
*/
export function fetchAccountsBalances (api, tokens, updates) {
const addresses = Object.keys(updates);
const promises = addresses
.map((who) => {
const tokensIds = updates[who];
const tokensToUpdate = tokensIds.map((tokenId) => tokens.find((t) => t.id === tokenId));
return fetchAccountBalances(api, tokensToUpdate, who);
});
return Promise.all(promises)
.then((results) => {
return results.reduce((balances, accountBalances, index) => {
balances[addresses[index]] = accountBalances;
return balances;
}, {});
});
}
/**
* Returns a Promise resolved with the balances in the shape:
* {
* [ tokenId ]: BigNumber // Token balance value
* }
*/
export function fetchAccountBalances (api, tokens, who) {
const calldata = '0x' + BALANCEOF_SIGNATURE.slice(2, 10) + ADDRESS_PADDING + who.slice(2);
const promises = tokens.map((token) => fetchTokenBalance(api, token, { who, calldata }));
return Promise.all(promises)
.then((results) => {
return results.reduce((balances, value, index) => {
const token = tokens[index];
balances[token.id] = value;
return balances;
}, {});
});
}
export function fetchTokenBalance (api, token, { who, calldata }) {
if (token.native) {
return api.eth.getBalance(who);
}
return api.eth
.call({ data: calldata, to: token.address })
.then((result) => {
const cleanResult = result.replace(/^0x/, '');
return new BigNumber(`0x${cleanResult || 0}`);
});
}

View File

@ -22,9 +22,12 @@ import { Balance, Certifications, Container, CopyToClipboard, ContainerTitle, Id
import styles from './header.css'; import styles from './header.css';
export default class Header extends Component { export default class Header extends Component {
static contextTypes = {
api: PropTypes.object.isRequired
};
static propTypes = { static propTypes = {
account: PropTypes.object, account: PropTypes.object,
balance: PropTypes.object,
children: PropTypes.node, children: PropTypes.node,
className: PropTypes.string, className: PropTypes.string,
disabled: PropTypes.bool, disabled: PropTypes.bool,
@ -39,8 +42,49 @@ export default class Header extends Component {
isContract: false isContract: false
}; };
state = {
txCount: null
};
txCountSubId = null;
componentWillMount () {
if (this.props.account && !this.props.isContract) {
this.subscribeTxCount();
}
}
componentWillUnmount () {
this.unsubscribeTxCount();
}
subscribeTxCount () {
const { api } = this.context;
api
.subscribe('eth_blockNumber', (error) => {
if (error) {
return console.error(error);
}
api.eth.getTransactionCount(this.props.account.address)
.then((txCount) => this.setState({ txCount }));
})
.then((subscriptionId) => {
this.txCountSubId = subscriptionId;
});
}
unsubscribeTxCount () {
if (!this.txCountSubId) {
return;
}
this.context.api.unsubscribe(this.txCountSubId);
}
render () { render () {
const { account, balance, children, className, disabled, hideName } = this.props; const { account, children, className, disabled, hideName } = this.props;
if (!account) { if (!account) {
return null; return null;
@ -76,8 +120,7 @@ export default class Header extends Component {
{ this.renderTxCount() } { this.renderTxCount() }
<div className={ styles.balances }> <div className={ styles.balances }>
<Balance <Balance
account={ account } address={ address }
balance={ balance }
/> />
<Certifications address={ address } /> <Certifications address={ address } />
{ this.renderVault() } { this.renderVault() }
@ -115,15 +158,10 @@ export default class Header extends Component {
} }
renderTxCount () { renderTxCount () {
const { balance, isContract } = this.props; const { isContract } = this.props;
const { txCount } = this.state;
if (!balance || isContract) { if (!txCount || isContract) {
return null;
}
const { txCount } = balance;
if (!txCount) {
return null; return null;
} }

View File

@ -18,6 +18,8 @@ import BigNumber from 'bignumber.js';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import React from 'react'; import React from 'react';
import { ETH_TOKEN } from '~/util/tokens';
import Header from './'; import Header from './';
const ACCOUNT = { const ACCOUNT = {
@ -28,17 +30,44 @@ const ACCOUNT = {
}, },
uuid: '0xabcdef' uuid: '0xabcdef'
}; };
const subscriptions = {};
let component; let component;
let instance; let instance;
const api = {
subscribe: (method, callback) => {
subscriptions[method] = (subscriptions[method] || []).concat(callback);
return Promise.resolve(0);
},
eth: {
getTransactionCount: () => Promise.resolve(new BigNumber(1))
}
};
function reduxStore () {
const getState = () => ({
balances: {},
tokens: {
[ETH_TOKEN.id]: ETH_TOKEN
}
});
return {
getState,
dispatch: () => null,
subscribe: () => null
};
}
function render (props = {}) { function render (props = {}) {
if (props && !props.account) { if (props && !props.account) {
props.account = ACCOUNT; props.account = ACCOUNT;
} }
component = shallow( component = shallow(
<Header { ...props } /> <Header { ...props } />,
{ context: { api } }
); );
instance = component.instance(); instance = component.instance();
@ -72,8 +101,9 @@ describe('views/Account/Header', () => {
let balance; let balance;
beforeEach(() => { beforeEach(() => {
render({ balance: { balance: 'testing' } }); render();
balance = component.find('Balance'); balance = component.find('Connect(Balance)')
.shallow({ context: { store: reduxStore() } });
}); });
it('renders', () => { it('renders', () => {
@ -81,11 +111,7 @@ describe('views/Account/Header', () => {
}); });
it('passes the account', () => { it('passes the account', () => {
expect(balance.props().account).to.deep.equal(ACCOUNT); expect(balance.props().address).to.deep.equal(ACCOUNT.address);
});
it('passes the balance', () => {
}); });
}); });
@ -177,24 +203,33 @@ describe('views/Account/Header', () => {
}); });
describe('renderTxCount', () => { describe('renderTxCount', () => {
it('renders null when contract', () => {
render({ balance: { txCount: new BigNumber(1) }, isContract: true });
expect(instance.renderTxCount()).to.be.null;
});
it('renders null when no balance', () => {
render({ balance: null, isContract: false });
expect(instance.renderTxCount()).to.be.null;
});
it('renders null when txCount is null', () => { it('renders null when txCount is null', () => {
render({ balance: { txCount: null }, isContract: false }); render();
expect(instance.renderTxCount()).to.be.null; expect(instance.renderTxCount()).to.be.null;
}); });
it('renders null when contract', () => {
render({ isContract: true });
subscriptions['eth_blockNumber'].forEach((callback) => {
callback();
setTimeout(() => {
expect(instance.renderTxCount()).to.be.null;
});
});
});
it('renders the tx count', () => { it('renders the tx count', () => {
render({ balance: { txCount: new BigNumber(1) }, isContract: false }); render();
expect(instance.renderTxCount()).not.to.be.null;
subscriptions['eth_blockNumber'].forEach((callback) => {
callback();
setTimeout(() => {
expect(instance.renderTxCount()).not.to.be.null;
});
});
}); });
}); });

View File

@ -46,8 +46,7 @@ class Account extends Component {
fetchCertifications: PropTypes.func.isRequired, fetchCertifications: PropTypes.func.isRequired,
setVisibleAccounts: PropTypes.func.isRequired, setVisibleAccounts: PropTypes.func.isRequired,
accounts: PropTypes.object, account: PropTypes.object,
balances: PropTypes.object,
certifications: PropTypes.object, certifications: PropTypes.object,
netVersion: PropTypes.string.isRequired, netVersion: PropTypes.string.isRequired,
params: PropTypes.object params: PropTypes.object
@ -83,12 +82,9 @@ class Account extends Component {
} }
render () { render () {
const { accounts, balances } = this.props; const { account } = this.props;
const { address } = this.props.params; const { address } = this.props.params;
const account = (accounts || {})[address];
const balance = (balances || {})[address];
if (!account) { if (!account) {
return null; return null;
} }
@ -102,17 +98,15 @@ class Account extends Component {
{ this.renderFaucetDialog() } { this.renderFaucetDialog() }
{ this.renderFundDialog() } { this.renderFundDialog() }
{ this.renderPasswordDialog(account) } { this.renderPasswordDialog(account) }
{ this.renderTransferDialog(account, balance) } { this.renderTransferDialog(account) }
{ this.renderVerificationDialog() } { this.renderVerificationDialog() }
{ this.renderActionbar(account, balance) } { this.renderActionbar(account) }
<Page padded> <Page padded>
<Header <Header
account={ account } account={ account }
balance={ balance }
disabled={ !isAvailable } disabled={ !isAvailable }
/> />
<Transactions <Transactions
accounts={ accounts }
address={ address } address={ address }
/> />
</Page> </Page>
@ -143,16 +137,14 @@ class Account extends Component {
return certifications.length !== 0; return certifications.length !== 0;
} }
renderActionbar (account, balance) { renderActionbar (account) {
const { certifications, netVersion } = this.props; const { certifications, netVersion } = this.props;
const { address } = this.props.params; const { address } = this.props.params;
const showTransferButton = !!(balance && balance.tokens);
const isVerifiable = this.isMainnet(netVersion); const isVerifiable = this.isMainnet(netVersion);
const isFaucettable = this.isFaucettable(netVersion, certifications, address); const isFaucettable = this.isFaucettable(netVersion, certifications, address);
const buttons = [ const buttons = [
<Button <Button
disabled={ !showTransferButton }
icon={ <SendIcon /> } icon={ <SendIcon /> }
key='transferFunds' key='transferFunds'
label={ label={
@ -374,18 +366,14 @@ class Account extends Component {
); );
} }
renderTransferDialog (account, balance) { renderTransferDialog (account) {
if (!this.store.isTransferVisible) { if (!this.store.isTransferVisible) {
return null; return null;
} }
const { balances } = this.props;
return ( return (
<Transfer <Transfer
account={ account } account={ account }
balance={ balance }
balances={ balances }
onClose={ this.store.toggleTransferDialog } onClose={ this.store.toggleTransferDialog }
/> />
); );
@ -407,15 +395,17 @@ class Account extends Component {
} }
} }
function mapStateToProps (state) { function mapStateToProps (state, props) {
const { address } = props.params;
const { accounts } = state.personal; const { accounts } = state.personal;
const { balances } = state.balances;
const certifications = state.certifications; const certifications = state.certifications;
const { netVersion } = state.nodeStatus; const { netVersion } = state.nodeStatus;
const account = (accounts || {})[address];
return { return {
accounts, account,
balances,
certifications, certifications,
netVersion netVersion
}; };

View File

@ -14,21 +14,23 @@
// 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 { pick } from 'lodash';
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 { Container, SectionList } from '~/ui'; import { Container, SectionList } from '~/ui';
import { fetchCertifiers, fetchCertifications } from '~/redux/providers/certifications/actions'; import { fetchCertifiers, fetchCertifications } from '~/redux/providers/certifications/actions';
import { ETH_TOKEN } from '~/util/tokens';
import Summary from '../Summary'; import Summary from '../Summary';
import styles from './list.css'; import styles from './list.css';
class List extends Component { class List extends Component {
static propTypes = { static propTypes = {
accounts: PropTypes.object, balances: PropTypes.object.isRequired,
balances: PropTypes.object,
certifications: PropTypes.object.isRequired, certifications: PropTypes.object.isRequired,
accounts: PropTypes.object,
disabled: PropTypes.object, disabled: PropTypes.object,
empty: PropTypes.bool, empty: PropTypes.bool,
link: PropTypes.string, link: PropTypes.string,
@ -51,7 +53,7 @@ class List extends Component {
} }
render () { render () {
const { accounts, balances, disabled, empty } = this.props; const { accounts, disabled, empty } = this.props;
if (empty) { if (empty) {
return ( return (
@ -67,13 +69,11 @@ class List extends Component {
.getAddresses() .getAddresses()
.map((address) => { .map((address) => {
const account = accounts[address] || {}; const account = accounts[address] || {};
const balance = balances[address] || {};
const isDisabled = disabled ? disabled[address] : false; const isDisabled = disabled ? disabled[address] : false;
const owners = account.owners || null; const owners = account.owners || null;
return { return {
account, account,
balance,
isDisabled, isDisabled,
owners owners
}; };
@ -88,13 +88,12 @@ class List extends Component {
} }
renderSummary = (item) => { renderSummary = (item) => {
const { account, balance, isDisabled, owners } = item; const { account, isDisabled, owners } = item;
const { handleAddSearchToken, link } = this.props; const { handleAddSearchToken, link } = this.props;
return ( return (
<Summary <Summary
account={ account } account={ account }
balance={ balance }
disabled={ isDisabled } disabled={ isDisabled }
handleAddSearchToken={ handleAddSearchToken } handleAddSearchToken={ handleAddSearchToken }
link={ link } link={ link }
@ -160,8 +159,8 @@ class List extends Component {
return 1; return 1;
} }
const ethA = balanceA.tokens.find(token => token.token.tag.toLowerCase() === 'eth'); const ethA = balanceA[ETH_TOKEN.id];
const ethB = balanceB.tokens.find(token => token.token.tag.toLowerCase() === 'eth'); const ethB = balanceB[ETH_TOKEN.id];
if (!ethA && !ethB) { if (!ethA && !ethB) {
return 0; return 0;
@ -171,7 +170,7 @@ class List extends Component {
return 1; return 1;
} }
return -1 * ethA.value.comparedTo(ethB.value); return -1 * ethA.comparedTo(ethB);
} }
if (key === 'tags') { if (key === 'tags') {
@ -257,10 +256,12 @@ class List extends Component {
} }
} }
function mapStateToProps (state) { function mapStateToProps (state, props) {
const addresses = Object.keys(props.accounts);
const balances = pick(state.balances, addresses);
const { certifications } = state; const { certifications } = state;
return { certifications }; return { balances, certifications };
} }
function mapDispatchToProps (dispatch) { function mapDispatchToProps (dispatch) {

View File

@ -36,7 +36,6 @@ class Summary extends Component {
static propTypes = { static propTypes = {
account: PropTypes.object.isRequired, account: PropTypes.object.isRequired,
accountsInfo: PropTypes.object.isRequired, accountsInfo: PropTypes.object.isRequired,
balance: PropTypes.object,
disabled: PropTypes.bool, disabled: PropTypes.bool,
link: PropTypes.string, link: PropTypes.string,
name: PropTypes.string, name: PropTypes.string,
@ -74,20 +73,6 @@ class Summary extends Component {
return true; return true;
} }
const prevTokens = this.props.balance.tokens || [];
const nextTokens = nextProps.balance.tokens || [];
if (prevTokens.length !== nextTokens.length) {
return true;
}
const prevValues = prevTokens.map((t) => ({ value: t.value.toNumber(), image: t.token.image }));
const nextValues = nextTokens.map((t) => ({ value: t.value.toNumber(), image: t.token.image }));
if (!isEqual(prevValues, nextValues)) {
return true;
}
const prevOwners = this.props.owners; const prevOwners = this.props.owners;
const nextOwners = nextProps.owners; const nextOwners = nextProps.owners;
@ -249,15 +234,11 @@ class Summary extends Component {
} }
renderBalance (onlyEth) { renderBalance (onlyEth) {
const { balance } = this.props; const { account } = this.props;
if (!balance) {
return null;
}
return ( return (
<Balance <Balance
balance={ balance } address={ account.address }
className={ className={
onlyEth onlyEth
? styles.ethBalances ? styles.ethBalances

View File

@ -41,7 +41,6 @@ class Accounts extends Component {
static propTypes = { static propTypes = {
accounts: PropTypes.object.isRequired, accounts: PropTypes.object.isRequired,
accountsInfo: PropTypes.object.isRequired, accountsInfo: PropTypes.object.isRequired,
balances: PropTypes.object,
hasAccounts: PropTypes.bool.isRequired, hasAccounts: PropTypes.bool.isRequired,
setVisibleAccounts: PropTypes.func.isRequired setVisibleAccounts: PropTypes.func.isRequired
} }
@ -133,7 +132,7 @@ class Accounts extends Component {
} }
renderAccounts () { renderAccounts () {
const { accounts, balances } = this.props; const { accounts } = this.props;
const _accounts = pickBy(accounts, (account) => account.uuid); const _accounts = pickBy(accounts, (account) => account.uuid);
const _hasAccounts = Object.keys(_accounts).length > 0; const _hasAccounts = Object.keys(_accounts).length > 0;
@ -147,7 +146,6 @@ class Accounts extends Component {
<List <List
search={ searchValues } search={ searchValues }
accounts={ _accounts } accounts={ _accounts }
balances={ balances }
empty={ !_hasAccounts } empty={ !_hasAccounts }
order={ sortOrder } order={ sortOrder }
handleAddSearchToken={ this.onAddSearchToken } handleAddSearchToken={ this.onAddSearchToken }
@ -156,7 +154,7 @@ class Accounts extends Component {
} }
renderWallets () { renderWallets () {
const { accounts, balances } = this.props; const { accounts } = this.props;
const wallets = pickBy(accounts, (account) => account.wallet); const wallets = pickBy(accounts, (account) => account.wallet);
const hasWallets = Object.keys(wallets).length > 0; const hasWallets = Object.keys(wallets).length > 0;
@ -175,7 +173,6 @@ class Accounts extends Component {
link='wallet' link='wallet'
search={ searchValues } search={ searchValues }
accounts={ wallets } accounts={ wallets }
balances={ balances }
order={ sortOrder } order={ sortOrder }
handleAddSearchToken={ this.onAddSearchToken } handleAddSearchToken={ this.onAddSearchToken }
/> />
@ -183,7 +180,7 @@ class Accounts extends Component {
} }
renderExternalAccounts () { renderExternalAccounts () {
const { accounts, balances } = this.props; const { accounts } = this.props;
const { wallets } = this.hwstore; const { wallets } = this.hwstore;
const hardware = pickBy(accounts, (account) => account.hardware); const hardware = pickBy(accounts, (account) => account.hardware);
const external = pickBy(accounts, (account) => account.external); const external = pickBy(accounts, (account) => account.external);
@ -210,7 +207,6 @@ class Accounts extends Component {
<List <List
search={ searchValues } search={ searchValues }
accounts={ all } accounts={ all }
balances={ balances }
disabled={ disabled } disabled={ disabled }
order={ sortOrder } order={ sortOrder }
handleAddSearchToken={ this.onAddSearchToken } handleAddSearchToken={ this.onAddSearchToken }
@ -408,12 +404,10 @@ class Accounts extends Component {
function mapStateToProps (state) { function mapStateToProps (state) {
const { accounts, accountsInfo, hasAccounts } = state.personal; const { accounts, accountsInfo, hasAccounts } = state.personal;
const { balances } = state.balances;
return { return {
accounts, accounts,
accountsInfo, accountsInfo,
balances,
hasAccounts hasAccounts
}; };
} }

View File

@ -38,7 +38,6 @@ class Address extends Component {
setVisibleAccounts: PropTypes.func.isRequired, setVisibleAccounts: PropTypes.func.isRequired,
contacts: PropTypes.object, contacts: PropTypes.object,
balances: PropTypes.object,
params: PropTypes.object params: PropTypes.object
}; };
@ -73,7 +72,7 @@ class Address extends Component {
} }
render () { render () {
const { contacts, balances } = this.props; const { contacts } = this.props;
const { address } = this.props.params; const { address } = this.props.params;
if (Object.keys(contacts).length === 0) { if (Object.keys(contacts).length === 0) {
@ -81,7 +80,6 @@ class Address extends Component {
} }
const contact = (contacts || {})[address]; const contact = (contacts || {})[address];
const balance = (balances || {})[address];
return ( return (
<div> <div>
@ -92,7 +90,6 @@ class Address extends Component {
<Page padded> <Page padded>
<Header <Header
account={ contact || { address, meta: {} } } account={ contact || { address, meta: {} } }
balance={ balance }
hideName={ !contact } hideName={ !contact }
/> />
<Transactions <Transactions
@ -240,11 +237,9 @@ class Address extends Component {
function mapStateToProps (state) { function mapStateToProps (state) {
const { contacts } = state.personal; const { contacts } = state.personal;
const { balances } = state.balances;
return { return {
contacts, contacts
balances
}; };
} }

View File

@ -24,7 +24,7 @@ import { uniq, isEqual } from 'lodash';
import List from '../Accounts/List'; import List from '../Accounts/List';
import Summary from '../Accounts/Summary'; import Summary from '../Accounts/Summary';
import { AddAddress } from '~/modals'; import { AddAddress } from '~/modals';
import { Actionbar, ActionbarExport, ActionbarImport, ActionbarSearch, ActionbarSort, Button, Page, Loading } from '~/ui'; import { Actionbar, ActionbarExport, ActionbarImport, ActionbarSearch, ActionbarSort, Button, Page } from '~/ui';
import { setVisibleAccounts } from '~/redux/providers/personalActions'; import { setVisibleAccounts } from '~/redux/providers/personalActions';
import styles from './addresses.css'; import styles from './addresses.css';
@ -37,7 +37,6 @@ class Addresses extends Component {
static propTypes = { static propTypes = {
setVisibleAccounts: PropTypes.func.isRequired, setVisibleAccounts: PropTypes.func.isRequired,
balances: PropTypes.object,
contacts: PropTypes.object, contacts: PropTypes.object,
hasContacts: PropTypes.bool hasContacts: PropTypes.bool
} }
@ -86,21 +85,14 @@ class Addresses extends Component {
} }
renderAccountsList () { renderAccountsList () {
const { balances, contacts, hasContacts } = this.props; const { contacts, hasContacts } = this.props;
const { searchValues, sortOrder } = this.state; const { searchValues, sortOrder } = this.state;
if (hasContacts && Object.keys(balances).length === 0) {
return (
<Loading />
);
}
return ( return (
<List <List
link='addresses' link='addresses'
search={ searchValues } search={ searchValues }
accounts={ contacts } accounts={ contacts }
balances={ balances }
empty={ !hasContacts } empty={ !hasContacts }
order={ sortOrder } order={ sortOrder }
handleAddSearchToken={ this.onAddSearchToken } handleAddSearchToken={ this.onAddSearchToken }
@ -282,11 +274,9 @@ class Addresses extends Component {
} }
function mapStateToProps (state) { function mapStateToProps (state) {
const { balances } = state.balances;
const { contacts, hasContacts } = state.personal; const { contacts, hasContacts } = state.personal;
return { return {
balances,
contacts, contacts,
hasContacts hasContacts
}; };

View File

@ -45,7 +45,6 @@ class Contract extends Component {
accounts: PropTypes.object, accounts: PropTypes.object,
accountsInfo: PropTypes.object, accountsInfo: PropTypes.object,
balances: PropTypes.object,
contracts: PropTypes.object, contracts: PropTypes.object,
netVersion: PropTypes.string.isRequired, netVersion: PropTypes.string.isRequired,
params: PropTypes.object params: PropTypes.object
@ -115,10 +114,9 @@ class Contract extends Component {
} }
render () { render () {
const { accountsInfo, balances, contracts, netVersion, params } = this.props; const { accountsInfo, contracts, netVersion, params } = this.props;
const { allEvents, contract, queryValues, loadingEvents } = this.state; const { allEvents, contract, queryValues, loadingEvents } = this.state;
const account = contracts[params.address]; const account = contracts[params.address];
const balance = balances[params.address];
if (!account) { if (!account) {
return null; return null;
@ -133,7 +131,6 @@ class Contract extends Component {
<Page padded> <Page padded>
<Header <Header
account={ account } account={ account }
balance={ balance }
isContract isContract
> >
{ this.renderBlockNumber(account.meta) } { this.renderBlockNumber(account.meta) }
@ -520,13 +517,11 @@ class Contract extends Component {
function mapStateToProps (state) { function mapStateToProps (state) {
const { accounts, accountsInfo, contracts } = state.personal; const { accounts, accountsInfo, contracts } = state.personal;
const { balances } = state.balances;
const { netVersion } = state.nodeStatus; const { netVersion } = state.nodeStatus;
return { return {
accounts, accounts,
accountsInfo, accountsInfo,
balances,
contracts, contracts,
netVersion netVersion
}; };

View File

@ -57,7 +57,6 @@ class Contracts extends Component {
static propTypes = { static propTypes = {
setVisibleAccounts: PropTypes.func.isRequired, setVisibleAccounts: PropTypes.func.isRequired,
balances: PropTypes.object,
accounts: PropTypes.object, accounts: PropTypes.object,
contracts: PropTypes.object, contracts: PropTypes.object,
hasContracts: PropTypes.bool hasContracts: PropTypes.bool
@ -96,7 +95,7 @@ class Contracts extends Component {
} }
render () { render () {
const { contracts, hasContracts, balances } = this.props; const { contracts, hasContracts } = this.props;
const { searchValues, sortOrder } = this.state; const { searchValues, sortOrder } = this.state;
return ( return (
@ -109,7 +108,6 @@ class Contracts extends Component {
link='contracts' link='contracts'
search={ searchValues } search={ searchValues }
accounts={ contracts } accounts={ contracts }
balances={ balances }
empty={ !hasContracts } empty={ !hasContracts }
order={ sortOrder } order={ sortOrder }
orderFallback='name' orderFallback='name'
@ -267,13 +265,11 @@ class Contracts extends Component {
function mapStateToProps (state) { function mapStateToProps (state) {
const { accounts, contracts, hasContracts } = state.personal; const { accounts, contracts, hasContracts } = state.personal;
const { balances } = state.balances;
return { return {
accounts, accounts,
contracts, contracts,
hasContracts, hasContracts
balances
}; };
} }

View File

@ -48,7 +48,6 @@ class ParityBar extends Component {
}; };
static propTypes = { static propTypes = {
balances: PropTypes.object,
dapp: PropTypes.bool, dapp: PropTypes.bool,
externalLink: PropTypes.string, externalLink: PropTypes.string,
pending: PropTypes.array pending: PropTypes.array
@ -370,13 +369,9 @@ class ParityBar extends Component {
} }
renderAccount = (account) => { renderAccount = (account) => {
const { balances } = this.props;
const balance = balances[account.address];
return ( return (
<AccountCard <AccountCard
account={ account } account={ account }
balance={ balance }
/> />
); );
} }
@ -700,11 +695,9 @@ class ParityBar extends Component {
} }
function mapStateToProps (state) { function mapStateToProps (state) {
const { balances } = state.balances;
const { pending } = state.signer; const { pending } = state.signer;
return { return {
balances,
pending pending
}; };
} }

View File

@ -65,7 +65,6 @@ class Wallet extends Component {
static propTypes = { static propTypes = {
address: PropTypes.string.isRequired, address: PropTypes.string.isRequired,
balance: nullableProptype(PropTypes.object.isRequired),
netVersion: PropTypes.string.isRequired, netVersion: PropTypes.string.isRequired,
owned: PropTypes.bool.isRequired, owned: PropTypes.bool.isRequired,
setVisibleAccounts: PropTypes.func.isRequired, setVisibleAccounts: PropTypes.func.isRequired,
@ -105,7 +104,7 @@ class Wallet extends Component {
} }
render () { render () {
const { walletAccount, balance, wallet } = this.props; const { walletAccount, wallet } = this.props;
if (!walletAccount) { if (!walletAccount) {
return null; return null;
@ -125,7 +124,6 @@ class Wallet extends Component {
<Header <Header
className={ styles.header } className={ styles.header }
account={ walletAccount } account={ walletAccount }
balance={ balance }
isContract isContract
> >
{ this.renderInfos() } { this.renderInfos() }
@ -212,15 +210,13 @@ class Wallet extends Component {
} }
renderActionbar () { renderActionbar () {
const { balance, owned } = this.props; const { owned } = this.props;
const showTransferButton = !!(balance && balance.tokens);
const buttons = []; const buttons = [];
if (owned) { if (owned) {
buttons.push( buttons.push(
<Button <Button
disabled={ !showTransferButton }
icon={ <SendIcon /> } icon={ <SendIcon /> }
key='transferFunds' key='transferFunds'
label={ label={
@ -343,12 +339,11 @@ class Wallet extends Component {
return null; return null;
} }
const { walletAccount, balance } = this.props; const { walletAccount } = this.props;
return ( return (
<Transfer <Transfer
account={ walletAccount } account={ walletAccount }
balance={ balance }
onClose={ this.onTransferClose } onClose={ this.onTransferClose }
/> />
); );
@ -391,7 +386,6 @@ function mapStateToProps (_, initProps) {
return (state) => { return (state) => {
const { netVersion } = state.nodeStatus; const { netVersion } = state.nodeStatus;
const { accountsInfo = {}, accounts = {} } = state.personal; const { accountsInfo = {}, accounts = {} } = state.personal;
const { balances } = state.balances;
const walletAccount = accounts[address] || accountsInfo[address] || null; const walletAccount = accounts[address] || accountsInfo[address] || null;
if (walletAccount) { if (walletAccount) {
@ -399,12 +393,10 @@ function mapStateToProps (_, initProps) {
} }
const wallet = state.wallet.wallets[address] || {}; const wallet = state.wallet.wallets[address] || {};
const balance = balances[address] || null;
const owned = !!accounts[address]; const owned = !!accounts[address];
return { return {
address, address,
balance,
netVersion, netVersion,
owned, owned,
wallet, wallet,

View File

@ -446,9 +446,9 @@ class WriteContract extends Component {
return ( return (
<DeployContract <DeployContract
abi={ contract.interface } abi={ contract.interface }
accounts={ this.props.accounts }
code={ `0x${contract.bytecode}` } code={ `0x${contract.bytecode}` }
source={ sourcecode } source={ sourcecode }
accounts={ this.props.accounts }
onClose={ this.store.handleCloseDeployModal } onClose={ this.store.handleCloseDeployModal }
readOnly readOnly
/> />