diff --git a/js/src/api/api.js b/js/src/api/api.js
index bb622ab46..1eac2d89d 100644
--- a/js/src/api/api.js
+++ b/js/src/api/api.js
@@ -14,6 +14,8 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see .
+import EventEmitter from 'eventemitter3';
+
import { Http, Ws } from './transport';
import Contract from './contract';
@@ -22,8 +24,10 @@ import Subscriptions from './subscriptions';
import util from './util';
import { isFunction } from './util/types';
-export default class Api {
+export default class Api extends EventEmitter {
constructor (transport) {
+ super();
+
if (!transport || !isFunction(transport.execute)) {
throw new Error('EthApi needs transport with execute() function defined');
}
diff --git a/js/src/api/transport/http/http.js b/js/src/api/transport/http/http.js
index 17d428e75..36e0ae1b7 100644
--- a/js/src/api/transport/http/http.js
+++ b/js/src/api/transport/http/http.js
@@ -51,14 +51,14 @@ export default class Http extends JsonRpcBase {
return fetch(this._url, request)
.catch((error) => {
- this._connected = false;
+ this._setDisconnected();
throw error;
})
.then((response) => {
- this._connected = true;
+ this._setConnected();
if (response.status !== 200) {
- this._connected = false;
+ this._setDisconnected();
this.error(JSON.stringify({ status: response.status, statusText: response.statusText }));
console.error(`${method}(${JSON.stringify(params)}): ${response.status}: ${response.statusText}`);
diff --git a/js/src/api/transport/http/http.spec.js b/js/src/api/transport/http/http.spec.js
index d67f11307..685c6b948 100644
--- a/js/src/api/transport/http/http.spec.js
+++ b/js/src/api/transport/http/http.spec.js
@@ -37,6 +37,26 @@ describe('api/transport/Http', () => {
});
});
+ describe('transport emitter', () => {
+ it('emits close event', (done) => {
+ transport.once('close', () => {
+ done();
+ });
+
+ transport.execute('eth_call');
+ });
+
+ it('emits open event', (done) => {
+ mockHttp([{ method: 'eth_call', reply: { result: '' } }]);
+
+ transport.once('open', () => {
+ done();
+ });
+
+ transport.execute('eth_call');
+ });
+ });
+
describe('transport', () => {
const RESULT = ['this is some result'];
diff --git a/js/src/api/transport/jsonRpcBase.js b/js/src/api/transport/jsonRpcBase.js
index 76f380935..d5c1e8cb7 100644
--- a/js/src/api/transport/jsonRpcBase.js
+++ b/js/src/api/transport/jsonRpcBase.js
@@ -14,8 +14,12 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see .
-export default class JsonRpcBase {
+import EventEmitter from 'eventemitter3';
+
+export default class JsonRpcBase extends EventEmitter {
constructor () {
+ super();
+
this._id = 1;
this._debug = false;
this._connected = false;
@@ -32,6 +36,20 @@ export default class JsonRpcBase {
return json;
}
+ _setConnected () {
+ if (!this._connected) {
+ this._connected = true;
+ this.emit('open');
+ }
+ }
+
+ _setDisconnected () {
+ if (this._connected) {
+ this._connected = false;
+ this.emit('close');
+ }
+ }
+
get id () {
return this._id;
}
diff --git a/js/src/api/transport/ws/ws.js b/js/src/api/transport/ws/ws.js
index 591cf3062..ef739ae13 100644
--- a/js/src/api/transport/ws/ws.js
+++ b/js/src/api/transport/ws/ws.js
@@ -22,7 +22,7 @@ import TransportError from '../error';
/* global WebSocket */
export default class Ws extends JsonRpcBase {
- constructor (url, token, connect = true) {
+ constructor (url, token, autoconnect = true) {
super();
this._url = url;
@@ -32,14 +32,14 @@ export default class Ws extends JsonRpcBase {
this._connecting = false;
this._connected = false;
this._lastError = null;
- this._autoConnect = false;
+ this._autoConnect = autoconnect;
this._retries = 0;
this._reconnectTimeoutId = null;
this._connectPromise = null;
this._connectPromiseFunctions = {};
- if (connect) {
+ if (autoconnect) {
this.connect();
}
}
@@ -124,11 +124,8 @@ export default class Ws extends JsonRpcBase {
}
_onOpen = (event) => {
- console.log('ws:onOpen');
-
- this._connected = true;
+ this._setConnected();
this._connecting = false;
- this._autoConnect = true;
this._retries = 0;
Object.keys(this._messages)
@@ -142,7 +139,7 @@ export default class Ws extends JsonRpcBase {
}
_onClose = (event) => {
- this._connected = false;
+ this._setDisconnected();
this._connecting = false;
event.timestamp = Date.now();
@@ -209,8 +206,8 @@ export default class Ws extends JsonRpcBase {
if (result.error) {
this.error(event.data);
- // Don't print error if request rejected...
- if (!/rejected/.test(result.error.message)) {
+ // Don't print error if request rejected or not is not yet up...
+ if (!/(rejected|not yet up)/.test(result.error.message)) {
console.error(`${method}(${JSON.stringify(params)}): ${result.error.code}: ${result.error.message}`);
}
diff --git a/js/src/api/transport/ws/ws.spec.js b/js/src/api/transport/ws/ws.spec.js
index 9303803bf..019cd166e 100644
--- a/js/src/api/transport/ws/ws.spec.js
+++ b/js/src/api/transport/ws/ws.spec.js
@@ -21,6 +21,37 @@ describe('api/transport/Ws', () => {
let transport;
let scope;
+ describe('transport emitter', () => {
+ const connect = () => {
+ const scope = mockWs();
+ const transport = new Ws(TEST_WS_URL);
+
+ return { transport, scope };
+ };
+
+ it('emits open event', (done) => {
+ const { transport, scope } = connect();
+
+ transport.once('open', () => {
+ done();
+ });
+
+ scope.stop();
+ });
+
+ it('emits close event', (done) => {
+ const { transport, scope } = connect();
+
+ transport.once('open', () => {
+ scope.server.close();
+ });
+
+ transport.once('close', () => {
+ done();
+ });
+ });
+ });
+
describe('transport', () => {
let result;
diff --git a/js/src/config.js b/js/src/config.js
index 87fedecb8..6914f2f03 100644
--- a/js/src/config.js
+++ b/js/src/config.js
@@ -17,12 +17,20 @@
import LogLevel from 'loglevel';
export const LOG_KEYS = {
+ Balances: {
+ key: 'balances',
+ desc: 'Balances fetching'
+ },
TransferModalStore: {
- path: 'modals/Transfer/store',
- desc: 'Transfer Modal MobX Store'
+ key: 'modalsTransferStore',
+ desc: 'Transfer modal MobX store'
+ },
+ Signer: {
+ key: 'secureApi',
+ desc: 'The Signer and the Secure API'
}
};
export const getLogger = (LOG_KEY) => {
- return LogLevel.getLogger(LOG_KEY.path);
+ return LogLevel.getLogger(LOG_KEY.key);
};
diff --git a/js/src/redux/providers/balances.js b/js/src/redux/providers/balances.js
index 8d46e42d2..bb5ed19fa 100644
--- a/js/src/redux/providers/balances.js
+++ b/js/src/redux/providers/balances.js
@@ -21,51 +21,164 @@ import { padRight } from '~/api/util/format';
import Contracts from '~/contracts';
+let instance = null;
+
export default class Balances {
constructor (store, api) {
this._api = api;
this._store = store;
- this._tokenregSubId = null;
- this._tokenregMetaSubId = null;
+ this._tokenreg = null;
+ this._tokenregSID = null;
+ this._tokenMetaSID = null;
- // Throttled `retrieveTokens` function
+ this._blockNumberSID = null;
+ this._accountsInfoSID = null;
+
+ // Throtthled load tokens (no more than once
+ // every minute)
+ this.loadTokens = throttle(
+ this._loadTokens,
+ 60 * 1000,
+ { leading: true, trailing: true }
+ );
+
+ // Throttled `_fetchBalances` function
// that gets called max once every 40s
this.longThrottledFetch = throttle(
- this.fetchBalances,
+ this._fetchBalances,
40 * 1000,
- { trailing: true }
+ { leading: false, trailing: true }
);
this.shortThrottledFetch = throttle(
- this.fetchBalances,
+ this._fetchBalances,
2 * 1000,
- { trailing: true }
+ { leading: false, trailing: true }
);
// Fetch all tokens every 2 minutes
this.throttledTokensFetch = throttle(
- this.fetchTokens,
- 60 * 1000,
- { trailing: true }
+ this._fetchTokens,
+ 2 * 60 * 1000,
+ { leading: false, trailing: true }
);
+
+ // Unsubscribe previous instance if it exists
+ if (instance) {
+ Balances.stop();
+ }
}
- start () {
- this.subscribeBlockNumber();
- this.subscribeAccountsInfo();
+ static get (store = {}) {
+ if (!instance && store) {
+ const { api } = store.getState();
+ return Balances.instantiate(store, api);
+ }
- this.loadTokens();
+ return instance;
+ }
+
+ static instantiate (store, api) {
+ if (!instance) {
+ instance = new Balances(store, api);
+ }
+
+ return instance;
+ }
+
+ static start () {
+ if (!instance) {
+ return Promise.reject('BalancesProvider has not been intiated yet');
+ }
+
+ const self = instance;
+
+ // Unsubscribe from previous subscriptions
+ return Balances
+ .stop()
+ .then(() => self.loadTokens())
+ .then(() => {
+ const promises = [
+ self.subscribeBlockNumber(),
+ self.subscribeAccountsInfo()
+ ];
+
+ return Promise.all(promises);
+ });
+ }
+
+ static stop () {
+ if (!instance) {
+ return Promise.resolve();
+ }
+
+ const self = instance;
+ const promises = [];
+
+ if (self._blockNumberSID) {
+ const p = self._api
+ .unsubscribe(self._blockNumberSID)
+ .then(() => {
+ self._blockNumberSID = null;
+ });
+
+ promises.push(p);
+ }
+
+ if (self._accountsInfoSID) {
+ const p = self._api
+ .unsubscribe(self._accountsInfoSID)
+ .then(() => {
+ self._accountsInfoSID = null;
+ });
+
+ promises.push(p);
+ }
+
+ // Unsubscribe without adding the promises
+ // to the result, since it would have to wait for a
+ // reconnection to resolve if the Node is disconnected
+ if (self._tokenreg) {
+ if (self._tokenregSID) {
+ const tokenregSID = self._tokenregSID;
+
+ self._tokenreg
+ .unsubscribe(tokenregSID)
+ .then(() => {
+ if (self._tokenregSID === tokenregSID) {
+ self._tokenregSID = null;
+ }
+ });
+ }
+
+ if (self._tokenMetaSID) {
+ const tokenMetaSID = self._tokenMetaSID;
+
+ self._tokenreg
+ .unsubscribe(tokenMetaSID)
+ .then(() => {
+ if (self._tokenMetaSID === tokenMetaSID) {
+ self._tokenMetaSID = null;
+ }
+ });
+ }
+ }
+
+ return Promise.all(promises);
}
subscribeAccountsInfo () {
- this._api
+ return this._api
.subscribe('parity_allAccountsInfo', (error, accountsInfo) => {
if (error) {
return;
}
- this.fetchBalances();
+ this.fetchAllBalances();
+ })
+ .then((accountsInfoSID) => {
+ this._accountsInfoSID = accountsInfoSID;
})
.catch((error) => {
console.warn('_subscribeAccountsInfo', error);
@@ -73,49 +186,97 @@ export default class Balances {
}
subscribeBlockNumber () {
- this._api
+ return this._api
.subscribe('eth_blockNumber', (error) => {
if (error) {
return console.warn('_subscribeBlockNumber', error);
}
- const { syncing } = this._store.getState().nodeStatus;
-
- this.throttledTokensFetch();
-
- // If syncing, only retrieve balances once every
- // few seconds
- if (syncing) {
- this.shortThrottledFetch.cancel();
- return this.longThrottledFetch();
- }
-
- this.longThrottledFetch.cancel();
- return this.shortThrottledFetch();
+ return this.fetchAllBalances();
+ })
+ .then((blockNumberSID) => {
+ this._blockNumberSID = blockNumberSID;
})
.catch((error) => {
console.warn('_subscribeBlockNumber', error);
});
}
- fetchBalances () {
- this._store.dispatch(fetchBalances());
+ fetchAllBalances (options = {}) {
+ // If it's a network change, reload the tokens
+ // ( and then fetch the tokens balances ) and fetch
+ // the accounts balances
+ if (options.changedNetwork) {
+ this.loadTokens({ skipNotifications: true });
+ this.loadTokens.flush();
+
+ this.fetchBalances({
+ force: true,
+ skipNotifications: true
+ });
+
+ return;
+ }
+
+ this.fetchTokensBalances(options);
+ this.fetchBalances(options);
}
- fetchTokens () {
- this._store.dispatch(fetchTokensBalances());
+ fetchTokensBalances (options) {
+ const { skipNotifications = false, force = false } = options;
+
+ this.throttledTokensFetch(skipNotifications);
+
+ if (force) {
+ this.throttledTokensFetch.flush();
+ }
+ }
+
+ fetchBalances (options) {
+ const { skipNotifications = false, force = false } = options;
+ const { syncing } = this._store.getState().nodeStatus;
+
+ // If syncing, only retrieve balances once every
+ // few seconds
+ if (syncing) {
+ this.shortThrottledFetch.cancel();
+ this.longThrottledFetch(skipNotifications);
+
+ if (force) {
+ this.longThrottledFetch.flush();
+ }
+
+ return;
+ }
+
+ this.longThrottledFetch.cancel();
+ this.shortThrottledFetch(skipNotifications);
+
+ if (force) {
+ this.shortThrottledFetch.flush();
+ }
+ }
+
+ _fetchBalances (skipNotifications = false) {
+ this._store.dispatch(fetchBalances(null, skipNotifications));
+ }
+
+ _fetchTokens (skipNotifications = false) {
+ this._store.dispatch(fetchTokensBalances(null, null, skipNotifications));
}
getTokenRegistry () {
return Contracts.get().tokenReg.getContract();
}
- loadTokens () {
- this
+ _loadTokens (options = {}) {
+ return this
.getTokenRegistry()
.then((tokenreg) => {
+ this._tokenreg = tokenreg;
+
this._store.dispatch(setTokenReg(tokenreg));
- this._store.dispatch(loadTokens());
+ this._store.dispatch(loadTokens(options));
return this.attachToTokens(tokenreg);
})
@@ -133,7 +294,7 @@ export default class Balances {
}
attachToNewToken (tokenreg) {
- if (this._tokenregSubId) {
+ if (this._tokenregSID) {
return Promise.resolve();
}
@@ -149,13 +310,13 @@ export default class Balances {
this.handleTokensLogs(logs);
})
- .then((tokenregSubId) => {
- this._tokenregSubId = tokenregSubId;
+ .then((tokenregSID) => {
+ this._tokenregSID = tokenregSID;
});
}
attachToTokenMetaChange (tokenreg) {
- if (this._tokenregMetaSubId) {
+ if (this._tokenMetaSID) {
return Promise.resolve();
}
@@ -172,8 +333,8 @@ export default class Balances {
this.handleTokensLogs(logs);
})
- .then((tokenregMetaSubId) => {
- this._tokenregMetaSubId = tokenregMetaSubId;
+ .then((tokenMetaSID) => {
+ this._tokenMetaSID = tokenMetaSID;
});
}
diff --git a/js/src/redux/providers/balancesActions.js b/js/src/redux/providers/balancesActions.js
index 56a2ebafd..932ddfb8d 100644
--- a/js/src/redux/providers/balancesActions.js
+++ b/js/src/redux/providers/balancesActions.js
@@ -23,18 +23,27 @@ import { setAddressImage } from './imagesActions';
import * as ABIS from '~/contracts/abi';
import { notifyTransaction } from '~/util/notifications';
+import { LOG_KEYS, getLogger } from '~/config';
import imagesEthereum from '../../../assets/images/contracts/ethereum-black-64x64.png';
+const log = getLogger(LOG_KEYS.Balances);
+
const ETH = {
name: 'Ethereum',
tag: 'ETH',
image: imagesEthereum
};
-function setBalances (_balances) {
+function setBalances (_balances, skipNotifications = false) {
return (dispatch, getState) => {
const state = getState();
+ const currentTokens = Object.values(state.balances.tokens || {});
+ const currentTags = [ 'eth' ]
+ .concat(currentTokens.map((token) => token.tag))
+ .filter((tag) => tag)
+ .map((tag) => tag.toLowerCase());
+
const accounts = state.personal.accounts;
const nextBalances = _balances;
const prevBalances = state.balances.balances;
@@ -48,38 +57,55 @@ function setBalances (_balances) {
const balance = Object.assign({}, balances[address]);
const { tokens, txCount = balance.txCount } = nextBalances[address];
- const nextTokens = balance.tokens.slice();
- tokens.forEach((t) => {
- const { token, value } = t;
- const { tag } = token;
+ const prevTokens = balance.tokens.slice();
+ const nextTokens = [];
- const tokenIndex = nextTokens.findIndex((tok) => tok.token.tag === tag);
+ currentTags
+ .forEach((tag) => {
+ const prevToken = prevTokens.find((tok) => tok.token.tag.toLowerCase() === tag);
+ const nextToken = tokens.find((tok) => tok.token.tag.toLowerCase() === tag);
- if (tokenIndex === -1) {
- nextTokens.push({
- token,
- value
- });
- } else {
- const oldValue = nextTokens[tokenIndex].value;
+ // 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 (oldValue.lt(value) && accounts[address]) {
+ if (prevValue.lt(value) && accounts[address] && !skipNotifications) {
const account = accounts[address];
- const txValue = value.minus(oldValue);
+ const txValue = value.minus(prevValue);
const redirectToAccount = () => {
- const route = `/account/${account.address}`;
+ const route = `/accounts/${account.address}`;
dispatch(push(route));
};
notifyTransaction(account, token, txValue, redirectToAccount);
}
- nextTokens[tokenIndex] = { token, value };
- }
- });
+ return nextTokens.push({
+ ...prevToken,
+ value
+ });
+ });
balances[address] = { txCount: txCount || new BigNumber(0), tokens: nextTokens };
});
@@ -123,7 +149,9 @@ export function setTokenImage (tokenAddress, image) {
};
}
-export function loadTokens () {
+export function loadTokens (options = {}) {
+ log.debug('loading tokens', Object.keys(options).length ? options : '');
+
return (dispatch, getState) => {
const { tokenreg } = getState().balances;
@@ -131,7 +159,7 @@ export function loadTokens () {
.call()
.then((numTokens) => {
const tokenIds = range(numTokens.toNumber());
- dispatch(fetchTokens(tokenIds));
+ dispatch(fetchTokens(tokenIds, options));
})
.catch((error) => {
console.warn('balances::loadTokens', error);
@@ -139,8 +167,9 @@ export function loadTokens () {
};
}
-export function fetchTokens (_tokenIds) {
+export function fetchTokens (_tokenIds, options = {}) {
const tokenIds = uniq(_tokenIds || []);
+
return (dispatch, getState) => {
const { api, images, balances } = getState();
const { tokenreg } = balances;
@@ -161,8 +190,9 @@ export function fetchTokens (_tokenIds) {
dispatch(setAddressImage(address, image, true));
});
+ log.debug('fetched token', tokens);
dispatch(setTokens(tokens));
- dispatch(fetchBalances());
+ dispatch(updateTokensFilter(null, null, options));
})
.catch((error) => {
console.warn('balances::fetchTokens', error);
@@ -170,7 +200,7 @@ export function fetchTokens (_tokenIds) {
};
}
-export function fetchBalances (_addresses) {
+export function fetchBalances (_addresses, skipNotifications = false) {
return (dispatch, getState) => {
const { api, personal } = getState();
const { visibleAccounts, accounts } = personal;
@@ -192,8 +222,7 @@ export function fetchBalances (_addresses) {
balances[addr] = accountsBalances[idx];
});
- dispatch(setBalances(balances));
- updateTokensFilter(addresses)(dispatch, getState);
+ dispatch(setBalances(balances, skipNotifications));
})
.catch((error) => {
console.warn('balances::fetchBalances', error);
@@ -201,7 +230,7 @@ export function fetchBalances (_addresses) {
};
}
-export function updateTokensFilter (_addresses, _tokens) {
+export function updateTokensFilter (_addresses, _tokens, options = {}) {
return (dispatch, getState) => {
const { api, balances, personal } = getState();
const { visibleAccounts, accounts } = personal;
@@ -214,27 +243,32 @@ export function updateTokensFilter (_addresses, _tokens) {
const tokenAddresses = tokens.map((t) => t.address).sort();
if (tokensFilter.filterFromId || tokensFilter.filterToId) {
+ // Has the tokens addresses changed (eg. a network change)
const sameTokens = isEqual(tokenAddresses, tokensFilter.tokenAddresses);
- const sameAddresses = isEqual(addresses, tokensFilter.addresses);
- if (sameTokens && sameAddresses) {
+ // Addresses that are not in the current filter (omit those
+ // that the filter includes)
+ const newAddresses = addresses.filter((address) => !tokensFilter.addresses.includes(address));
+
+ // If no new addresses and the same tokens, don't change the filter
+ if (sameTokens && newAddresses.length === 0) {
+ log.debug('no need to update token filter', addresses, tokenAddresses, tokensFilter);
return queryTokensFilter(tokensFilter)(dispatch, getState);
}
}
- let promise = Promise.resolve();
+ log.debug('updating the token filter', addresses, tokenAddresses);
+ const promises = [];
if (tokensFilter.filterFromId) {
- promise = promise.then(() => api.eth.uninstallFilter(tokensFilter.filterFromId));
+ promises.push(api.eth.uninstallFilter(tokensFilter.filterFromId));
}
if (tokensFilter.filterToId) {
- promise = promise.then(() => api.eth.uninstallFilter(tokensFilter.filterToId));
+ promises.push(api.eth.uninstallFilter(tokensFilter.filterToId));
}
- if (tokenAddresses.length === 0 || addresses.length === 0) {
- return promise;
- }
+ const promise = Promise.all(promises);
const TRANSFER_SIGNATURE = api.util.sha3('Transfer(address,address,uint256)');
const topicsFrom = [ TRANSFER_SIGNATURE, addresses, null ];
@@ -269,8 +303,10 @@ export function updateTokensFilter (_addresses, _tokens) {
addresses, tokenAddresses
};
+ const { skipNotifications } = options;
+
dispatch(setTokensFilter(nextTokensFilter));
- fetchTokensBalances(addresses, tokens)(dispatch, getState);
+ fetchTokensBalances(addresses, tokens, skipNotifications)(dispatch, getState);
})
.catch((error) => {
console.warn('balances::updateTokensFilter', error);
@@ -326,7 +362,7 @@ export function queryTokensFilter (tokensFilter) {
};
}
-export function fetchTokensBalances (_addresses = null, _tokens = null) {
+export function fetchTokensBalances (_addresses = null, _tokens = null, skipNotifications = false) {
return (dispatch, getState) => {
const { api, personal, balances } = getState();
const { visibleAccounts, accounts } = personal;
@@ -348,7 +384,7 @@ export function fetchTokensBalances (_addresses = null, _tokens = null) {
balances[addr] = tokensBalances[idx];
});
- dispatch(setBalances(balances));
+ dispatch(setBalances(balances, skipNotifications));
})
.catch((error) => {
console.warn('balances::fetchTokensBalances', error);
diff --git a/js/src/redux/providers/chainMiddleware.js b/js/src/redux/providers/chainMiddleware.js
index 77c757da6..62c10bcb2 100644
--- a/js/src/redux/providers/chainMiddleware.js
+++ b/js/src/redux/providers/chainMiddleware.js
@@ -14,6 +14,7 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see .
+import BalancesProvider from './balances';
import { showSnackbar } from './snackbarActions';
import { DEFAULT_NETCHAIN } from './statusReducer';
@@ -29,6 +30,11 @@ export default class ChainMiddleware {
if (newChain !== nodeStatus.netChain && nodeStatus.netChain !== DEFAULT_NETCHAIN) {
store.dispatch(showSnackbar(`Switched to ${newChain}. Please reload the page.`, 60000));
+
+ // Fetch the new balances without notifying the user of any change
+ BalancesProvider.get(store).fetchAllBalances({
+ changedNetwork: true
+ });
}
}
}
diff --git a/js/src/redux/providers/chainMiddleware.spec.js b/js/src/redux/providers/chainMiddleware.spec.js
index ed2d5eca6..2c0a51602 100644
--- a/js/src/redux/providers/chainMiddleware.spec.js
+++ b/js/src/redux/providers/chainMiddleware.spec.js
@@ -16,13 +16,18 @@
import sinon from 'sinon';
+import Contracts from '~/contracts';
import { initialState as defaultNodeStatusState } from './statusReducer';
import ChainMiddleware from './chainMiddleware';
+import { createWsApi } from '~/../test/e2e/ethapi';
let middleware;
let next;
let store;
+const api = createWsApi();
+Contracts.create(api);
+
function createMiddleware (collection = {}) {
middleware = new ChainMiddleware().toMiddleware();
next = sinon.stub();
@@ -30,6 +35,7 @@ function createMiddleware (collection = {}) {
dispatch: sinon.stub(),
getState: () => {
return {
+ api: api,
nodeStatus: Object.assign({}, defaultNodeStatusState, collection)
};
}
diff --git a/js/src/redux/providers/personalActions.js b/js/src/redux/providers/personalActions.js
index d16e722bf..27cc47c27 100644
--- a/js/src/redux/providers/personalActions.js
+++ b/js/src/redux/providers/personalActions.js
@@ -16,7 +16,8 @@
import { isEqual, intersection } from 'lodash';
-import { fetchBalances } from './balancesActions';
+import BalancesProvider from './balances';
+import { updateTokensFilter } from './balancesActions';
import { attachWallets } from './walletActions';
import Contract from '~/api/contract';
@@ -98,7 +99,10 @@ export function personalAccountsInfo (accountsInfo) {
dispatch(_personalAccountsInfo(data));
dispatch(attachWallets(wallets));
- dispatch(fetchBalances());
+
+ BalancesProvider.get().fetchAllBalances({
+ force: true
+ });
})
.catch((error) => {
console.warn('personalAccountsInfo', error);
@@ -130,6 +134,18 @@ export function setVisibleAccounts (addresses) {
}
dispatch(_setVisibleAccounts(addresses));
- dispatch(fetchBalances(addresses));
+
+ // Don't update the balances if no new addresses displayed
+ if (addresses.length === 0) {
+ return;
+ }
+
+ // Update the Tokens filter to take into account the new
+ // addresses
+ dispatch(updateTokensFilter());
+
+ BalancesProvider.get().fetchBalances({
+ force: true
+ });
};
}
diff --git a/js/src/redux/providers/status.js b/js/src/redux/providers/status.js
index 6d0e24c6b..e419d78f8 100644
--- a/js/src/redux/providers/status.js
+++ b/js/src/redux/providers/status.js
@@ -14,9 +14,15 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see .
+import BalancesProvider from './balances';
import { statusBlockNumber, statusCollection, statusLogs } from './statusActions';
import { isEqual } from 'lodash';
+import { LOG_KEYS, getLogger } from '~/config';
+
+const log = getLogger(LOG_KEYS.Signer);
+let instance = null;
+
export default class Status {
constructor (store, api) {
this._api = api;
@@ -27,20 +33,90 @@ export default class Status {
this._longStatus = {};
this._minerSettings = {};
- this._longStatusTimeoutId = null;
+ this._timeoutIds = {};
+ this._blockNumberSubscriptionId = null;
this._timestamp = Date.now();
+
+ // On connecting, stop all subscriptions
+ api.on('connecting', this.stop, this);
+
+ // On connected, start the subscriptions
+ api.on('connected', this.start, this);
+
+ // On disconnected, stop all subscriptions
+ api.on('disconnected', this.stop, this);
+
+ this.updateApiStatus();
+ }
+
+ static instantiate (store, api) {
+ if (!instance) {
+ instance = new Status(store, api);
+ }
+
+ return instance;
}
start () {
- this._subscribeBlockNumber();
- this._pollStatus();
- this._pollLongStatus();
- this._pollLogs();
+ log.debug('status::start');
+
+ Promise
+ .all([
+ this._subscribeBlockNumber(),
+
+ this._pollLogs(),
+ this._pollLongStatus(),
+ this._pollStatus()
+ ])
+ .then(() => {
+ return BalancesProvider.start();
+ });
}
- _subscribeBlockNumber () {
- this._api
+ stop () {
+ log.debug('status::stop');
+
+ const promises = [];
+
+ if (this._blockNumberSubscriptionId) {
+ const promise = this._api
+ .unsubscribe(this._blockNumberSubscriptionId)
+ .then(() => {
+ this._blockNumberSubscriptionId = null;
+ });
+
+ promises.push(promise);
+ }
+
+ Object.values(this._timeoutIds).forEach((timeoutId) => {
+ clearTimeout(timeoutId);
+ });
+
+ const promise = BalancesProvider.stop();
+ promises.push(promise);
+
+ return Promise.all(promises)
+ .then(() => true)
+ .catch((error) => {
+ console.error('status::stop', error);
+ return true;
+ })
+ .then(() => this.updateApiStatus());
+ }
+
+ updateApiStatus () {
+ const apiStatus = this.getApiStatus();
+ log.debug('status::updateApiStatus', apiStatus);
+
+ if (!isEqual(apiStatus, this._apiStatus)) {
+ this._store.dispatch(statusCollection(apiStatus));
+ this._apiStatus = apiStatus;
+ }
+ }
+
+ _subscribeBlockNumber = () => {
+ return this._api
.subscribe('eth_blockNumber', (error, blockNumber) => {
if (error) {
return;
@@ -51,6 +127,10 @@ export default class Status {
this._api.eth
.getBlockByNumber(blockNumber)
.then((block) => {
+ if (!block) {
+ return;
+ }
+
this._store.dispatch(statusCollection({
blockTimestamp: block.timestamp,
gasLimit: block.gasLimit
@@ -59,6 +139,9 @@ export default class Status {
.catch((error) => {
console.warn('status._subscribeBlockNumber', 'getBlockByNumber', error);
});
+ })
+ .then((blockNumberSubscriptionId) => {
+ this._blockNumberSubscriptionId = blockNumberSubscriptionId;
});
}
@@ -72,11 +155,7 @@ export default class Status {
.catch(() => false);
}
- _pollStatus = () => {
- const nextTimeout = (timeout = 1000) => {
- setTimeout(() => this._pollStatus(), timeout);
- };
-
+ getApiStatus = () => {
const { isConnected, isConnecting, needsToken, secureToken } = this._api;
const apiStatus = {
@@ -86,19 +165,23 @@ export default class Status {
secureToken
};
- const gotConnected = !this._apiStatus.isConnected && apiStatus.isConnected;
+ return apiStatus;
+ }
- if (gotConnected) {
- this._pollLongStatus();
- }
+ _pollStatus = () => {
+ const nextTimeout = (timeout = 1000) => {
+ if (this._timeoutIds.status) {
+ clearTimeout(this._timeoutIds.status);
+ }
- if (!isEqual(apiStatus, this._apiStatus)) {
- this._store.dispatch(statusCollection(apiStatus));
- this._apiStatus = apiStatus;
- }
+ this._timeoutIds.status = setTimeout(() => this._pollStatus(), timeout);
+ };
- if (!isConnected) {
- return nextTimeout(250);
+ this.updateApiStatus();
+
+ if (!this._api.isConnected) {
+ nextTimeout(250);
+ return Promise.resolve();
}
const { refreshStatus } = this._store.getState().nodeStatus;
@@ -110,7 +193,7 @@ export default class Status {
statusPromises.push(this._api.eth.hashrate());
}
- Promise
+ return Promise
.all(statusPromises)
.then(([ syncing, ...statusResults ]) => {
const status = statusResults.length === 0
@@ -125,11 +208,11 @@ export default class Status {
this._store.dispatch(statusCollection(status));
this._status = status;
}
-
- nextTimeout();
})
.catch((error) => {
console.error('_pollStatus', error);
+ })
+ .then(() => {
nextTimeout();
});
}
@@ -140,7 +223,7 @@ export default class Status {
* from the UI
*/
_pollMinerSettings = () => {
- Promise
+ return Promise
.all([
this._api.eth.coinbase(),
this._api.parity.extraData(),
@@ -175,21 +258,21 @@ export default class Status {
*/
_pollLongStatus = () => {
if (!this._api.isConnected) {
- return;
+ return Promise.resolve();
}
const nextTimeout = (timeout = 30000) => {
- if (this._longStatusTimeoutId) {
- clearTimeout(this._longStatusTimeoutId);
+ if (this._timeoutIds.longStatus) {
+ clearTimeout(this._timeoutIds.longStatus);
}
- this._longStatusTimeoutId = setTimeout(this._pollLongStatus, timeout);
+ this._timeoutIds.longStatus = setTimeout(() => this._pollLongStatus(), timeout);
};
// Poll Miner settings just in case
- this._pollMinerSettings();
+ const minerPromise = this._pollMinerSettings();
- Promise
+ const mainPromise = Promise
.all([
this._api.parity.netPeers(),
this._api.web3.clientVersion(),
@@ -225,21 +308,31 @@ export default class Status {
})
.catch((error) => {
console.error('_pollLongStatus', error);
+ })
+ .then(() => {
+ nextTimeout(60000);
});
- nextTimeout(60000);
+ return Promise.all([ minerPromise, mainPromise ]);
}
_pollLogs = () => {
- const nextTimeout = (timeout = 1000) => setTimeout(this._pollLogs, timeout);
+ const nextTimeout = (timeout = 1000) => {
+ if (this._timeoutIds.logs) {
+ clearTimeout(this._timeoutIds.logs);
+ }
+
+ this._timeoutIds.logs = setTimeout(this._pollLogs, timeout);
+ };
+
const { devLogsEnabled } = this._store.getState().nodeStatus;
if (!devLogsEnabled) {
nextTimeout();
- return;
+ return Promise.resolve();
}
- Promise
+ return Promise
.all([
this._api.parity.devLogs(),
this._api.parity.devLogsLevels()
@@ -249,11 +342,12 @@ export default class Status {
devLogs: devLogs.slice(-1024),
devLogsLevels
}));
- nextTimeout();
})
.catch((error) => {
console.error('_pollLogs', error);
- nextTimeout();
+ })
+ .then(() => {
+ return nextTimeout();
});
}
}
diff --git a/js/src/redux/store.js b/js/src/redux/store.js
index 9924aa461..132375784 100644
--- a/js/src/redux/store.js
+++ b/js/src/redux/store.js
@@ -38,10 +38,10 @@ export default function (api, browserHistory) {
const middleware = initMiddleware(api, browserHistory);
const store = applyMiddleware(...middleware)(storeCreation)(reducers);
- new BalancesProvider(store, api).start();
+ BalancesProvider.instantiate(store, api);
+ StatusProvider.instantiate(store, api);
new PersonalProvider(store, api).start();
new SignerProvider(store, api).start();
- new StatusProvider(store, api).start();
store.dispatch(loadWallet(api));
setupWorker(store);
diff --git a/js/src/secureApi.js b/js/src/secureApi.js
index 445151c69..b39ecaf3b 100644
--- a/js/src/secureApi.js
+++ b/js/src/secureApi.js
@@ -17,133 +17,34 @@
import { uniq } from 'lodash';
import Api from './api';
+import { LOG_KEYS, getLogger } from '~/config';
+const log = getLogger(LOG_KEYS.Signer);
const sysuiToken = window.localStorage.getItem('sysuiToken');
export default class SecureApi extends Api {
+ _isConnecting = false;
+ _needsToken = false;
+ _tokens = [];
+
+ _dappsInterface = null;
+ _dappsPort = 8080;
+ _signerPort = 8180;
+
constructor (url, nextToken) {
- super(new Api.Transport.Ws(url, sysuiToken, false));
+ const transport = new Api.Transport.Ws(url, sysuiToken, false);
+ super(transport);
this._url = url;
- this._isConnecting = true;
- this._needsToken = false;
- this._dappsPort = 8080;
- this._dappsInterface = null;
- this._signerPort = 8180;
-
- // Try tokens from localstorage, then from hash
+ // Try tokens from localstorage, from hash and 'initial'
this._tokens = uniq([sysuiToken, nextToken, 'initial'])
.filter((token) => token)
.map((token) => ({ value: token, tried: false }));
- this._tryNextToken();
- }
-
- saveToken = () => {
- window.localStorage.setItem('sysuiToken', this._transport.token);
- // DEBUG: console.log('SecureApi:saveToken', this._transport.token);
- }
-
- /**
- * Returns a Promise that gets resolved with
- * a boolean: `true` if the node is up, `false`
- * otherwise
- */
- _checkNodeUp () {
- const url = this._url.replace(/wss?/, 'http');
- return fetch(url, { method: 'HEAD' })
- .then(
- (r) => r.status === 200,
- () => false
- )
- .catch(() => false);
- }
-
- _setManual () {
- this._needsToken = true;
- this._isConnecting = false;
- }
-
- _tryNextToken () {
- const nextTokenIndex = this._tokens.findIndex((t) => !t.tried);
-
- if (nextTokenIndex < 0) {
- return this._setManual();
- }
-
- const nextToken = this._tokens[nextTokenIndex];
- nextToken.tried = true;
-
- this.updateToken(nextToken.value);
- }
-
- _followConnection = () => {
- const token = this.transport.token;
-
- return this
- .transport
- .connect()
- .then(() => {
- if (token === 'initial') {
- return this.signer
- .generateAuthorizationToken()
- .then((token) => {
- return this.updateToken(token);
- })
- .catch((e) => console.error(e));
- }
-
- this.connectSuccess();
- return true;
- })
- .catch((e) => {
- this
- ._checkNodeUp()
- .then((isNodeUp) => {
- // Try again in a few...
- if (!isNodeUp) {
- this._isConnecting = false;
- const timeout = this.transport.retryTimeout;
-
- window.setTimeout(() => {
- this._followConnection();
- }, timeout);
-
- return;
- }
-
- this._tryNextToken();
- return false;
- });
- });
- }
-
- connectSuccess () {
- this._isConnecting = false;
- this._needsToken = false;
-
- this.saveToken();
-
- Promise
- .all([
- this.parity.dappsPort(),
- this.parity.dappsInterface(),
- this.parity.signerPort()
- ])
- .then(([dappsPort, dappsInterface, signerPort]) => {
- this._dappsPort = dappsPort.toNumber();
- this._dappsInterface = dappsInterface;
- this._signerPort = signerPort.toNumber();
- });
-
- // DEBUG: console.log('SecureApi:connectSuccess', this._transport.token);
- }
-
- updateToken (token) {
- this._transport.updateToken(token.replace(/[^a-zA-Z0-9]/g, ''), false);
- return this._followConnection();
- // DEBUG: console.log('SecureApi:updateToken', this._transport.token, connectState);
+ // When the transport is closed, try to reconnect
+ transport.on('close', this.connect, this);
+ this.connect();
}
get dappsPort () {
@@ -151,17 +52,19 @@ export default class SecureApi extends Api {
}
get dappsUrl () {
- let hostname;
+ return `http://${this.hostname}:${this.dappsPort}`;
+ }
+ get hostname () {
if (window.location.hostname === 'home.parity') {
- hostname = 'dapps.parity';
- } else if (!this._dappsInterface || this._dappsInterface === '0.0.0.0') {
- hostname = window.location.hostname;
- } else {
- hostname = this._dappsInterface;
+ return 'dapps.parity';
}
- return `http://${hostname}:${this._dappsPort}`;
+ if (!this._dappsInterface || this._dappsInterface === '0.0.0.0') {
+ return window.location.hostname;
+ }
+
+ return this._dappsInterface;
}
get signerPort () {
@@ -183,4 +86,279 @@ export default class SecureApi extends Api {
get secureToken () {
return this._transport.token;
}
+
+ connect () {
+ if (this._isConnecting) {
+ return;
+ }
+
+ log.debug('trying to connect...');
+
+ this._isConnecting = true;
+
+ this.emit('connecting');
+
+ // Reset the tested Tokens
+ this._resetTokens();
+
+ // Try to connect
+ return this._connect()
+ .then((connected) => {
+ this._isConnecting = false;
+
+ if (connected) {
+ const token = this.secureToken;
+ log.debug('got connected ; saving token', token);
+
+ // Save the sucessful token
+ this._saveToken(token);
+ this._needsToken = false;
+
+ // Emit the connected event
+ return this.emit('connected');
+ }
+
+ // If not connected, we need a new token
+ log.debug('needs a token');
+ this._needsToken = true;
+
+ return this.emit('disconnected');
+ })
+ .catch((error) => {
+ this._isConnecting = false;
+
+ log.debug('emitting "disconnected"');
+ this.emit('disconnected');
+ console.error('unhandled error in secureApi', error);
+ });
+ }
+
+ /**
+ * Returns a Promise that gets resolved with
+ * a boolean: `true` if the node is up, `false`
+ * otherwise (HEAD request to the Node)
+ */
+ isNodeUp () {
+ const url = this._url.replace(/wss?/, 'http');
+ return fetch(url, { method: 'HEAD' })
+ .then(
+ (r) => r.status === 200,
+ () => false
+ )
+ .catch(() => false);
+ }
+
+ /**
+ * Update the given token, ie. add it to the token
+ * list, and then try to connect (if not already connecting)
+ */
+ updateToken (_token) {
+ const token = this._sanitiseToken(_token);
+ log.debug('updating token', token);
+
+ // Update the tokens list: put the new one on first position
+ this._tokens = [ { value: token, tried: false } ].concat(this._tokens);
+
+ // Try to connect with the new token added
+ return this.connect();
+ }
+
+ /**
+ * Try to connect to the Node with the next Token in
+ * the list
+ */
+ _connect () {
+ log.debug('trying next token');
+
+ // Get the first not-tried token
+ const nextToken = this._getNextToken();
+
+ // If no more tokens to try, user has to enter a new one
+ if (!nextToken) {
+ return Promise.resolve(false);
+ }
+
+ nextToken.tried = true;
+
+ return this._connectWithToken(nextToken.value)
+ .then((validToken) => {
+ // If not valid, try again with the next token in the list
+ if (!validToken) {
+ return this._connect();
+ }
+
+ // If correct and valid token, wait until the Node is ready
+ // and resolve as connected
+ return this._waitUntilNodeReady()
+ .then(() => this._fetchSettings())
+ .then(() => true);
+ })
+ .catch((error) => {
+ log.error('unkown error in _connect', error);
+ return false;
+ });
+ }
+
+ /**
+ * Connect with the given token.
+ * It returns a Promise that gets resolved
+ * with `validToken` as argument, whether the given token
+ * is valid or not
+ */
+ _connectWithToken (_token) {
+ // Sanitize the token first
+ const token = this._sanitiseToken(_token);
+
+ // Update the token in the transport layer
+ this.transport.updateToken(token, false);
+ log.debug('connecting with token', token);
+
+ return this.transport.connect()
+ .then(() => {
+ log.debug('connected with', token);
+
+ if (token === 'initial') {
+ return this._generateAuthorizationToken();
+ }
+
+ // The token is valid !
+ return true;
+ })
+ .catch((error) => {
+ // Log if it's not a close error (ie. wrong token)
+ if (error && error.type !== 'close') {
+ log.debug('did not connect ; error', error);
+ }
+
+ // Check if the Node is up
+ return this.isNodeUp()
+ .then((isNodeUp) => {
+ // If it's not up, try again in a few...
+ if (!isNodeUp) {
+ const timeout = this.transport.retryTimeout;
+
+ log.debug('node is not up ; will try again in', timeout, 'ms');
+
+ return new Promise((resolve, reject) => {
+ window.setTimeout(() => {
+ this._connectWithToken(token).then(resolve).catch(reject);
+ }, timeout);
+ });
+ }
+
+ // The token is invalid
+ log.debug('tried with a wrong token', token);
+ return false;
+ });
+ });
+ }
+
+ /**
+ * Retrieve the correct ports from the Node
+ */
+ _fetchSettings () {
+ return Promise
+ .all([
+ this.parity.dappsPort(),
+ this.parity.dappsInterface(),
+ this.parity.signerPort()
+ ])
+ .then(([dappsPort, dappsInterface, signerPort]) => {
+ this._dappsPort = dappsPort.toNumber();
+ this._dappsInterface = dappsInterface;
+ this._signerPort = signerPort.toNumber();
+ });
+ }
+
+ /**
+ * Try to generate an Authorization Token.
+ * Then try to connect with the new token.
+ */
+ _generateAuthorizationToken () {
+ return this.signer
+ .generateAuthorizationToken()
+ .then((token) => this._connectWithToken(token));
+ }
+
+ /**
+ * Get the next token to try, if any left
+ */
+ _getNextToken () {
+ // Get the first not-tried token
+ const nextTokenIndex = this._tokens.findIndex((t) => !t.tried);
+
+ // If no more tokens to try, user has to enter a new one
+ if (nextTokenIndex < 0) {
+ return null;
+ }
+
+ const nextToken = this._tokens[nextTokenIndex];
+ return nextToken;
+ }
+
+ _resetTokens () {
+ this._tokens = this._tokens.map((token) => ({
+ ...token,
+ tried: false
+ }));
+ }
+
+ _sanitiseToken (token) {
+ return token.replace(/[^a-zA-Z0-9]/g, '');
+ }
+
+ _saveToken (token) {
+ window.localStorage.setItem('sysuiToken', token);
+ }
+
+ /**
+ * Promise gets resolved when the node is up
+ * and running (it might take some time before
+ * the node is actually ready even when the client
+ * is connected).
+ *
+ * We check that the `parity_enode` RPC calls
+ * returns successfully
+ */
+ _waitUntilNodeReady (_timeleft) {
+ // Default timeout to 30 seconds
+ const timeleft = Number.isFinite(_timeleft)
+ ? _timeleft
+ : 30 * 1000;
+
+ // After timeout, just resolve the promise...
+ if (timeleft <= 0) {
+ console.warn('node is still not ready after 30 seconds...');
+ return Promise.resolve(true);
+ }
+
+ const start = Date.now();
+
+ return this
+ .parity.enode()
+ .then(() => true)
+ .catch((error) => {
+ if (!error) {
+ return true;
+ }
+
+ if (error.type !== 'NETWORK_DISABLED') {
+ throw error;
+ }
+
+ // Timeout between 250ms and 750ms
+ const timeout = Math.floor(250 + (500 * Math.random()));
+
+ log.debug('waiting until node is ready', 'retry in', timeout, 'ms');
+
+ // Retry in a few...
+ return new Promise((resolve, reject) => {
+ window.setTimeout(() => {
+ const duration = Date.now() - start;
+
+ this._waitUntilNodeReady(timeleft - duration).then(resolve).catch(reject);
+ }, timeout);
+ });
+ });
+ }
}
diff --git a/js/src/util/notifications.js b/js/src/util/notifications.js
index 488ac2daf..b8bbfe08a 100644
--- a/js/src/util/notifications.js
+++ b/js/src/util/notifications.js
@@ -16,7 +16,6 @@
import Push from 'push.js';
import BigNumber from 'bignumber.js';
-import { noop } from 'lodash';
import { fromWei } from '~/api/util/wei';
@@ -33,13 +32,34 @@ export function notifyTransaction (account, token, _value, onClick) {
? ethereumIcon
: (token.image || unkownIcon);
- Push.create(`${name}`, {
- body: `You just received ${value.toFormat()} ${token.tag.toUpperCase()}`,
- icon: {
- x16: icon,
- x32: icon
- },
- timeout: 20000,
- onClick: onClick || noop
- });
+ let _notification = null;
+
+ Push
+ .create(`${name}`, {
+ body: `You just received ${value.toFormat(3)} ${token.tag.toUpperCase()}`,
+ icon: {
+ x16: icon,
+ x32: icon
+ },
+ timeout: 20000,
+ onClick: () => {
+ // Focus on the UI
+ try {
+ window.focus();
+ } catch (e) {}
+
+ if (onClick && typeof onClick === 'function') {
+ onClick();
+ }
+
+ // Close the notification
+ if (_notification) {
+ _notification.close();
+ _notification = null;
+ }
+ }
+ })
+ .then((notification) => {
+ _notification = notification;
+ });
}
diff --git a/js/src/views/Application/Status/status.js b/js/src/views/Application/Status/status.js
index 1e2cd4d41..54dad960e 100644
--- a/js/src/views/Application/Status/status.js
+++ b/js/src/views/Application/Status/status.js
@@ -59,6 +59,10 @@ class Status extends Component {
renderConsensus () {
const { upgradeStore } = this.props;
+ if (!upgradeStore || !upgradeStore.consensusCapability) {
+ return null;
+ }
+
if (upgradeStore.consensusCapability === 'capable') {
return (
@@ -67,7 +71,9 @@ class Status extends Component {
defaultMessage='Capable' />
);
- } else if (upgradeStore.consensusCapability.capableUntil) {
+ }
+
+ if (upgradeStore.consensusCapability.capableUntil) {
return (
);
- } else if (upgradeStore.consensusCapability.incapableSince) {
+ }
+
+ if (upgradeStore.consensusCapability.incapableSince) {
return (
{
const log = LOG_KEYS[logKey];
- const logger = LogLevel.getLogger(log.path);
+ const logger = LogLevel.getLogger(log.key);
const level = logger.getLevel();
nextState[logKey] = { level, log };
@@ -133,11 +133,11 @@ export default class Parity extends Component {
return Object.keys(logLevels).map((logKey) => {
const { level, log } = logLevels[logKey];
- const { path, desc } = log;
+ const { key, desc } = log;
const onChange = (_, index) => {
const nextLevel = Object.values(selectValues)[index].value;
- LogLevel.getLogger(path).setLevel(nextLevel);
+ LogLevel.getLogger(key).setLevel(nextLevel);
this.loadLogLevels();
};
diff --git a/js/src/views/Status/components/MiningSettings/miningSettings.js b/js/src/views/Status/components/MiningSettings/miningSettings.js
index b65eed929..8cc04d45e 100644
--- a/js/src/views/Status/components/MiningSettings/miningSettings.js
+++ b/js/src/views/Status/components/MiningSettings/miningSettings.js
@@ -37,6 +37,14 @@ export default class MiningSettings extends Component {
const { nodeStatus } = this.props;
const { coinbase, defaultExtraData, extraData, gasFloorTarget, minGasPrice } = nodeStatus;
+ const extradata = extraData
+ ? decodeExtraData(extraData)
+ : '';
+
+ const defaultExtradata = defaultExtraData
+ ? decodeExtraData(defaultExtraData)
+ : '';
+
return (
@@ -53,9 +61,9 @@ export default class MiningSettings extends Component {
@@ -121,7 +127,7 @@ export default class Status extends Component {
allowCopy
readOnly
label='network port'
- value={ nodeStatus.netPort.toString() }
+ value={ netPort.toString() }
{ ...this._test('network-port') } />
@@ -146,7 +152,7 @@ export default class Status extends Component {
allowCopy
readOnly
label='rpc port'
- value={ rpcSettings.port.toString() }
+ value={ rpcPort.toString() }
{ ...this._test('rpc-port') } />
diff --git a/js/test/mockRpc.js b/js/test/mockRpc.js
index 13f752f8e..fef27de4b 100644
--- a/js/test/mockRpc.js
+++ b/js/test/mockRpc.js
@@ -47,8 +47,8 @@ export function mockHttp (requests) {
}
export function mockWs (requests) {
- const scope = { requests: 0, body: {} };
let mockServer = new MockWsServer(TEST_WS_URL);
+ const scope = { requests: 0, body: {}, server: mockServer };
scope.isDone = () => scope.requests === requests.length;
scope.stop = () => {