diff --git a/js/src/api/format/input.js b/js/src/api/format/input.js index 55c85e4f3..80f3bc0eb 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 341839f69..6cb0dac17 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 c30c910e6..53600b6d3 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 93f31a161..f1909748d 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 b07949a28..027de57af 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 9e9c0a481..6b1084934 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; - } + fetchTokens () { + this._store.dispatch(fetchTokensBalances()); + } - if (this._fetchedTokens) { - return this._retrieveBalances(); - } - - 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 = []; + this._store.dispatch(setTokenReg(tokenreg)); + this._store.dispatch(loadTokens()); - 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(); + return this.attachToTokens(tokenreg); }) .catch((error) => { - console.warn('balances::_retrieveTokens', error); + console.warn('balances::loadTokens', 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._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; - }) - .catch((error) => { - console.warn('_retrieveBalances', error); - this._fetchingBalances = false; - }); + attachToTokens (tokenreg) { + return Promise + .all([ + this.attachToTokenMetaChange(tokenreg), + this.attachToNewToken(tokenreg) + ]); } - attachToTokens () { - this.attachToTokenMetaChange(); - this.attachToNewToken(); - } - - 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 2771c455e..f5d602b73 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 ea28a5217..f26f08f7d 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 ce9221a3b..8ef3c3b39 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 396576cc8..3f91f262f 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 2e422cb1a..d8f925b2d 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 6d35610e9..622c81ee5 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 cedf62d89..936fe3f25 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 f0ef0cb1b..07ba4af5b 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 af62da2cf..8243420b5 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 98e10f504..8abd5d656 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 66f0a36b9..b28abeb69 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 3e14dd923..2261284a1 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 b36f58618..1181b7f73 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; + const ethA = balanceA.tokens.find(token => token.token.tag.toLowerCase() === 'eth'); + const ethB = balanceB.tokens.find(token => token.token.tag.toLowerCase() === 'eth'); - return -1 * ethA.comparedTo(ethB); + 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 88249bb1c..4baf838ce 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 0075a15a2..df55a47e7 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 210f63b99..f0a452185 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;