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:
parent
fc18299869
commit
37690cfde2
@ -59,7 +59,8 @@ export default class Api extends EventEmitter {
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
})
|
||||
.catch(() => null);
|
||||
|
||||
transport.addMiddleware(middleware);
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ export default class TokenReg {
|
||||
}
|
||||
|
||||
getInstance () {
|
||||
return this.getContract().instance;
|
||||
return this.getContract().then((contract) => contract.instance);
|
||||
}
|
||||
|
||||
tokenCount () {
|
||||
|
@ -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
|
||||
} }
|
||||
/>
|
||||
);
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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) => {
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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 };
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
});
|
||||
};
|
||||
}
|
@ -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);
|
@ -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';
|
||||
|
@ -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));
|
||||
});
|
||||
}
|
||||
|
88
js/src/redux/providers/tokensActions.js
Normal file
88
js/src/redux/providers/tokensActions.js
Normal 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);
|
||||
});
|
||||
};
|
||||
}
|
34
js/src/redux/providers/tokensReducer.js
Normal file
34
js/src/redux/providers/tokensReducer.js
Normal 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);
|
@ -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
|
||||
});
|
||||
|
@ -84,6 +84,7 @@ export default class AccountCard extends Component {
|
||||
</div>
|
||||
|
||||
<Balance
|
||||
address={ address }
|
||||
balance={ balance }
|
||||
className={ styles.balance }
|
||||
showOnlyEth
|
||||
|
@ -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', () => {
|
||||
|
@ -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);
|
||||
|
@ -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(
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
};
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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', () => {
|
||||
|
@ -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 };
|
||||
|
@ -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
133
js/src/util/tokens.js
Normal 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}`);
|
||||
});
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -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
|
||||
};
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
};
|
||||
}
|
||||
|
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
};
|
||||
|
@ -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
|
||||
};
|
||||
|
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
};
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
/>
|
||||
|
Loading…
Reference in New Issue
Block a user