diff --git a/js/src/api/transport/ws/ws.js b/js/src/api/transport/ws/ws.js index a0b609e5da1..ef739ae137b 100644 --- a/js/src/api/transport/ws/ws.js +++ b/js/src/api/transport/ws/ws.js @@ -206,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/redux/providers/balances.js b/js/src/redux/providers/balances.js index 8d46e42d2c4..cb26f1356c7 100644 --- a/js/src/redux/providers/balances.js +++ b/js/src/redux/providers/balances.js @@ -21,51 +21,139 @@ 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; + + this._blockNumberSID = null; + this._accountsInfoSID = null; // Throttled `retrieveTokens` function // that gets called max once every 40s this.longThrottledFetch = throttle( this.fetchBalances, 40 * 1000, - { trailing: true } + { leading: true } ); this.shortThrottledFetch = throttle( this.fetchBalances, 2 * 1000, - { trailing: true } + { leading: true } ); // Fetch all tokens every 2 minutes this.throttledTokensFetch = throttle( this.fetchTokens, 60 * 1000, - { trailing: true } + { leading: true } ); + + // Unsubscribe previous instance if it exists + if (instance) { + Balances.stop(); + } + + instance = this; } - start () { - this.subscribeBlockNumber(); - this.subscribeAccountsInfo(); + static instantiate (store, api) { + return new Balances(store, api); + } + + static start () { + if (!instance) { + return Promise.reject('BalancesProvider has not been intiated yet'); + } + + const self = instance; + + // Unsubscribe from previous subscriptions + return Balances + .stop() + .then(() => { + const promises = [ + self.subscribeBlockNumber(), + self.subscribeAccountsInfo(), - this.loadTokens(); + self.loadTokens() + ]; + + 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); + } + + if (self._tokenreg) { + if (self._tokenregSID) { + const p = self._tokenreg + .unsubscribe(self._tokenregSID) + .then(() => { + self._tokenregSID = null; + }); + + promises.push(p); + } + + if (self._tokenMetaSID) { + const p = self._tokenreg + .unsubscribe(self._tokenMetaSID) + .then(() => { + self._tokenMetaSID = null; + }); + + promises.push(p); + } + } + + 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,31 +161,38 @@ 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); }); } + fetchAllBalances () { + 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(); + } + fetchBalances () { this._store.dispatch(fetchBalances()); } @@ -111,9 +206,11 @@ export default class Balances { } loadTokens () { - this + return this .getTokenRegistry() .then((tokenreg) => { + this._tokenreg = tokenreg; + this._store.dispatch(setTokenReg(tokenreg)); this._store.dispatch(loadTokens()); @@ -133,7 +230,7 @@ export default class Balances { } attachToNewToken (tokenreg) { - if (this._tokenregSubId) { + if (this._tokenregSID) { return Promise.resolve(); } @@ -149,13 +246,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 +269,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 e818d057b2f..30e56afc55d 100644 --- a/js/src/redux/providers/balancesActions.js +++ b/js/src/redux/providers/balancesActions.js @@ -162,7 +162,6 @@ export function fetchTokens (_tokenIds) { }); dispatch(setTokens(tokens)); - dispatch(fetchBalances()); }) .catch((error) => { console.warn('balances::fetchTokens', error); diff --git a/js/src/redux/providers/status.js b/js/src/redux/providers/status.js index a50dbb2b708..912e4852121 100644 --- a/js/src/redux/providers/status.js +++ b/js/src/redux/providers/status.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 { statusBlockNumber, statusCollection, statusLogs } from './statusActions'; import { isEqual } from 'lodash'; @@ -37,10 +38,7 @@ export default class Status { }); api.transport.on('close', () => { - const apiStatus = this.getApiStatus(); - this._apiStatus = apiStatus; - this._store.dispatch(statusCollection(apiStatus)); - + this.updateApiStatus(); this.start(); }); } @@ -51,26 +49,20 @@ export default class Status { this._api .connect() .then((connected) => { - // Get the API status, set connected to false - // before the long status is ok - const apiStatus = this.getApiStatus(); - apiStatus.isConnected = false; - - this._apiStatus = apiStatus; - this._store.dispatch(statusCollection(apiStatus)); + // Update the API Status + this.updateApiStatus(); if (connected) { + BalancesProvider.start(); this._subscribeBlockNumber(); - this._pollLongStatus(true); + + this._pollLogs(); + this._pollLongStatus(); + this._pollStatus(); } }); } - startPolling () { - this._pollLogs(); - this._pollStatus(); - } - stop () { if (this._blockNumberSubscriptionId) { this._api.unsubscribe(this._blockNumberSubscriptionId); @@ -80,6 +72,17 @@ export default class Status { Object.values(this._timeoutIds).forEach((timeoutId) => { clearTimeout(timeoutId); }); + + BalancesProvider.stop(); + } + + updateApiStatus () { + const apiStatus = this.getApiStatus(); + + if (!isEqual(apiStatus, this._apiStatus)) { + this._store.dispatch(statusCollection(apiStatus)); + this._apiStatus = apiStatus; + } } _subscribeBlockNumber = () => { @@ -144,21 +147,9 @@ export default class Status { this._timeoutIds.status = setTimeout(() => this._pollStatus(), timeout); }; - const { isConnected } = this._api; - const apiStatus = this.getApiStatus(); + this.updateApiStatus(); - const hasConnected = !this._apiStatus.isConnected && apiStatus.isConnected; - - if (hasConnected) { - this._pollLongStatus(hasConnected); - } - - if (!isEqual(apiStatus, this._apiStatus)) { - this._store.dispatch(statusCollection(apiStatus)); - this._apiStatus = apiStatus; - } - - if (!isConnected) { + if (!this._api.isConnected) { return nextTimeout(250); } @@ -234,17 +225,17 @@ export default class Status { * fetched every 30s just in case, and whenever * the client got reconnected. */ - _pollLongStatus = (hasConnected = false) => { + _pollLongStatus = () => { if (!this._api.isConnected) { return; } - const nextTimeout = (timeout = 30000, hasConnected = false) => { + const nextTimeout = (timeout = 30000) => { if (this._timeoutIds.longStatus) { clearTimeout(this._timeoutIds.longStatus); } - this._timeoutIds.longStatus = setTimeout(() => this._pollLongStatus(hasConnected), timeout); + this._timeoutIds.longStatus = setTimeout(() => this._pollLongStatus(), timeout); }; // Poll Miner settings just in case @@ -283,28 +274,12 @@ export default class Status { this._store.dispatch(statusCollection(longStatus)); this._longStatus = longStatus; } - - if (hasConnected) { - this.startPolling(); - } - - return false; }) .catch((error) => { - // Try again soon if just got reconnected (network might take some time - // to boot up) - if (hasConnected) { - nextTimeout(500, true); - return true; - } - console.error('_pollLongStatus', error); - return false; }) - .then((called) => { - if (!called) { - nextTimeout(60000); - } + .then(() => { + nextTimeout(60000); }); return Promise.all([ minerPromise, mainPromise ]); diff --git a/js/src/redux/store.js b/js/src/redux/store.js index 9924aa461c2..8d77c3b6bb4 100644 --- a/js/src/redux/store.js +++ b/js/src/redux/store.js @@ -38,7 +38,7 @@ 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); new PersonalProvider(store, api).start(); new SignerProvider(store, api).start(); new StatusProvider(store, api).start(); diff --git a/js/src/secureApi.js b/js/src/secureApi.js index b511debd9ab..352df1536c1 100644 --- a/js/src/secureApi.js +++ b/js/src/secureApi.js @@ -101,6 +101,36 @@ export default class SecureApi extends Api { .catch(() => false); } + /** + * 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 () { + return this + .parity.enode() + .then(() => true) + .catch((error) => { + if (!error) { + return true; + } + + if (error.type !== 'NETWORK_DISABLED') { + return false; + } + + return new Promise((resolve, reject) => { + window.setTimeout(() => { + this.waitUntilNodeReady().then(resolve).catch(reject); + }, 250); + }); + }); + } + _setManual () { this._needsToken = true; this._isConnecting = false; @@ -145,7 +175,9 @@ export default class SecureApi extends Api { }); } - return this.connectSuccess(token).then(() => true, () => true); + return this.waitUntilNodeReady().then(() => { + return this.connectSuccess(token).then(() => true, () => true); + }); }) .catch((e) => { log.debug('did not connect ; error', e);