diff --git a/js/src/api/format/input.js b/js/src/api/format/input.js
index 55c85e4f3c1..80f3bc0eb48 100644
--- a/js/src/api/format/input.js
+++ b/js/src/api/format/input.js
@@ -15,9 +15,9 @@
// along with Parity. If not, see .
import BigNumber from 'bignumber.js';
-import { range } from 'lodash';
import { isArray, isHex, isInstanceOf, isString } from '../util/types';
+import { padLeft } from '../util/format';
export function inAddress (address) {
// TODO: address validation if we have upper-lower addresses
@@ -51,19 +51,20 @@ export function inHash (hash) {
return inHex(hash);
}
-export function pad (input, length) {
- const value = inHex(input).substr(2, length * 2);
- return '0x' + value + range(length * 2 - value.length).map(() => '0').join('');
-}
-
export function inTopics (_topics) {
let topics = (_topics || [])
.filter((topic) => topic === null || topic)
- .map((topic) => topic === null ? null : pad(topic, 32));
+ .map((topic) => {
+ if (topic === null) {
+ return null;
+ }
- // while (topics.length < 4) {
- // topics.push(null);
- // }
+ if (Array.isArray(topic)) {
+ return inTopics(topic);
+ }
+
+ return padLeft(topic, 32);
+ });
return topics;
}
diff --git a/js/src/api/transport/error.js b/js/src/api/transport/error.js
index 341839f69ad..6cb0dac1705 100644
--- a/js/src/api/transport/error.js
+++ b/js/src/api/transport/error.js
@@ -36,7 +36,8 @@ export const ERROR_CODES = {
REQUEST_NOT_FOUND: -32042,
COMPILATION_ERROR: -32050,
ENCRYPTION_ERROR: -32055,
- FETCH_ERROR: -32060
+ FETCH_ERROR: -32060,
+ INVALID_PARAMS: -32602
};
export default class TransportError extends ExtendableError {
diff --git a/js/src/api/transport/ws/ws.js b/js/src/api/transport/ws/ws.js
index c30c910e6ad..53600b6d3a7 100644
--- a/js/src/api/transport/ws/ws.js
+++ b/js/src/api/transport/ws/ws.js
@@ -79,7 +79,7 @@ export default class Ws extends JsonRpcBase {
this._ws.onclose = this._onClose;
this._ws.onmessage = this._onMessage;
- // Get counts in dev mode
+ // Get counts in dev mode only
if (process.env.NODE_ENV === 'development') {
this._count = 0;
this._lastCount = {
@@ -93,8 +93,13 @@ export default class Ws extends JsonRpcBase {
const s = Math.round(1000 * n / t) / 1000;
if (this._debug) {
- console.log('::parityWS', `speed: ${s} req/s`, `count: ${this._count}`);
+ console.log('::parityWS', `speed: ${s} req/s`, `count: ${this._count}`, `(+${n})`);
}
+
+ this._lastCount = {
+ timestamp: Date.now(),
+ count: this._count
+ };
}, 5000);
window._parityWS = this;
@@ -117,6 +122,7 @@ export default class Ws extends JsonRpcBase {
this._connected = false;
this._connecting = false;
+ event.timestamp = Date.now();
this._lastError = event;
if (this._autoConnect) {
@@ -144,6 +150,8 @@ export default class Ws extends JsonRpcBase {
window.setTimeout(() => {
if (this._connected) {
console.error('ws:onError', event);
+
+ event.timestamp = Date.now();
this._lastError = event;
}
}, 50);
diff --git a/js/src/api/util/format.js b/js/src/api/util/format.js
index 93f31a16191..f1909748d7d 100644
--- a/js/src/api/util/format.js
+++ b/js/src/api/util/format.js
@@ -14,6 +14,9 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see .
+import { range } from 'lodash';
+import { inHex } from '../format/input';
+
export function bytesToHex (bytes) {
return '0x' + bytes.map((b) => ('0' + b.toString(16)).slice(-2)).join('');
}
@@ -33,3 +36,13 @@ export function hex2Ascii (_hex) {
export function asciiToHex (string) {
return '0x' + string.split('').map((s) => s.charCodeAt(0).toString(16)).join('');
}
+
+export function padRight (input, length) {
+ const value = inHex(input).substr(2, length * 2);
+ return '0x' + value + range(length * 2 - value.length).map(() => '0').join('');
+}
+
+export function padLeft (input, length) {
+ const value = inHex(input).substr(2, length * 2);
+ return '0x' + range(length * 2 - value.length).map(() => '0').join('') + value;
+}
diff --git a/js/src/dapps/tokenreg/Status/actions.js b/js/src/dapps/tokenreg/Status/actions.js
index b07949a2830..027de57af6a 100644
--- a/js/src/dapps/tokenreg/Status/actions.js
+++ b/js/src/dapps/tokenreg/Status/actions.js
@@ -127,7 +127,7 @@ export const subscribeEvents = () => (dispatch, getState) => {
const params = log.params;
if (event === 'Registered' && type === 'pending') {
- return dispatch(setTokenData(params.id.toNumber(), {
+ return dispatch(setTokenData(params.id.value.toNumber(), {
tla: '...',
base: -1,
address: params.addr.value,
diff --git a/js/src/redux/providers/balances.js b/js/src/redux/providers/balances.js
index 9e9c0a48131..6b10849344f 100644
--- a/js/src/redux/providers/balances.js
+++ b/js/src/redux/providers/balances.js
@@ -16,68 +16,63 @@
import { throttle } from 'lodash';
-import { getBalances, getTokens } from './balancesActions';
-import { setAddressImage } from './imagesActions';
+import { loadTokens, setTokenReg, fetchBalances, fetchTokens, fetchTokensBalances } from './balancesActions';
+import { padRight } from '../../api/util/format';
import Contracts from '../../contracts';
-import * as abis from '../../contracts/abi';
-
-import imagesEthereum from '../../../assets/images/contracts/ethereum-black-64x64.png';
-
-const ETH = {
- name: 'Ethereum',
- tag: 'ETH',
- image: imagesEthereum
-};
export default class Balances {
constructor (store, api) {
this._api = api;
this._store = store;
- this._tokens = {};
- this._images = {};
-
- this._accountsInfo = null;
- this._tokenreg = null;
- this._fetchingBalances = false;
- this._fetchingTokens = false;
- this._fetchedTokens = false;
-
this._tokenregSubId = null;
this._tokenregMetaSubId = null;
// Throttled `retrieveTokens` function
- // that gets called max once every 20s
- this._throttledRetrieveTokens = throttle(
- this._retrieveTokens,
- 20 * 1000,
+ // that gets called max once every 40s
+ this.longThrottledFetch = throttle(
+ this.fetchBalances,
+ 40 * 1000,
+ { trailing: true }
+ );
+
+ this.shortThrottledFetch = throttle(
+ this.fetchBalances,
+ 2 * 1000,
+ { trailing: true }
+ );
+
+ // Fetch all tokens every 2 minutes
+ this.throttledTokensFetch = throttle(
+ this.fetchTokens,
+ 60 * 1000,
{ trailing: true }
);
}
start () {
- this._subscribeBlockNumber();
- this._subscribeAccountsInfo();
- this._retrieveTokens();
+ this.subscribeBlockNumber();
+ this.subscribeAccountsInfo();
+
+ this.loadTokens();
}
- _subscribeAccountsInfo () {
+ subscribeAccountsInfo () {
this._api
.subscribe('parity_accountsInfo', (error, accountsInfo) => {
if (error) {
return;
}
- this._accountsInfo = accountsInfo;
- this._retrieveTokens();
+ this.fetchBalances();
})
.catch((error) => {
console.warn('_subscribeAccountsInfo', error);
});
}
- _subscribeBlockNumber () {
+ subscribeBlockNumber () {
this._api
.subscribe('eth_blockNumber', (error) => {
if (error) {
@@ -86,123 +81,63 @@ export default class Balances {
const { syncing } = this._store.getState().nodeStatus;
+ this.throttledTokensFetch();
+
// If syncing, only retrieve balances once every
// few seconds
if (syncing) {
- return this._throttledRetrieveTokens();
+ this.shortThrottledFetch();
+ return this.longThrottledFetch();
}
- this._throttledRetrieveTokens.cancel();
- this._retrieveTokens();
+ this.longThrottledFetch.cancel();
+ return this.shortThrottledFetch();
})
.catch((error) => {
console.warn('_subscribeBlockNumber', error);
});
}
- getTokenRegistry () {
- if (this._tokenreg) {
- return Promise.resolve(this._tokenreg);
- }
-
- return Contracts.get().tokenReg
- .getContract()
- .then((tokenreg) => {
- this._tokenreg = tokenreg;
- this.attachToTokens();
-
- return tokenreg;
- });
+ fetchBalances () {
+ this._store.dispatch(fetchBalances());
}
- _retrieveTokens () {
- if (this._fetchingTokens) {
- return;
- }
-
- if (this._fetchedTokens) {
- return this._retrieveBalances();
- }
+ fetchTokens () {
+ this._store.dispatch(fetchTokensBalances());
+ }
- this._fetchingTokens = true;
- this._fetchedTokens = false;
+ getTokenRegistry () {
+ return Contracts.get().tokenReg.getContract();
+ }
+ loadTokens () {
this
.getTokenRegistry()
.then((tokenreg) => {
- return tokenreg.instance.tokenCount
- .call()
- .then((numTokens) => {
- const promises = [];
-
- for (let i = 0; i < numTokens.toNumber(); i++) {
- promises.push(this.fetchTokenInfo(tokenreg, i));
- }
-
- return Promise.all(promises);
- });
- })
- .then(() => {
- this._fetchingTokens = false;
- this._fetchedTokens = true;
-
- this._store.dispatch(getTokens(this._tokens));
- this._retrieveBalances();
- })
- .catch((error) => {
- console.warn('balances::_retrieveTokens', error);
- });
- }
-
- _retrieveBalances () {
- if (this._fetchingBalances) {
- return;
- }
-
- if (!this._accountsInfo) {
- return;
- }
-
- this._fetchingBalances = true;
-
- const addresses = Object
- .keys(this._accountsInfo)
- .filter((address) => {
- const account = this._accountsInfo[address];
- return !account.meta || !account.meta.deleted;
- });
+ this._store.dispatch(setTokenReg(tokenreg));
+ this._store.dispatch(loadTokens());
- this._balances = {};
-
- Promise
- .all(addresses.map((a) => this.fetchAccountBalance(a)))
- .then((balances) => {
- addresses.forEach((a, idx) => {
- this._balances[a] = balances[idx];
- });
-
- this._store.dispatch(getBalances(this._balances));
- this._fetchingBalances = false;
+ return this.attachToTokens(tokenreg);
})
.catch((error) => {
- console.warn('_retrieveBalances', error);
- this._fetchingBalances = false;
+ console.warn('balances::loadTokens', error);
});
}
- attachToTokens () {
- this.attachToTokenMetaChange();
- this.attachToNewToken();
+ attachToTokens (tokenreg) {
+ return Promise
+ .all([
+ this.attachToTokenMetaChange(tokenreg),
+ this.attachToNewToken(tokenreg)
+ ]);
}
- attachToNewToken () {
+ attachToNewToken (tokenreg) {
if (this._tokenregSubId) {
- return;
+ return Promise.resolve();
}
- this._tokenreg
- .instance
- .Registered
+ return tokenreg.instance.Registered
.subscribe({
fromBlock: 0,
toBlock: 'latest',
@@ -212,138 +147,38 @@ export default class Balances {
return console.error('balances::attachToNewToken', 'failed to attach to tokenreg Registered', error.toString(), error.stack);
}
- const promises = logs.map((log) => {
- const id = log.params.id.value.toNumber();
- return this.fetchTokenInfo(this._tokenreg, id);
- });
-
- return Promise.all(promises);
+ this.handleTokensLogs(logs);
})
.then((tokenregSubId) => {
this._tokenregSubId = tokenregSubId;
- })
- .catch((e) => {
- console.warn('balances::attachToNewToken', e);
});
}
- attachToTokenMetaChange () {
+ attachToTokenMetaChange (tokenreg) {
if (this._tokenregMetaSubId) {
- return;
+ return Promise.resolve();
}
- this._tokenreg
- .instance
- .MetaChanged
+ return tokenreg.instance.MetaChanged
.subscribe({
fromBlock: 0,
toBlock: 'latest',
- topics: [ null, this._api.util.asciiToHex('IMG') ],
+ topics: [ null, padRight(this._api.util.asciiToHex('IMG'), 32) ],
skipInitFetch: true
}, (error, logs) => {
if (error) {
return console.error('balances::attachToTokenMetaChange', 'failed to attach to tokenreg MetaChanged', error.toString(), error.stack);
}
- // In case multiple logs for same token
- // in one block. Take the last value.
- const tokens = logs
- .filter((log) => log.type === 'mined')
- .reduce((_tokens, log) => {
- const id = log.params.id.value.toNumber();
- const image = log.params.value.value;
-
- const token = Object.values(this._tokens).find((c) => c.id === id);
- const { address } = token;
-
- _tokens[address] = { address, id, image };
- return _tokens;
- }, {});
-
- Object
- .values(tokens)
- .forEach((token) => {
- const { address, image } = token;
-
- if (this._images[address] !== image.toString()) {
- this._store.dispatch(setAddressImage(address, image));
- this._images[address] = image.toString();
- }
- });
+ this.handleTokensLogs(logs);
})
.then((tokenregMetaSubId) => {
this._tokenregMetaSubId = tokenregMetaSubId;
- })
- .catch((e) => {
- console.warn('balances::attachToTokenMetaChange', e);
});
}
- fetchTokenInfo (tokenreg, tokenId) {
- 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 = this._api.newContract(abis.eip20, address);
-
- if (this._images[address] !== image.toString()) {
- this._store.dispatch(setAddressImage(address, image));
- this._images[address] = image.toString();
- }
-
- const token = {
- format: format.toString(),
- id: tokenId,
-
- address,
- tag,
- name,
- contract
- };
-
- this._tokens[address] = token;
-
- return token;
- })
- .catch((e) => {
- console.warn('balances::fetchTokenInfo', `couldn't fetch token #${tokenId}`, e);
- });
- }
-
- /**
- * TODO?: txCount is only shown on an address page, so we
- * might not need to fetch it for each address for each block,
- * but only for one address when the user is on the account
- * view.
- */
- fetchAccountBalance (address) {
- const _tokens = Object.values(this._tokens);
- const tokensPromises = _tokens
- .map((token) => {
- return token.contract.instance.balanceOf.call({}, [ address ]);
- });
-
- return Promise
- .all([
- this._api.eth.getTransactionCount(address),
- this._api.eth.getBalance(address)
- ].concat(tokensPromises))
- .then(([ txCount, ethBalance, ...tokensBalance ]) => {
- const tokens = []
- .concat(
- { token: ETH, value: ethBalance },
- _tokens
- .map((token, index) => ({
- token,
- value: tokensBalance[index]
- }))
- );
-
- const balance = { txCount, tokens };
- return balance;
- });
+ handleTokensLogs (logs) {
+ const tokenIds = logs.map((log) => log.params.id.value.toNumber());
+ this._store.dispatch(fetchTokens(tokenIds));
}
}
diff --git a/js/src/redux/providers/balancesActions.js b/js/src/redux/providers/balancesActions.js
index 2771c455e23..f5d602b73b2 100644
--- a/js/src/redux/providers/balancesActions.js
+++ b/js/src/redux/providers/balancesActions.js
@@ -14,16 +14,354 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see .
-export function getBalances (balances) {
+import { range, uniq, isEqual } from 'lodash';
+
+import { hashToImageUrl } from './imagesReducer';
+import { setAddressImage } from './imagesActions';
+
+import * as ABIS from '../../contracts/abi';
+import imagesEthereum from '../../../assets/images/contracts/ethereum-black-64x64.png';
+
+const ETH = {
+ name: 'Ethereum',
+ tag: 'ETH',
+ image: imagesEthereum
+};
+
+export function setBalances (balances) {
return {
- type: 'getBalances',
+ type: 'setBalances',
balances
};
}
-export function getTokens (tokens) {
+export function setTokens (tokens) {
return {
- type: 'getTokens',
+ 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 () {
+ return (dispatch, getState) => {
+ const { tokenreg } = getState().balances;
+
+ return tokenreg.instance.tokenCount
+ .call()
+ .then((numTokens) => {
+ const tokenIds = range(numTokens.toNumber());
+ dispatch(fetchTokens(tokenIds));
+ })
+ .catch((error) => {
+ console.warn('balances::loadTokens', error);
+ });
+ };
+}
+
+export function fetchTokens (_tokenIds) {
+ const tokenIds = uniq(_tokenIds || []);
+ return (dispatch, getState) => {
+ const { api, images, balances } = getState();
+ const { tokenreg } = balances;
+
+ return Promise
+ .all(tokenIds.map((id) => fetchTokenInfo(tokenreg, id, api)))
+ .then((tokens) => {
+ // dispatch only the changed images
+ tokens
+ .forEach((token) => {
+ const { image, address } = token;
+
+ if (images[address] === image) {
+ return;
+ }
+
+ dispatch(setTokenImage(address, image));
+ dispatch(setAddressImage(address, image, true));
+ });
+
+ dispatch(setTokens(tokens));
+ dispatch(fetchBalances());
+ })
+ .catch((error) => {
+ console.warn('balances::fetchTokens', error);
+ });
+ };
+}
+
+export function fetchBalances (_addresses) {
+ return (dispatch, getState) => {
+ const { api, personal } = getState();
+ const { visibleAccounts } = personal;
+
+ const addresses = uniq(_addresses || visibleAccounts || []);
+
+ if (addresses.length === 0) {
+ return Promise.resolve();
+ }
+
+ const fullFetch = addresses.length === 1;
+
+ return Promise
+ .all(addresses.map((addr) => fetchAccount(addr, api, fullFetch)))
+ .then((accountsBalances) => {
+ const balances = {};
+
+ addresses.forEach((addr, idx) => {
+ balances[addr] = accountsBalances[idx];
+ });
+
+ dispatch(setBalances(balances));
+ updateTokensFilter(addresses)(dispatch, getState);
+ })
+ .catch((error) => {
+ console.warn('balances::fetchBalances', error);
+ });
+ };
+}
+
+export function updateTokensFilter (_addresses, _tokens) {
+ return (dispatch, getState) => {
+ const { api, balances, personal } = getState();
+ const { visibleAccounts } = personal;
+ const { tokensFilter } = balances;
+
+ const addresses = uniq(_addresses || visibleAccounts || []).sort();
+ const tokens = _tokens || Object.values(balances.tokens) || [];
+ const tokenAddresses = tokens.map((t) => t.address).sort();
+
+ if (tokensFilter.filterFromId || tokensFilter.filterToId) {
+ const sameTokens = isEqual(tokenAddresses, tokensFilter.tokenAddresses);
+ const sameAddresses = isEqual(addresses, tokensFilter.addresses);
+
+ if (sameTokens && sameAddresses) {
+ return queryTokensFilter(tokensFilter)(dispatch, getState);
+ }
+ }
+
+ let promise = Promise.resolve();
+
+ if (tokensFilter.filterFromId) {
+ promise = promise.then(() => api.eth.uninstallFilter(tokensFilter.filterFromId));
+ }
+
+ if (tokensFilter.filterToId) {
+ promise = promise.then(() => api.eth.uninstallFilter(tokensFilter.filterToId));
+ }
+
+ if (tokenAddresses.length === 0 || addresses.length === 0) {
+ return promise;
+ }
+
+ const TRANSFER_SIGNATURE = api.util.sha3('Transfer(address,address,uint256)');
+ const topicsFrom = [ TRANSFER_SIGNATURE, addresses, null ];
+ const topicsTo = [ TRANSFER_SIGNATURE, null, addresses ];
+
+ const options = {
+ fromBlock: 0,
+ toBlock: 'pending',
+ address: tokenAddresses
+ };
+
+ const optionsFrom = {
+ ...options,
+ topics: topicsFrom
+ };
+
+ const optionsTo = {
+ ...options,
+ topics: topicsTo
+ };
+
+ const newFilters = Promise.all([
+ api.eth.newFilter(optionsFrom),
+ api.eth.newFilter(optionsTo)
+ ]);
+
+ promise
+ .then(() => newFilters)
+ .then(([ filterFromId, filterToId ]) => {
+ const nextTokensFilter = {
+ filterFromId, filterToId,
+ addresses, tokenAddresses
+ };
+
+ dispatch(setTokensFilter(nextTokensFilter));
+ fetchTokensBalances(addresses, tokens)(dispatch, getState);
+ })
+ .catch((error) => {
+ console.warn('balances::updateTokensFilter', error);
+ });
+ };
+}
+
+export function queryTokensFilter (tokensFilter) {
+ return (dispatch, getState) => {
+ const { api, personal, balances } = getState();
+ const { visibleAccounts } = personal;
+ const visibleAddresses = visibleAccounts.map((a) => a.toLowerCase());
+
+ Promise
+ .all([
+ api.eth.getFilterChanges(tokensFilter.filterFromId),
+ api.eth.getFilterChanges(tokensFilter.filterToId)
+ ])
+ .then(([ logsFrom, logsTo ]) => {
+ const addresses = [];
+ const tokenAddresses = [];
+
+ logsFrom
+ .concat(logsTo)
+ .forEach((log) => {
+ const tokenAddress = log.address;
+ const fromAddress = '0x' + log.topics[1].slice(-40);
+ const toAddress = '0x' + log.topics[2].slice(-40);
+
+ const fromIdx = visibleAddresses.indexOf(fromAddress);
+ const toIdx = visibleAddresses.indexOf(toAddress);
+
+ if (fromIdx > -1) {
+ addresses.push(visibleAccounts[fromIdx]);
+ }
+
+ if (toIdx > -1) {
+ addresses.push(visibleAccounts[toIdx]);
+ }
+
+ tokenAddresses.push(tokenAddress);
+ });
+
+ if (addresses.length === 0) {
+ return;
+ }
+
+ const tokens = balances.tokens.filter((t) => tokenAddresses.includes(t.address));
+
+ fetchTokensBalances(uniq(addresses), tokens)(dispatch, getState);
+ });
+ };
+}
+
+export function fetchTokensBalances (_addresses = null, _tokens = null) {
+ return (dispatch, getState) => {
+ const { api, personal, balances } = getState();
+ const { visibleAccounts } = personal;
+
+ const addresses = _addresses || visibleAccounts;
+ const tokens = _tokens || Object.values(balances.tokens);
+
+ 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];
+ });
+
+ dispatch(setBalances(balances));
+ })
+ .catch((error) => {
+ console.warn('balances::fetchTokensBalances', error);
+ });
+ };
+}
+
+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);
+ });
+}
diff --git a/js/src/redux/providers/balancesReducer.js b/js/src/redux/providers/balancesReducer.js
index ea28a52174d..f26f08f7d64 100644
--- a/js/src/redux/providers/balancesReducer.js
+++ b/js/src/redux/providers/balancesReducer.js
@@ -15,22 +15,92 @@
// along with Parity. If not, see .
import { handleActions } from 'redux-actions';
+import BigNumber from 'bignumber.js';
const initialState = {
balances: {},
- tokens: {}
+ tokens: {},
+ tokenreg: null,
+ tokensFilter: {}
};
export default handleActions({
- getBalances (state, action) {
- const { balances } = action;
+ setBalances (state, action) {
+ const nextBalances = action.balances;
+ const prevBalances = state.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 nextTokens = [].concat(balance.tokens);
+
+ tokens.forEach((t) => {
+ const { token, value } = t;
+ const { tag } = token;
+
+ const tokenIndex = nextTokens.findIndex((tok) => tok.token.tag === tag);
+
+ if (tokenIndex === -1) {
+ nextTokens.push({
+ token,
+ value
+ });
+ } else {
+ nextTokens[tokenIndex] = { token, value };
+ }
+ });
+
+ balances[address] = Object.assign({}, { txCount: txCount || new BigNumber(0), tokens: nextTokens });
+ });
return Object.assign({}, state, { balances });
},
- getTokens (state, action) {
+ setTokens (state, action) {
const { tokens } = action;
-
return Object.assign({}, state, { tokens });
+ },
+
+ setTokenImage (state, action) {
+ const { tokenAddress, image } = action;
+ const { balances } = state;
+ const nextBalances = {};
+
+ Object.keys(balances).forEach((address) => {
+ const tokenIndex = balances[address].tokens.findIndex((t) => t.token.address === tokenAddress);
+
+ if (tokenIndex === -1 || balances[address].tokens[tokenIndex].value.equals(0)) {
+ return;
+ }
+
+ const tokens = [].concat(balances[address].tokens);
+ tokens[tokenIndex].token = {
+ ...tokens[tokenIndex].token,
+ image
+ };
+
+ nextBalances[address] = {
+ ...balances[address],
+ tokens
+ };
+ });
+
+ return Object.assign({}, state, { balance: { ...balances, nextBalances } });
+ },
+
+ setTokenReg (state, action) {
+ const { tokenreg } = action;
+ return Object.assign({}, state, { tokenreg });
+ },
+
+ setTokensFilter (state, action) {
+ const { tokensFilter } = action;
+ return Object.assign({}, state, { tokensFilter });
}
}, initialState);
diff --git a/js/src/redux/providers/imagesActions.js b/js/src/redux/providers/imagesActions.js
index ce9221a3b97..8ef3c3b39f3 100644
--- a/js/src/redux/providers/imagesActions.js
+++ b/js/src/redux/providers/imagesActions.js
@@ -14,10 +14,11 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see .
-export function setAddressImage (address, hashArray) {
+export function setAddressImage (address, hashArray, converted = false) {
return {
type: 'setAddressImage',
address,
- hashArray
+ hashArray,
+ converted
};
}
diff --git a/js/src/redux/providers/imagesReducer.js b/js/src/redux/providers/imagesReducer.js
index 396576cc8fc..3f91f262fe6 100644
--- a/js/src/redux/providers/imagesReducer.js
+++ b/js/src/redux/providers/imagesReducer.js
@@ -31,10 +31,12 @@ export function hashToImageUrl (hashArray) {
export default handleActions({
setAddressImage (state, action) {
- const { address, hashArray } = action;
+ const { address, hashArray, converted } = action;
+
+ const image = converted ? hashArray : hashToImageUrl(hashArray);
return Object.assign({}, state, {
- [address]: hashToImageUrl(hashArray)
+ [address]: image
});
}
}, initialState);
diff --git a/js/src/redux/providers/personalActions.js b/js/src/redux/providers/personalActions.js
index 2e422cb1afa..d8f925b2da9 100644
--- a/js/src/redux/providers/personalActions.js
+++ b/js/src/redux/providers/personalActions.js
@@ -14,9 +14,33 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see .
+import { isEqual } from 'lodash';
+
+import { fetchBalances } from './balancesActions';
+
export function personalAccountsInfo (accountsInfo) {
return {
type: 'personalAccountsInfo',
accountsInfo
};
}
+
+export function _setVisibleAccounts (addresses) {
+ return {
+ type: 'setVisibleAccounts',
+ addresses
+ };
+}
+
+export function setVisibleAccounts (addresses) {
+ return (dispatch, getState) => {
+ const { visibleAccounts } = getState().personal;
+
+ if (isEqual(addresses.sort(), visibleAccounts.sort())) {
+ return;
+ }
+
+ dispatch(fetchBalances(addresses));
+ dispatch(_setVisibleAccounts(addresses));
+ };
+}
diff --git a/js/src/redux/providers/personalReducer.js b/js/src/redux/providers/personalReducer.js
index 6d35610e9b3..622c81ee517 100644
--- a/js/src/redux/providers/personalReducer.js
+++ b/js/src/redux/providers/personalReducer.js
@@ -15,6 +15,7 @@
// along with Parity. If not, see .
import { handleActions } from 'redux-actions';
+import { isEqual } from 'lodash';
const initialState = {
accountsInfo: {},
@@ -23,7 +24,8 @@ const initialState = {
contacts: {},
hasContacts: false,
contracts: {},
- hasContracts: false
+ hasContracts: false,
+ visibleAccounts: []
};
export default handleActions({
@@ -55,5 +57,17 @@ export default handleActions({
contracts,
hasContracts: Object.keys(contracts).length !== 0
});
+ },
+
+ setVisibleAccounts (state, action) {
+ const addresses = (action.addresses || []).sort();
+
+ if (isEqual(addresses, state.addresses)) {
+ return state;
+ }
+
+ return Object.assign({}, state, {
+ visibleAccounts: addresses
+ });
}
}, initialState);
diff --git a/js/src/redux/providers/status.js b/js/src/redux/providers/status.js
index cedf62d891a..936fe3f2552 100644
--- a/js/src/redux/providers/status.js
+++ b/js/src/redux/providers/status.js
@@ -30,6 +30,8 @@ export default class Status {
this._pollPingTimeoutId = null;
this._longStatusTimeoutId = null;
+
+ this._timestamp = Date.now();
}
start () {
@@ -131,10 +133,10 @@ export default class Status {
secureToken
};
- const gotReconnected = !this._apiStatus.isConnected && apiStatus.isConnected;
+ const gotConnected = !this._apiStatus.isConnected && apiStatus.isConnected;
- if (gotReconnected) {
- this._pollLongStatus(true);
+ if (gotConnected) {
+ this._pollLongStatus();
this._store.dispatch(statusCollection({ isPingable: true }));
}
@@ -156,20 +158,22 @@ export default class Status {
const { refreshStatus } = this._store.getState().nodeStatus;
- const statusPromises = [ this._api.eth.syncing(), this._api.parity.netPeers() ];
+ const statusPromises = [ this._api.eth.syncing() ];
if (refreshStatus) {
+ statusPromises.push(this._api.parity.netPeers());
statusPromises.push(this._api.eth.hashrate());
}
Promise
.all(statusPromises)
- .then(([ syncing, netPeers, ...statusResults ]) => {
+ .then(([ syncing, ...statusResults ]) => {
const status = statusResults.length === 0
- ? { syncing, netPeers }
+ ? { syncing }
: {
- syncing, netPeers,
- hashrate: statusResults[0]
+ syncing,
+ netPeers: statusResults[0],
+ hashrate: statusResults[1]
};
if (!isEqual(status, this._status)) {
@@ -223,7 +227,7 @@ export default class Status {
* fetched every 30s just in case, and whenever
* the client got reconnected.
*/
- _pollLongStatus = (newConnection = false) => {
+ _pollLongStatus = () => {
if (!this._api.isConnected) {
return;
}
@@ -241,34 +245,33 @@ export default class Status {
Promise
.all([
+ this._api.parity.netPeers(),
this._api.web3.clientVersion(),
this._api.net.version(),
this._api.parity.defaultExtraData(),
this._api.parity.netChain(),
this._api.parity.netPort(),
this._api.parity.rpcSettings(),
- newConnection ? Promise.resolve(null) : this._api.parity.enode()
+ this._api.parity.enode()
])
.then(([
- clientVersion, netVersion, defaultExtraData, netChain, netPort, rpcSettings, enode
+ netPeers, clientVersion, netVersion, defaultExtraData, netChain, netPort, rpcSettings, enode
]) => {
const isTest =
netVersion === '2' || // morden
netVersion === '3'; // ropsten
const longStatus = {
+ netPeers,
clientVersion,
defaultExtraData,
netChain,
netPort,
rpcSettings,
- isTest
+ isTest,
+ enode
};
- if (enode) {
- longStatus.enode = enode;
- }
-
if (!isEqual(longStatus, this._longStatus)) {
this._store.dispatch(statusCollection(longStatus));
this._longStatus = longStatus;
@@ -278,7 +281,7 @@ export default class Status {
console.error('_pollLongStatus', error);
});
- nextTimeout(newConnection ? 5000 : 30000);
+ nextTimeout(60000);
}
_pollLogs = () => {
diff --git a/js/src/redux/providers/statusReducer.js b/js/src/redux/providers/statusReducer.js
index f0ef0cb1bf6..07ba4af5b35 100644
--- a/js/src/redux/providers/statusReducer.js
+++ b/js/src/redux/providers/statusReducer.js
@@ -43,7 +43,7 @@ const initialState = {
isConnected: false,
isConnecting: false,
isPingable: false,
- isTest: false,
+ isTest: undefined,
refreshStatus: false,
traceMode: undefined
};
diff --git a/js/src/secureApi.js b/js/src/secureApi.js
index af62da2cf68..8243420b5f8 100644
--- a/js/src/secureApi.js
+++ b/js/src/secureApi.js
@@ -77,6 +77,12 @@ export default class SecureApi extends Api {
return this
._checkNodeUp()
.then((isNodeUp) => {
+ const { timestamp } = lastError;
+
+ if ((Date.now() - timestamp) > 250) {
+ return nextTick();
+ }
+
const nextToken = this._tokensToTry[0] || 'initial';
const nextState = nextToken !== 'initial' ? 0 : 1;
@@ -89,7 +95,7 @@ export default class SecureApi extends Api {
this.updateToken(nextToken, nextState);
}
- nextTick();
+ return nextTick();
});
}
break;
diff --git a/js/src/ui/BlockStatus/blockStatus.js b/js/src/ui/BlockStatus/blockStatus.js
index 98e10f50472..8abd5d656a7 100644
--- a/js/src/ui/BlockStatus/blockStatus.js
+++ b/js/src/ui/BlockStatus/blockStatus.js
@@ -44,7 +44,7 @@ class BlockStatus extends Component {
);
}
- if (!syncing.warpChunksAmount.eq(syncing.warpChunksProcessed)) {
+ if (syncing.warpChunksAmount && syncing.warpChunksProcessed && !syncing.warpChunksAmount.eq(syncing.warpChunksProcessed)) {
return (
{ syncing.warpChunksProcessed.mul(100).div(syncing.warpChunksAmount).toFormat(2) }% warp restore
diff --git a/js/src/views/Account/Header/header.js b/js/src/views/Account/Header/header.js
index 66f0a36b99c..b28abeb69ca 100644
--- a/js/src/views/Account/Header/header.js
+++ b/js/src/views/Account/Header/header.js
@@ -28,20 +28,7 @@ export default class Header extends Component {
static propTypes = {
account: PropTypes.object,
- balance: PropTypes.object,
- isTest: PropTypes.bool
- }
-
- state = {
- name: null
- }
-
- componentWillMount () {
- this.setName();
- }
-
- componentWillReceiveProps () {
- this.setName();
+ balance: PropTypes.object
}
render () {
@@ -87,13 +74,13 @@ export default class Header extends Component {
}
renderTxCount () {
- const { isTest, balance } = this.props;
+ const { balance } = this.props;
if (!balance) {
return null;
}
- const txCount = balance.txCount.sub(isTest ? 0x100000 : 0);
+ const { txCount } = balance;
return (
@@ -101,28 +88,4 @@ export default class Header extends Component {
);
}
-
- onSubmitName = (name) => {
- const { api } = this.context;
- const { account } = this.props;
-
- this.setState({ name }, () => {
- api.parity
- .setAccountName(account.address, name)
- .catch((error) => {
- console.error(error);
- });
- });
- }
-
- setName () {
- const { account } = this.props;
-
- if (account && account.name !== this.propName) {
- this.propName = account.name;
- this.setState({
- name: account.name
- });
- }
- }
}
diff --git a/js/src/views/Account/Transactions/transactions.js b/js/src/views/Account/Transactions/transactions.js
index 3e14dd9236e..2261284a1c8 100644
--- a/js/src/views/Account/Transactions/transactions.js
+++ b/js/src/views/Account/Transactions/transactions.js
@@ -143,6 +143,12 @@ class Transactions extends Component {
getTransactions = (props) => {
const { isTest, address, traceMode } = props;
+ // Don't fetch the transactions if we don't know in which
+ // network we are yet...
+ if (isTest === undefined) {
+ return;
+ }
+
return this
.fetchTransactions(isTest, address, traceMode)
.then(transactions => {
diff --git a/js/src/views/Account/account.js b/js/src/views/Account/account.js
index b36f5861838..1181b7f7384 100644
--- a/js/src/views/Account/account.js
+++ b/js/src/views/Account/account.js
@@ -30,6 +30,7 @@ import shapeshiftBtn from '../../../assets/images/shapeshift-btn.png';
import Header from './Header';
import Transactions from './Transactions';
+import { setVisibleAccounts } from '../../redux/providers/personalActions';
import VerificationStore from '../../modals/SMSVerification/store';
@@ -41,11 +42,12 @@ class Account extends Component {
}
static propTypes = {
+ setVisibleAccounts: PropTypes.func.isRequired,
+ images: PropTypes.object.isRequired,
+
params: PropTypes.object,
accounts: PropTypes.object,
- balances: PropTypes.object,
- images: PropTypes.object.isRequired,
- isTest: PropTypes.bool
+ balances: PropTypes.object
}
propName = null
@@ -66,10 +68,30 @@ class Account extends Component {
const verificationStore = new VerificationStore(api, address);
this.setState({ verificationStore });
+ this.setVisibleAccounts();
+ }
+
+ componentWillReceiveProps (nextProps) {
+ const prevAddress = this.props.params.address;
+ const nextAddress = nextProps.params.address;
+
+ if (prevAddress !== nextAddress) {
+ this.setVisibleAccounts(nextProps);
+ }
+ }
+
+ componentWillUnmount () {
+ this.props.setVisibleAccounts([]);
+ }
+
+ setVisibleAccounts (props = this.props) {
+ const { params, setVisibleAccounts } = props;
+ const addresses = [ params.address ];
+ setVisibleAccounts(addresses);
}
render () {
- const { accounts, balances, isTest } = this.props;
+ const { accounts, balances } = this.props;
const { address } = this.props.params;
const account = (accounts || {})[address];
@@ -90,7 +112,6 @@ class Account extends Component {
{ this.renderActionbar() }
token.token.tag.toLowerCase() === 'eth')
- .value;
- const ethB = balanceB.tokens
- .find(token => token.token.tag.toLowerCase() === 'eth')
- .value;
-
- return -1 * ethA.comparedTo(ethB);
+ const ethA = balanceA.tokens.find(token => token.token.tag.toLowerCase() === 'eth');
+ const ethB = balanceB.tokens.find(token => token.token.tag.toLowerCase() === 'eth');
+
+ if (!ethA && !ethB) return 0;
+ if (ethA && !ethB) return -1;
+ if (!ethA && ethB) return 1;
+
+ return -1 * ethA.value.comparedTo(ethB.value);
}
if (key === 'tags') {
diff --git a/js/src/views/Accounts/Summary/summary.js b/js/src/views/Accounts/Summary/summary.js
index 88249bb1cec..4baf838ce48 100644
--- a/js/src/views/Accounts/Summary/summary.js
+++ b/js/src/views/Accounts/Summary/summary.js
@@ -38,10 +38,6 @@ export default class Summary extends Component {
noLink: false
};
- state = {
- name: 'Unnamed'
- };
-
shouldComponentUpdate (nextProps) {
const prev = {
link: this.props.link, name: this.props.name,
@@ -66,8 +62,8 @@ export default class Summary extends Component {
return true;
}
- const prevValues = prevTokens.map((t) => t.value.toNumber());
- const nextValues = nextTokens.map((t) => t.value.toNumber());
+ 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;
diff --git a/js/src/views/Accounts/accounts.js b/js/src/views/Accounts/accounts.js
index 0075a15a2ec..df55a47e70f 100644
--- a/js/src/views/Accounts/accounts.js
+++ b/js/src/views/Accounts/accounts.js
@@ -18,11 +18,12 @@ import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import ContentAdd from 'material-ui/svg-icons/content/add';
-import { uniq } from 'lodash';
+import { uniq, isEqual } from 'lodash';
import List from './List';
import { CreateAccount } from '../../modals';
import { Actionbar, ActionbarExport, ActionbarSearch, ActionbarSort, Button, Page, Tooltip } from '../../ui';
+import { setVisibleAccounts } from '../../redux/providers/personalActions';
import styles from './accounts.css';
@@ -32,6 +33,8 @@ class Accounts extends Component {
}
static propTypes = {
+ setVisibleAccounts: PropTypes.func.isRequired,
+
accounts: PropTypes.object,
hasAccounts: PropTypes.bool,
balances: PropTypes.object
@@ -50,6 +53,27 @@ class Accounts extends Component {
window.setTimeout(() => {
this.setState({ show: true });
}, 100);
+
+ this.setVisibleAccounts();
+ }
+
+ componentWillReceiveProps (nextProps) {
+ const prevAddresses = Object.keys(this.props.accounts);
+ const nextAddresses = Object.keys(nextProps.accounts);
+
+ if (prevAddresses.length !== nextAddresses.length || !isEqual(prevAddresses.sort(), nextAddresses.sort())) {
+ this.setVisibleAccounts(nextProps);
+ }
+ }
+
+ componentWillUnmount () {
+ this.props.setVisibleAccounts([]);
+ }
+
+ setVisibleAccounts (props = this.props) {
+ const { accounts, setVisibleAccounts } = props;
+ const addresses = Object.keys(accounts);
+ setVisibleAccounts(addresses);
}
render () {
@@ -206,7 +230,9 @@ function mapStateToProps (state) {
}
function mapDispatchToProps (dispatch) {
- return bindActionCreators({}, dispatch);
+ return bindActionCreators({
+ setVisibleAccounts
+ }, dispatch);
}
export default connect(
diff --git a/js/src/views/Address/address.js b/js/src/views/Address/address.js
index 210f63b9997..f0a4521851f 100644
--- a/js/src/views/Address/address.js
+++ b/js/src/views/Address/address.js
@@ -26,6 +26,7 @@ import { Actionbar, Button, Page } from '../../ui';
import Header from '../Account/Header';
import Transactions from '../Account/Transactions';
import Delete from './Delete';
+import { setVisibleAccounts } from '../../redux/providers/personalActions';
import styles from './address.css';
@@ -36,9 +37,10 @@ class Address extends Component {
}
static propTypes = {
+ setVisibleAccounts: PropTypes.func.isRequired,
+
contacts: PropTypes.object,
balances: PropTypes.object,
- isTest: PropTypes.bool,
params: PropTypes.object
}
@@ -47,8 +49,31 @@ class Address extends Component {
showEditDialog: false
}
+ componentDidMount () {
+ this.setVisibleAccounts();
+ }
+
+ componentWillReceiveProps (nextProps) {
+ const prevAddress = this.props.params.address;
+ const nextAddress = nextProps.params.address;
+
+ if (prevAddress !== nextAddress) {
+ this.setVisibleAccounts(nextProps);
+ }
+ }
+
+ componentWillUnmount () {
+ this.props.setVisibleAccounts([]);
+ }
+
+ setVisibleAccounts (props = this.props) {
+ const { params, setVisibleAccounts } = props;
+ const addresses = [ params.address ];
+ setVisibleAccounts(addresses);
+ }
+
render () {
- const { contacts, balances, isTest } = this.props;
+ const { contacts, balances } = this.props;
const { address } = this.props.params;
const { showDeleteDialog } = this.state;
@@ -70,7 +95,6 @@ class Address extends Component {
onClose={ this.closeDeleteDialog } />
this.setState({ blockSubscriptionId }));
}
- componentWillReceiveProps (newProps) {
- const { accounts, contracts } = newProps;
+ componentWillReceiveProps (nextProps) {
+ const { accounts, contracts } = nextProps;
if (Object.keys(contracts).length !== Object.keys(this.props.contracts).length) {
- this.attachContract(newProps);
+ this.attachContract(nextProps);
}
if (Object.keys(accounts).length !== Object.keys(this.props.accounts).length) {
- this.setBaseAccount(newProps);
+ this.setBaseAccount(nextProps);
+ }
+
+ const prevAddress = this.props.params.address;
+ const nextAddress = nextProps.params.address;
+
+ if (prevAddress !== nextAddress) {
+ this.setVisibleAccounts(nextProps);
}
}
@@ -92,6 +104,13 @@ class Contract extends Component {
api.unsubscribe(blockSubscriptionId);
contract.unsubscribe(subscriptionId);
+ this.props.setVisibleAccounts([]);
+ }
+
+ setVisibleAccounts (props = this.props) {
+ const { params, setVisibleAccounts } = props;
+ const addresses = [ params.address ];
+ setVisibleAccounts(addresses);
}
render () {
@@ -112,7 +131,6 @@ class Contract extends Component {
{ this.renderExecuteDialog() }
{
return response.ok
? response.json()
: null;
})
+ .then((manifest) => {
+ if (manifest) {
+ this._manifests[manifestHash] = manifest;
+ }
+
+ return manifest;
+ })
.catch((error) => {
console.warn('DappsStore:fetchManifest', error);
return null;