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;
});
})
.catch(() => null);
transport.addMiddleware(middleware);
}

View File

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

View File

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

View File

@ -17,14 +17,12 @@
import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { AccountCard, Portal, SelectionList } from '~/ui';
@observer
class DappPermissions extends Component {
export default class DappPermissions extends Component {
static propTypes = {
balances: PropTypes.object,
permissionStore: PropTypes.object.isRequired
};
@ -66,27 +64,10 @@ class DappPermissions extends Component {
}
renderAccount = (account) => {
const { balances } = this.props;
const balance = balances[account.address];
return (
<AccountCard
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 React from 'react';
import sinon from 'sinon';
import DappPermissions from './';
let component;
let store;
function createRedux () {
store = {
dispatch: sinon.stub(),
subscribe: sinon.stub(),
getState: () => {
return {
balances: {
balances: {}
}
};
}
};
return store;
}
function renderShallow (permissionStore = {}) {
component = shallow(
<DappPermissions permissionStore={ permissionStore } />,
{
context: {
store: createRedux()
}
}
).find('DappPermissions').shallow();
<DappPermissions permissionStore={ permissionStore } />
);
return component;
}

View File

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

View File

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

View File

@ -34,7 +34,6 @@ export default class DetailsStep extends Component {
accounts: PropTypes.object.isRequired,
amount: PropTypes.string,
amountError: PropTypes.string,
balances: PropTypes.object,
contract: PropTypes.object.isRequired,
fromAddress: PropTypes.string,
fromAddressError: PropTypes.string,
@ -51,13 +50,12 @@ export default class DetailsStep extends Component {
}
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 (
<Form>
<AddressSelect
accounts={ accounts }
balances={ balances }
error={ fromAddressError }
hint={
<FormattedMessage

View File

@ -14,7 +14,6 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { pick } from 'lodash';
import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
@ -58,7 +57,6 @@ class ExecuteContract extends Component {
static propTypes = {
accounts: PropTypes.object,
balances: PropTypes.object,
contract: PropTypes.object.isRequired,
fromAddress: PropTypes.string,
gasLimit: PropTypes.object.isRequired,
@ -199,14 +197,16 @@ class ExecuteContract extends Component {
}
renderStep () {
const { onFromAddressChange } = this.props;
const { accounts, contract, fromAddress, onFromAddressChange } = this.props;
const { step } = this.state;
if (step === STEP_DETAILS) {
return (
<DetailsStep
{ ...this.props }
{ ...this.state }
accounts={ accounts }
contract={ contract }
fromAddress={ fromAddress }
onAmountChange={ this.onAmountChange }
onFromAddressChange={ onFromAddressChange }
onFuncChange={ this.onFuncChange }
@ -334,15 +334,10 @@ class ExecuteContract extends Component {
}
}
function mapStateToProps (initState, initProps) {
const fromAddresses = Object.keys(initProps.accounts);
function mapStateToProps (state) {
const { gasLimit } = state.nodeStatus;
return (state) => {
const balances = pick(state.balances.balances, fromAddresses);
const { gasLimit } = state.nodeStatus;
return { gasLimit, balances };
};
return { gasLimit };
}
export default connect(

View File

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

View File

@ -16,6 +16,7 @@
import BigNumber from 'bignumber.js';
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { MenuItem } from 'material-ui';
import { isEqual } from 'lodash';
@ -24,7 +25,7 @@ import TokenImage from '~/ui/TokenImage';
import styles from '../transfer.css';
export default class TokenSelect extends Component {
class TokenSelect extends Component {
static contextTypes = {
api: PropTypes.object
};
@ -32,7 +33,8 @@ export default class TokenSelect extends Component {
static propTypes = {
onChange: PropTypes.func.isRequired,
balance: PropTypes.object.isRequired,
tag: PropTypes.string.isRequired
tokens: PropTypes.object.isRequired,
value: PropTypes.string.isRequired
};
componentWillMount () {
@ -40,8 +42,10 @@ export default class TokenSelect extends Component {
}
componentWillReceiveProps (nextProps) {
const prevTokens = this.props.balance.tokens.map((t) => `${t.token.tag}_${t.value.toNumber()}`);
const nextTokens = nextProps.balance.tokens.map((t) => `${t.token.tag}_${t.value.toNumber()}`);
const prevTokens = Object.keys(this.props.balance)
.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)) {
this.computeTokens(nextProps);
@ -50,23 +54,27 @@ export default class TokenSelect extends Component {
computeTokens (props = this.props) {
const { api } = this.context;
const { balance } = this.props;
const { balance, tokens } = this.props;
const items = balance.tokens
.filter((token, index) => !index || token.value.gt(0))
.map((balance, index) => {
const token = balance.token;
const isEth = index === 0;
const items = Object.keys(balance)
.map((tokenId) => {
const token = tokens[tokenId];
const tokenValue = balance[tokenId];
const isEth = token.native;
if (!isEth && tokenValue.eq(0)) {
return null;
}
let value = 0;
if (isEth) {
value = api.util.fromWei(balance.value).toFormat(3);
value = api.util.fromWei(tokenValue).toFormat(3);
} else {
const format = balance.token.format || 1;
const format = token.format || 1;
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 = (
@ -83,20 +91,21 @@ export default class TokenSelect extends Component {
return (
<MenuItem
key={ `${index}_${token.tag}` }
value={ token.tag }
key={ tokenId }
value={ token.id }
label={ label }
>
{ label }
</MenuItem>
);
});
})
.filter((node) => node);
this.setState({ items });
}
render () {
const { tag, onChange } = this.props;
const { onChange, value } = this.props;
const { items } = this.state;
return (
@ -104,7 +113,7 @@ export default class TokenSelect extends Component {
className={ styles.tokenSelect }
label='type of token transfer'
hint='type of token to transfer'
value={ tag }
value={ value }
onChange={ onChange }
>
{ 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 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 Contract from '~/api/contract';
import ERRORS from './errors';
import { DEFAULT_GAS, DEFAULT_GASPRICE, MAX_GAS_ESTIMATION } from '~/util/constants';
import { ETH_TOKEN } from '~/util/tokens';
import GasPriceStore from '~/ui/GasPriceEditor/store';
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 {
@observable stage = 0;
@observable extras = false;
@observable isEth = true;
@observable valueAll = false;
@observable sending = false;
@observable tag = 'ETH';
@observable isEth = true;
@observable token = ETH_TOKEN;
@observable data = '';
@observable dataError = null;
@ -69,6 +70,8 @@ export default class TransferStore {
onClose = noop;
senders = null;
isWallet = false;
tokenContract = null;
tokens = {};
wallet = null;
gasStore = null;
@ -76,14 +79,16 @@ export default class TransferStore {
constructor (api, props) {
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.balance = balance;
this.isWallet = account && account.wallet;
this.newError = newError;
this.tokens = tokens;
this.gasStore = new GasPriceStore(api, { gasLimit });
this.tokenContract = api.newContract(tokenAbi, '');
if (this.isWallet) {
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 = () => {
this.stage += 1;
}
@ -166,8 +167,8 @@ export default class TransferStore {
case 'sender':
return this._onUpdateSender(value);
case 'tag':
return this._onUpdateTag(value);
case 'token':
return this._onUpdateToken(value);
case 'value':
return this._onUpdateValue(value);
@ -244,10 +245,10 @@ export default class TransferStore {
});
}
@action _onUpdateTag = (tag) => {
@action _onUpdateToken = (tokenId) => {
transaction(() => {
this.tag = tag;
this.isEth = tag.toLowerCase().trim() === 'eth';
this.token = { ...this.tokens[tokenId] };
this.isEth = this.token.native;
this.recalculateGas();
});
@ -323,46 +324,15 @@ export default class TransferStore {
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
* (in WEI for ETH, without formating for other tokens)
*/
getTokenBalance (tag = this.tag, forceSender = false) {
const token = this.getToken(tag, forceSender);
if (!token) {
return new BigNumber(0);
}
const value = new BigNumber(token.value || 0);
return value;
getTokenBalance (token = this.token, forceSender = false) {
return new BigNumber(this.balance[token.id] || 0);
}
getTokenValue (tag = this.tag, 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);
getTokenValue (token = this.token, value = this.value, inverse = false) {
let _value;
try {
@ -371,19 +341,11 @@ export default class TransferStore {
_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) {
return _value.div(format);
return _value.div(token.format);
}
return _value.mul(format);
return _value.mul(token.format);
}
getValues (_gasTotal) {
@ -425,7 +387,7 @@ export default class TransferStore {
}
// Otherwise, substract the gas estimate
const availableEth = this.getTokenBalance('ETH');
const availableEth = this.getTokenBalance(ETH_TOKEN);
const totalEthValue = availableEth.gt(gasTotal)
? availableEth.minus(gasTotal)
: new BigNumber(0);
@ -437,15 +399,7 @@ export default class TransferStore {
}
getFormattedTokenValue (tokenValue) {
const token = this.getToken();
if (!token) {
return new BigNumber(0);
}
const tag = token.token && token.token.tag || '';
return this.getTokenValue(tag, tokenValue, true);
return this.getTokenValue(this.token, tokenValue, true);
}
@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 ethBalance = this.getTokenBalance('ETH', true);
const ethBalance = this.getTokenBalance(ETH_TOKEN, true);
const tokenBalance = this.getTokenBalance();
const { eth, token } = this.getValues(gasTotal);
@ -545,7 +499,7 @@ export default class TransferStore {
return this.wallet.instance.execute;
}
return this.token.contract.instance.transfer;
return this.tokenContract.at(this.token.address).instance.transfer;
}
_getData (gas = false) {
@ -558,7 +512,7 @@ export default class TransferStore {
const func = this._getTransferMethod(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) {
@ -587,7 +541,7 @@ export default class TransferStore {
}
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 values = [
@ -621,14 +575,7 @@ export default class TransferStore {
}
_validateDecimals (num) {
const { balance } = this;
if (this.tag === 'ETH') {
return null;
}
const token = balance.tokens.find((balance) => balance.token.tag === this.tag).token;
const s = new BigNumber(num).mul(token.format || 1).toFixed();
const s = new BigNumber(num).mul(this.token.format || 1).toFixed();
if (s.indexOf('.') !== -1) {
return ERRORS.invalidDecimals;

View File

@ -45,12 +45,13 @@ class Transfer extends Component {
newError: PropTypes.func.isRequired,
gasLimit: PropTypes.object.isRequired,
senders: nullableProptype(PropTypes.object),
sendersBalances: nullableProptype(PropTypes.object),
account: 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);
@ -144,8 +145,8 @@ class Transfer extends Component {
renderDetailsPage () {
const { account, balance, senders } = this.props;
const { recipient, recipientError, sender, senderError, sendersBalances } = this.store;
const { valueAll, extras, tag, total, totalError, value, valueError } = this.store;
const { recipient, recipientError, sender, senderError } = this.store;
const { valueAll, extras, token, total, totalError, value, valueError } = this.store;
return (
<Details
@ -159,8 +160,7 @@ class Transfer extends Component {
sender={ sender }
senderError={ senderError }
senders={ senders }
sendersBalances={ sendersBalances }
tag={ tag }
token={ token }
total={ total }
totalError={ totalError }
value={ value }
@ -272,6 +272,7 @@ class Transfer extends Component {
}
function mapStateToProps (initState, initProps) {
const { tokens } = initState;
const { address } = initProps.account;
const isWallet = initProps.account && initProps.account.wallet;
@ -291,9 +292,12 @@ function mapStateToProps (initState, initProps) {
return (state) => {
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 = {
accounts: PropTypes.object.isRequired,
balances: PropTypes.object.isRequired,
newError: PropTypes.func.isRequired,
personalAccountsInfo: PropTypes.func.isRequired,
vaultStore: PropTypes.object.isRequired
@ -108,13 +107,9 @@ class VaultAccounts extends Component {
}
renderAccount = (account) => {
const { balances } = this.props;
const balance = balances[account.address];
return (
<AccountCard
account={ account }
balance={ balance }
/>
);
}
@ -171,12 +166,10 @@ class VaultAccounts extends Component {
}
function mapStateToProps (state) {
const { balances } = state.balances;
const { accounts } = state.personal;
return {
accounts,
balances
accounts
};
}

View File

@ -16,7 +16,8 @@
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 Contracts from '~/contracts';
@ -193,6 +194,7 @@ export default class Balances {
return console.warn('_subscribeBlockNumber', error);
}
this._store.dispatch(queryTokensFilter());
return this.fetchAllBalances();
})
.then((blockNumberSID) => {
@ -276,7 +278,6 @@ export default class Balances {
.then((tokenreg) => {
this._tokenreg = tokenreg;
this._store.dispatch(setTokenReg(tokenreg));
this._store.dispatch(loadTokens(options));
return this.attachToTokens(tokenreg);

View File

@ -14,119 +14,19 @@
// 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, uniq, isEqual } from 'lodash';
import BigNumber from 'bignumber.js';
import { uniq, isEqual } from 'lodash';
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 { ETH_TOKEN, fetchAccountsBalances } from '~/util/tokens';
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 ETH = {
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));
};
}
let tokensFilter = {};
function _setBalances (balances) {
return {
@ -135,129 +35,88 @@ function _setBalances (balances) {
};
}
export function setTokens (tokens) {
return {
type: 'setTokens',
tokens
};
}
export function setTokenReg (tokenreg) {
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 : '');
/**
* @param {Object} _balances - In the shape:
* {
* [ who ]: { [ tokenId ]: BigNumber } // The balances of `who`
* }
* @param {Boolean} skipNotifications [description]
*/
function setBalances (updates, skipNotifications = false) {
return (dispatch, getState) => {
const { tokenreg } = getState().balances;
const { tokens, balances } = getState();
return tokenreg.instance.tokenCount
.call()
.then((numTokens) => {
const tokenIds = range(numTokens.toNumber());
const prevBalances = balances;
const nextBalances = { ...prevBalances };
dispatch(fetchTokens(tokenIds, options));
})
.catch((error) => {
console.warn('balances::loadTokens', error);
});
};
}
Object.keys(updates)
.forEach((who) => {
const accountUpdates = updates[who];
export function fetchTokens (_tokenIds, options = {}) {
const tokenIds = uniq(_tokenIds || []);
Object.keys(accountUpdates)
.forEach((tokenId) => {
const token = tokens[tokenId];
const prevTokenValue = (prevBalances[who] || {})[tokenId];
const nextTokenValue = accountUpdates[tokenId];
return (dispatch, getState) => {
const { api, images, balances } = getState();
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;
if (prevTokenValue && prevTokenValue.lt(nextTokenValue)) {
dispatch(notifyBalanceChange(who, prevTokenValue, nextTokenValue, token));
}
dispatch(setTokenImage(address, image));
dispatch(setAddressImage(address, image, true));
// Add the token if it's native ETH or if it has a value
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) => {
const { api, personal } = getState();
const { visibleAccounts, accounts } = personal;
const account = getState().personal.accounts[who];
const addresses = uniq(_addresses || visibleAccounts || []);
if (account) {
const txValue = toValue.minus(fromValue);
// With only a single account, more info will be displayed.
const fullFetch = addresses.length === 1;
const redirectToAccount = () => {
const basePath = account.wallet
? 'wallet'
: 'accounts';
// Add accounts addresses (for notifications, accounts selection, etc.)
const addressesToFetch = uniq(addresses.concat(Object.keys(accounts)));
const route = `/${basePath}/${account.address}`;
return Promise
.all(addressesToFetch.map((addr) => fetchAccount(addr, api, fullFetch)))
.then((accountsBalances) => {
const balances = {};
dispatch(push(route));
};
addressesToFetch.forEach((addr, idx) => {
balances[addr] = accountsBalances[idx];
});
dispatch(setBalances(balances, skipNotifications));
})
.catch((error) => {
console.warn('balances::fetchBalances', error);
});
notifyTransaction(account, token, txValue, redirectToAccount);
}
};
}
// TODO: fetch txCount when needed
export function fetchBalances (_addresses, skipNotifications = false) {
return fetchTokensBalances(_addresses, [ ETH_TOKEN ], skipNotifications);
}
export function updateTokensFilter (_addresses, _tokens, options = {}) {
return (dispatch, getState) => {
const { api, balances, personal } = getState();
const { api, personal, tokens } = getState();
const { visibleAccounts, accounts } = personal;
const { tokensFilter } = balances;
const addressesToFetch = uniq(visibleAccounts.concat(Object.keys(accounts)));
const addresses = uniq(_addresses || addressesToFetch || []).sort();
const tokens = _tokens || Object.values(balances.tokens) || [];
const tokenAddresses = tokens.map((t) => t.address).sort();
const tokensToUpdate = _tokens || Object.values(tokens);
const tokenAddresses = tokensToUpdate
.map((t) => t.address)
.filter((address) => address)
.sort();
if (tokensFilter.filterFromId || tokensFilter.filterToId) {
// 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 TRANSFER_SIGNATURE = api.util.sha3('Transfer(address,address,uint256)');
const topicsFrom = [ TRANSFER_SIGNATURE, addresses, null ];
const topicsTo = [ TRANSFER_SIGNATURE, null, addresses ];
const options = {
const filterOptions = {
fromBlock: 0,
toBlock: 'pending',
address: tokenAddresses
};
const optionsFrom = {
...options,
...filterOptions,
topics: topicsFrom
};
const optionsTo = {
...options,
...filterOptions,
topics: topicsTo
};
@ -322,8 +180,8 @@ export function updateTokensFilter (_addresses, _tokens, options = {}) {
const { skipNotifications } = options;
dispatch(setTokensFilter(nextTokensFilter));
fetchTokensBalances(addresses, tokens, skipNotifications)(dispatch, getState);
tokensFilter = nextTokensFilter;
fetchTokensBalances(addresses, tokensToUpdate, skipNotifications)(dispatch, getState);
})
.catch((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) => {
const { api, personal, balances } = getState();
const { api, personal, tokens } = getState();
const { visibleAccounts, accounts } = personal;
const visibleAddresses = visibleAccounts.map((a) => a.toLowerCase());
const addressesToFetch = uniq(visibleAddresses.concat(Object.keys(accounts)));
const allAddresses = visibleAccounts.concat(Object.keys(accounts));
const addressesToFetch = uniq(allAddresses);
const lcAddresses = addressesToFetch.map((a) => a.toLowerCase());
Promise
.all([
@ -347,21 +206,28 @@ export function queryTokensFilter (tokensFilter) {
.then(([ logsFrom, logsTo ]) => {
const addresses = [];
const tokenAddresses = [];
const logs = logsFrom.concat(logsTo);
logsFrom
.concat(logsTo)
if (logs.length > 0) {
log.debug('got tokens filter logs', logs);
}
logs
.forEach((log) => {
const tokenAddress = log.address;
const fromAddress = '0x' + log.topics[1].slice(-40);
const toAddress = '0x' + log.topics[2].slice(-40);
if (addressesToFetch.includes(fromAddress)) {
addresses.push(fromAddress);
const fromAddressIndex = lcAddresses.indexOf(fromAddress);
const toAddressIndex = lcAddresses.indexOf(toAddress);
if (fromAddressIndex > -1) {
addresses.push(addressesToFetch[fromAddressIndex]);
}
if (addressesToFetch.includes(toAddress)) {
addresses.push(toAddress);
if (toAddressIndex > -1) {
addresses.push(addressesToFetch[toAddressIndex]);
}
tokenAddresses.push(tokenAddress);
@ -371,36 +237,35 @@ export function queryTokensFilter (tokensFilter) {
return;
}
const tokens = Object.values(balances.tokens)
const tokensToUpdate = Object.values(tokens)
.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) {
return (dispatch, getState) => {
const { api, personal, balances } = getState();
const { api, personal, tokens } = getState();
const { visibleAccounts, accounts } = personal;
const allTokens = Object.values(tokens);
const addressesToFetch = uniq(visibleAccounts.concat(Object.keys(accounts)));
const addresses = _addresses || addressesToFetch;
const tokens = _tokens || Object.values(balances.tokens);
const tokensToUpdate = _tokens || allTokens;
if (addresses.length === 0) {
return Promise.resolve();
}
return Promise
.all(addresses.map((addr) => fetchTokensBalance(addr, tokens, api)))
.then((tokensBalances) => {
const balances = {};
addresses.forEach((addr, idx) => {
balances[addr] = tokensBalances[idx];
});
const updates = addresses.reduce((updates, who) => {
updates[who] = tokensToUpdate.map((token) => token.id);
return updates;
}, {});
return fetchAccountsBalances(api, allTokens, updates)
.then((balances) => {
dispatch(setBalances(balances, skipNotifications));
})
.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';
const initialState = {
balances: {},
tokens: {},
tokenreg: null,
tokensFilter: {}
};
const initialState = {};
export default handleActions({
setBalances (state, action) {
const { balances } = action;
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 });
return Object.assign({}, state, balances);
}
}, 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 balancesReducer from './balancesReducer';
export blockchainReducer from './blockchainReducer';
export workerReducer from './workerReducer';
export imagesReducer from './imagesReducer';
export personalReducer from './personalReducer';
@ -29,4 +28,5 @@ export requestsReducer from './requestsReducer';
export signerReducer from './signerReducer';
export snackbarReducer from './snackbarReducer';
export statusReducer from './statusReducer';
export tokensReducer from './tokensReducer';
export walletReducer from './walletReducer';

View File

@ -35,6 +35,12 @@ export default class Personal {
return;
}
// Add the address to each accounts
Object.keys(accountsInfo)
.forEach((address) => {
accountsInfo[address].address = address;
});
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 {
apiReducer, balancesReducer, blockchainReducer,
apiReducer, balancesReducer,
workerReducer, imagesReducer, personalReducer, requestsReducer,
signerReducer, statusReducer as nodeStatusReducer,
snackbarReducer, walletReducer
snackbarReducer, tokensReducer, walletReducer
} from './providers';
import certificationsReducer from './providers/certifications/reducer';
import registryReducer from './providers/registry/reducer';
@ -40,7 +40,6 @@ export default function () {
balances: balancesReducer,
certifications: certificationsReducer,
blockchain: blockchainReducer,
images: imagesReducer,
nodeStatus: nodeStatusReducer,
personal: personalReducer,
@ -48,6 +47,7 @@ export default function () {
requests: requestsReducer,
signer: signerReducer,
snackbar: snackbarReducer,
tokens: tokensReducer,
wallet: walletReducer,
worker: workerReducer
});

View File

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

View File

@ -14,10 +14,13 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import BigNumber from 'bignumber.js';
import { shallow } from 'enzyme';
import React from 'react';
import sinon from 'sinon';
import { ETH_TOKEN } from '~/util/tokens';
import AccountCard from './';
const TEST_ADDRESS = '0x1234567890123456789012345678901234567890';
@ -27,6 +30,21 @@ let component;
let onClick;
let onFocus;
function reduxStore () {
const getState = () => ({
balances: {},
tokens: {
[ETH_TOKEN.id]: ETH_TOKEN
}
});
return {
getState,
dispatch: () => null,
subscribe: () => null
};
}
function render (props = {}) {
if (!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();
onFocus = sinon.stub();
@ -67,7 +91,9 @@ describe('ui/AccountCard', () => {
let balance;
beforeEach(() => {
balance = component.find('Balance');
balance = component.find('Connect(Balance)').shallow({
context: { store: reduxStore() }
});
});
it('renders the balance', () => {

View File

@ -17,18 +17,21 @@
import BigNumber from 'bignumber.js';
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import TokenImage from '~/ui/TokenImage';
import styles from './balance.css';
export default class Balance extends Component {
export class Balance extends Component {
static contextTypes = {
api: PropTypes.object
};
static propTypes = {
balance: PropTypes.object,
balance: PropTypes.object.isRequired,
tokens: PropTypes.object.isRequired,
address: PropTypes.string,
className: PropTypes.string,
showOnlyEth: PropTypes.bool,
showZeroValues: PropTypes.bool
@ -40,44 +43,38 @@ export default class Balance extends Component {
};
render () {
const { api } = this.context;
const { balance, className, showOnlyEth } = this.props;
const { balance, className, showOnlyEth, tokens } = this.props;
if (!balance || !balance.tokens) {
if (Object.keys(balance).length === 0) {
return null;
}
let body = balance.tokens
.filter((balance) => {
const isEthToken = (balance.token.tag || '').toLowerCase() === 'eth';
const hasBalance = new BigNumber(balance.value).gt(0);
let body = Object.keys(balance)
.map((tokenId) => {
const token = tokens[tokenId];
const balanceValue = balance[tokenId];
return hasBalance || isEthToken;
})
.map((balance, index) => {
const isFullToken = !showOnlyEth || (balance.token.tag || '').toLowerCase() === 'eth';
const token = balance.token;
const isEthToken = token.native;
const isFullToken = !showOnlyEth || isEthToken;
const hasBalance = balanceValue.gt(0);
let value;
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);
if (!hasBalance && !isEthToken) {
return null;
}
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];
let details = null;
@ -104,13 +101,14 @@ export default class Balance extends Component {
return (
<div
className={ classNames.join(' ') }
key={ `${index}_${token.tag}` }
key={ tokenId }
>
<TokenImage token={ token } />
{ details }
</div>
);
});
})
.filter((node) => node);
if (!body.length) {
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
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import BigNumber from 'bignumber.js';
import { shallow } from 'enzyme';
import React from 'react';
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 = {
tokens: [
{ value: '122', token: { tag: 'ETH' } },
{ value: '345', token: { tag: 'GAV', format: 1 } },
{ value: '0', token: { tag: 'TST', format: 1 } }
]
'eth': new BigNumber(122),
'gav': new BigNumber(345),
'tst': new BigNumber(0)
};
let api;
@ -46,6 +51,10 @@ function render (props = {}) {
props.balance = BALANCE;
}
if (!props.tokens) {
props.tokens = TOKENS;
}
const api = createApi();
component = shallow(

View File

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

View File

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

View File

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

View File

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

View File

@ -17,40 +17,19 @@
import { shallow } from 'enzyme';
import React from 'react';
import sinon from 'sinon';
import IdentityName from './identityName';
import { IdentityName } from './identityName';
const ADDR_A = '0x123456789abcdef0123456789A';
const ADDR_B = '0x123456789abcdef0123456789B';
const ADDR_C = '0x123456789abcdef0123456789C';
const ADDR_NULL = '0x0000000000000000000000000000000000000000';
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) {
return shallow(
<IdentityName
store={ STORE }
{ ...props }
/>
).find('IdentityName').shallow();
);
}
describe('ui/IdentityName', () => {

View File

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

View File

@ -17,20 +17,12 @@
import Push from 'push.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';
export function notifyTransaction (account, token, _value, onClick) {
const name = account.name || account.address;
const value = token.tag.toLowerCase() === 'eth'
? fromWei(_value)
: _value.div(new BigNumber(token.format || 1));
const icon = token.tag.toLowerCase() === 'eth'
? ethereumIcon
: (token.image || unkownIcon);
const value = _value.div(new BigNumber(token.format || 1));
const icon = token.image || unkownIcon;
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';
export default class Header extends Component {
static contextTypes = {
api: PropTypes.object.isRequired
};
static propTypes = {
account: PropTypes.object,
balance: PropTypes.object,
children: PropTypes.node,
className: PropTypes.string,
disabled: PropTypes.bool,
@ -39,8 +42,49 @@ export default class Header extends Component {
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 () {
const { account, balance, children, className, disabled, hideName } = this.props;
const { account, children, className, disabled, hideName } = this.props;
if (!account) {
return null;
@ -76,8 +120,7 @@ export default class Header extends Component {
{ this.renderTxCount() }
<div className={ styles.balances }>
<Balance
account={ account }
balance={ balance }
address={ address }
/>
<Certifications address={ address } />
{ this.renderVault() }
@ -115,15 +158,10 @@ export default class Header extends Component {
}
renderTxCount () {
const { balance, isContract } = this.props;
const { isContract } = this.props;
const { txCount } = this.state;
if (!balance || isContract) {
return null;
}
const { txCount } = balance;
if (!txCount) {
if (!txCount || isContract) {
return null;
}

View File

@ -18,6 +18,8 @@ import BigNumber from 'bignumber.js';
import { shallow } from 'enzyme';
import React from 'react';
import { ETH_TOKEN } from '~/util/tokens';
import Header from './';
const ACCOUNT = {
@ -28,17 +30,44 @@ const ACCOUNT = {
},
uuid: '0xabcdef'
};
const subscriptions = {};
let component;
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 = {}) {
if (props && !props.account) {
props.account = ACCOUNT;
}
component = shallow(
<Header { ...props } />
<Header { ...props } />,
{ context: { api } }
);
instance = component.instance();
@ -72,8 +101,9 @@ describe('views/Account/Header', () => {
let balance;
beforeEach(() => {
render({ balance: { balance: 'testing' } });
balance = component.find('Balance');
render();
balance = component.find('Connect(Balance)')
.shallow({ context: { store: reduxStore() } });
});
it('renders', () => {
@ -81,11 +111,7 @@ describe('views/Account/Header', () => {
});
it('passes the account', () => {
expect(balance.props().account).to.deep.equal(ACCOUNT);
});
it('passes the balance', () => {
expect(balance.props().address).to.deep.equal(ACCOUNT.address);
});
});
@ -177,24 +203,33 @@ describe('views/Account/Header', () => {
});
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', () => {
render({ balance: { txCount: null }, isContract: false });
render();
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', () => {
render({ balance: { txCount: new BigNumber(1) }, isContract: false });
expect(instance.renderTxCount()).not.to.be.null;
render();
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,
setVisibleAccounts: PropTypes.func.isRequired,
accounts: PropTypes.object,
balances: PropTypes.object,
account: PropTypes.object,
certifications: PropTypes.object,
netVersion: PropTypes.string.isRequired,
params: PropTypes.object
@ -83,12 +82,9 @@ class Account extends Component {
}
render () {
const { accounts, balances } = this.props;
const { account } = this.props;
const { address } = this.props.params;
const account = (accounts || {})[address];
const balance = (balances || {})[address];
if (!account) {
return null;
}
@ -102,17 +98,15 @@ class Account extends Component {
{ this.renderFaucetDialog() }
{ this.renderFundDialog() }
{ this.renderPasswordDialog(account) }
{ this.renderTransferDialog(account, balance) }
{ this.renderTransferDialog(account) }
{ this.renderVerificationDialog() }
{ this.renderActionbar(account, balance) }
{ this.renderActionbar(account) }
<Page padded>
<Header
account={ account }
balance={ balance }
disabled={ !isAvailable }
/>
<Transactions
accounts={ accounts }
address={ address }
/>
</Page>
@ -143,16 +137,14 @@ class Account extends Component {
return certifications.length !== 0;
}
renderActionbar (account, balance) {
renderActionbar (account) {
const { certifications, netVersion } = this.props;
const { address } = this.props.params;
const showTransferButton = !!(balance && balance.tokens);
const isVerifiable = this.isMainnet(netVersion);
const isFaucettable = this.isFaucettable(netVersion, certifications, address);
const buttons = [
<Button
disabled={ !showTransferButton }
icon={ <SendIcon /> }
key='transferFunds'
label={
@ -374,18 +366,14 @@ class Account extends Component {
);
}
renderTransferDialog (account, balance) {
renderTransferDialog (account) {
if (!this.store.isTransferVisible) {
return null;
}
const { balances } = this.props;
return (
<Transfer
account={ account }
balance={ balance }
balances={ balances }
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 { balances } = state.balances;
const certifications = state.certifications;
const { netVersion } = state.nodeStatus;
const account = (accounts || {})[address];
return {
accounts,
balances,
account,
certifications,
netVersion
};

View File

@ -14,21 +14,23 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { pick } from 'lodash';
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { Container, SectionList } from '~/ui';
import { fetchCertifiers, fetchCertifications } from '~/redux/providers/certifications/actions';
import { ETH_TOKEN } from '~/util/tokens';
import Summary from '../Summary';
import styles from './list.css';
class List extends Component {
static propTypes = {
accounts: PropTypes.object,
balances: PropTypes.object,
balances: PropTypes.object.isRequired,
certifications: PropTypes.object.isRequired,
accounts: PropTypes.object,
disabled: PropTypes.object,
empty: PropTypes.bool,
link: PropTypes.string,
@ -51,7 +53,7 @@ class List extends Component {
}
render () {
const { accounts, balances, disabled, empty } = this.props;
const { accounts, disabled, empty } = this.props;
if (empty) {
return (
@ -67,13 +69,11 @@ class List extends Component {
.getAddresses()
.map((address) => {
const account = accounts[address] || {};
const balance = balances[address] || {};
const isDisabled = disabled ? disabled[address] : false;
const owners = account.owners || null;
return {
account,
balance,
isDisabled,
owners
};
@ -88,13 +88,12 @@ class List extends Component {
}
renderSummary = (item) => {
const { account, balance, isDisabled, owners } = item;
const { account, isDisabled, owners } = item;
const { handleAddSearchToken, link } = this.props;
return (
<Summary
account={ account }
balance={ balance }
disabled={ isDisabled }
handleAddSearchToken={ handleAddSearchToken }
link={ link }
@ -160,8 +159,8 @@ class List extends Component {
return 1;
}
const ethA = balanceA.tokens.find(token => token.token.tag.toLowerCase() === 'eth');
const ethB = balanceB.tokens.find(token => token.token.tag.toLowerCase() === 'eth');
const ethA = balanceA[ETH_TOKEN.id];
const ethB = balanceB[ETH_TOKEN.id];
if (!ethA && !ethB) {
return 0;
@ -171,7 +170,7 @@ class List extends Component {
return 1;
}
return -1 * ethA.value.comparedTo(ethB.value);
return -1 * ethA.comparedTo(ethB);
}
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;
return { certifications };
return { balances, certifications };
}
function mapDispatchToProps (dispatch) {

View File

@ -36,7 +36,6 @@ class Summary extends Component {
static propTypes = {
account: PropTypes.object.isRequired,
accountsInfo: PropTypes.object.isRequired,
balance: PropTypes.object,
disabled: PropTypes.bool,
link: PropTypes.string,
name: PropTypes.string,
@ -74,20 +73,6 @@ class Summary extends Component {
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 nextOwners = nextProps.owners;
@ -249,15 +234,11 @@ class Summary extends Component {
}
renderBalance (onlyEth) {
const { balance } = this.props;
if (!balance) {
return null;
}
const { account } = this.props;
return (
<Balance
balance={ balance }
address={ account.address }
className={
onlyEth
? styles.ethBalances

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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