Don't pop-up notifications after network switch (#4076)
* Better notifications * Don't pollute with notifs if switched networks * Better connection close/open events / No more notifs on change network * PR Grumbles * Add close and open events to HTTP // Add tests * Fix tests * WIP Signer Fix * Fix Signer // Better reconnection handling * PR Grumbles * PR Grumbles * Fixes wrong fetching of balances + Notifications * Secure API WIP * Updated Secure API Connection + Status * Linting * Linting * Updated Secure API Logic * Proper handling of token updates // Fixing poping notifications * PR Grumbles * PR Grumbles * Fixing tests
This commit is contained in:
parent
bc2ebdc564
commit
81beec1352
@ -14,6 +14,8 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import EventEmitter from 'eventemitter3';
|
||||||
|
|
||||||
import { Http, Ws } from './transport';
|
import { Http, Ws } from './transport';
|
||||||
import Contract from './contract';
|
import Contract from './contract';
|
||||||
|
|
||||||
@ -22,8 +24,10 @@ import Subscriptions from './subscriptions';
|
|||||||
import util from './util';
|
import util from './util';
|
||||||
import { isFunction } from './util/types';
|
import { isFunction } from './util/types';
|
||||||
|
|
||||||
export default class Api {
|
export default class Api extends EventEmitter {
|
||||||
constructor (transport) {
|
constructor (transport) {
|
||||||
|
super();
|
||||||
|
|
||||||
if (!transport || !isFunction(transport.execute)) {
|
if (!transport || !isFunction(transport.execute)) {
|
||||||
throw new Error('EthApi needs transport with execute() function defined');
|
throw new Error('EthApi needs transport with execute() function defined');
|
||||||
}
|
}
|
||||||
|
@ -51,14 +51,14 @@ export default class Http extends JsonRpcBase {
|
|||||||
|
|
||||||
return fetch(this._url, request)
|
return fetch(this._url, request)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
this._connected = false;
|
this._setDisconnected();
|
||||||
throw error;
|
throw error;
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
this._connected = true;
|
this._setConnected();
|
||||||
|
|
||||||
if (response.status !== 200) {
|
if (response.status !== 200) {
|
||||||
this._connected = false;
|
this._setDisconnected();
|
||||||
this.error(JSON.stringify({ status: response.status, statusText: response.statusText }));
|
this.error(JSON.stringify({ status: response.status, statusText: response.statusText }));
|
||||||
console.error(`${method}(${JSON.stringify(params)}): ${response.status}: ${response.statusText}`);
|
console.error(`${method}(${JSON.stringify(params)}): ${response.status}: ${response.statusText}`);
|
||||||
|
|
||||||
|
@ -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', () => {
|
describe('transport', () => {
|
||||||
const RESULT = ['this is some result'];
|
const RESULT = ['this is some result'];
|
||||||
|
|
||||||
|
@ -14,8 +14,12 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
export default class JsonRpcBase {
|
import EventEmitter from 'eventemitter3';
|
||||||
|
|
||||||
|
export default class JsonRpcBase extends EventEmitter {
|
||||||
constructor () {
|
constructor () {
|
||||||
|
super();
|
||||||
|
|
||||||
this._id = 1;
|
this._id = 1;
|
||||||
this._debug = false;
|
this._debug = false;
|
||||||
this._connected = false;
|
this._connected = false;
|
||||||
@ -32,6 +36,20 @@ export default class JsonRpcBase {
|
|||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_setConnected () {
|
||||||
|
if (!this._connected) {
|
||||||
|
this._connected = true;
|
||||||
|
this.emit('open');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_setDisconnected () {
|
||||||
|
if (this._connected) {
|
||||||
|
this._connected = false;
|
||||||
|
this.emit('close');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
get id () {
|
get id () {
|
||||||
return this._id;
|
return this._id;
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,7 @@ import TransportError from '../error';
|
|||||||
|
|
||||||
/* global WebSocket */
|
/* global WebSocket */
|
||||||
export default class Ws extends JsonRpcBase {
|
export default class Ws extends JsonRpcBase {
|
||||||
constructor (url, token, connect = true) {
|
constructor (url, token, autoconnect = true) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this._url = url;
|
this._url = url;
|
||||||
@ -32,14 +32,14 @@ export default class Ws extends JsonRpcBase {
|
|||||||
this._connecting = false;
|
this._connecting = false;
|
||||||
this._connected = false;
|
this._connected = false;
|
||||||
this._lastError = null;
|
this._lastError = null;
|
||||||
this._autoConnect = false;
|
this._autoConnect = autoconnect;
|
||||||
this._retries = 0;
|
this._retries = 0;
|
||||||
this._reconnectTimeoutId = null;
|
this._reconnectTimeoutId = null;
|
||||||
|
|
||||||
this._connectPromise = null;
|
this._connectPromise = null;
|
||||||
this._connectPromiseFunctions = {};
|
this._connectPromiseFunctions = {};
|
||||||
|
|
||||||
if (connect) {
|
if (autoconnect) {
|
||||||
this.connect();
|
this.connect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -124,11 +124,8 @@ export default class Ws extends JsonRpcBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_onOpen = (event) => {
|
_onOpen = (event) => {
|
||||||
console.log('ws:onOpen');
|
this._setConnected();
|
||||||
|
|
||||||
this._connected = true;
|
|
||||||
this._connecting = false;
|
this._connecting = false;
|
||||||
this._autoConnect = true;
|
|
||||||
this._retries = 0;
|
this._retries = 0;
|
||||||
|
|
||||||
Object.keys(this._messages)
|
Object.keys(this._messages)
|
||||||
@ -142,7 +139,7 @@ export default class Ws extends JsonRpcBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_onClose = (event) => {
|
_onClose = (event) => {
|
||||||
this._connected = false;
|
this._setDisconnected();
|
||||||
this._connecting = false;
|
this._connecting = false;
|
||||||
|
|
||||||
event.timestamp = Date.now();
|
event.timestamp = Date.now();
|
||||||
@ -209,8 +206,8 @@ export default class Ws extends JsonRpcBase {
|
|||||||
if (result.error) {
|
if (result.error) {
|
||||||
this.error(event.data);
|
this.error(event.data);
|
||||||
|
|
||||||
// Don't print error if request rejected...
|
// Don't print error if request rejected or not is not yet up...
|
||||||
if (!/rejected/.test(result.error.message)) {
|
if (!/(rejected|not yet up)/.test(result.error.message)) {
|
||||||
console.error(`${method}(${JSON.stringify(params)}): ${result.error.code}: ${result.error.message}`);
|
console.error(`${method}(${JSON.stringify(params)}): ${result.error.code}: ${result.error.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,6 +21,37 @@ describe('api/transport/Ws', () => {
|
|||||||
let transport;
|
let transport;
|
||||||
let scope;
|
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', () => {
|
describe('transport', () => {
|
||||||
let result;
|
let result;
|
||||||
|
|
||||||
|
@ -17,12 +17,20 @@
|
|||||||
import LogLevel from 'loglevel';
|
import LogLevel from 'loglevel';
|
||||||
|
|
||||||
export const LOG_KEYS = {
|
export const LOG_KEYS = {
|
||||||
|
Balances: {
|
||||||
|
key: 'balances',
|
||||||
|
desc: 'Balances fetching'
|
||||||
|
},
|
||||||
TransferModalStore: {
|
TransferModalStore: {
|
||||||
path: 'modals/Transfer/store',
|
key: 'modalsTransferStore',
|
||||||
desc: 'Transfer Modal MobX Store'
|
desc: 'Transfer modal MobX store'
|
||||||
|
},
|
||||||
|
Signer: {
|
||||||
|
key: 'secureApi',
|
||||||
|
desc: 'The Signer and the Secure API'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getLogger = (LOG_KEY) => {
|
export const getLogger = (LOG_KEY) => {
|
||||||
return LogLevel.getLogger(LOG_KEY.path);
|
return LogLevel.getLogger(LOG_KEY.key);
|
||||||
};
|
};
|
||||||
|
@ -21,51 +21,164 @@ import { padRight } from '~/api/util/format';
|
|||||||
|
|
||||||
import Contracts from '~/contracts';
|
import Contracts from '~/contracts';
|
||||||
|
|
||||||
|
let instance = null;
|
||||||
|
|
||||||
export default class Balances {
|
export default class Balances {
|
||||||
constructor (store, api) {
|
constructor (store, api) {
|
||||||
this._api = api;
|
this._api = api;
|
||||||
this._store = store;
|
this._store = store;
|
||||||
|
|
||||||
this._tokenregSubId = null;
|
this._tokenreg = null;
|
||||||
this._tokenregMetaSubId = 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
|
// that gets called max once every 40s
|
||||||
this.longThrottledFetch = throttle(
|
this.longThrottledFetch = throttle(
|
||||||
this.fetchBalances,
|
this._fetchBalances,
|
||||||
40 * 1000,
|
40 * 1000,
|
||||||
{ trailing: true }
|
{ leading: false, trailing: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
this.shortThrottledFetch = throttle(
|
this.shortThrottledFetch = throttle(
|
||||||
this.fetchBalances,
|
this._fetchBalances,
|
||||||
2 * 1000,
|
2 * 1000,
|
||||||
{ trailing: true }
|
{ leading: false, trailing: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
// Fetch all tokens every 2 minutes
|
// Fetch all tokens every 2 minutes
|
||||||
this.throttledTokensFetch = throttle(
|
this.throttledTokensFetch = throttle(
|
||||||
this.fetchTokens,
|
this._fetchTokens,
|
||||||
60 * 1000,
|
2 * 60 * 1000,
|
||||||
{ trailing: true }
|
{ leading: false, trailing: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Unsubscribe previous instance if it exists
|
||||||
|
if (instance) {
|
||||||
|
Balances.stop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
start () {
|
static get (store = {}) {
|
||||||
this.subscribeBlockNumber();
|
if (!instance && store) {
|
||||||
this.subscribeAccountsInfo();
|
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 () {
|
subscribeAccountsInfo () {
|
||||||
this._api
|
return this._api
|
||||||
.subscribe('parity_allAccountsInfo', (error, accountsInfo) => {
|
.subscribe('parity_allAccountsInfo', (error, accountsInfo) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.fetchBalances();
|
this.fetchAllBalances();
|
||||||
|
})
|
||||||
|
.then((accountsInfoSID) => {
|
||||||
|
this._accountsInfoSID = accountsInfoSID;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.warn('_subscribeAccountsInfo', error);
|
console.warn('_subscribeAccountsInfo', error);
|
||||||
@ -73,49 +186,97 @@ export default class Balances {
|
|||||||
}
|
}
|
||||||
|
|
||||||
subscribeBlockNumber () {
|
subscribeBlockNumber () {
|
||||||
this._api
|
return this._api
|
||||||
.subscribe('eth_blockNumber', (error) => {
|
.subscribe('eth_blockNumber', (error) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
return console.warn('_subscribeBlockNumber', error);
|
return console.warn('_subscribeBlockNumber', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { syncing } = this._store.getState().nodeStatus;
|
return this.fetchAllBalances();
|
||||||
|
})
|
||||||
this.throttledTokensFetch();
|
.then((blockNumberSID) => {
|
||||||
|
this._blockNumberSID = blockNumberSID;
|
||||||
// If syncing, only retrieve balances once every
|
|
||||||
// few seconds
|
|
||||||
if (syncing) {
|
|
||||||
this.shortThrottledFetch.cancel();
|
|
||||||
return this.longThrottledFetch();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.longThrottledFetch.cancel();
|
|
||||||
return this.shortThrottledFetch();
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.warn('_subscribeBlockNumber', error);
|
console.warn('_subscribeBlockNumber', error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchBalances () {
|
fetchAllBalances (options = {}) {
|
||||||
this._store.dispatch(fetchBalances());
|
// 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 () {
|
fetchTokensBalances (options) {
|
||||||
this._store.dispatch(fetchTokensBalances());
|
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 () {
|
getTokenRegistry () {
|
||||||
return Contracts.get().tokenReg.getContract();
|
return Contracts.get().tokenReg.getContract();
|
||||||
}
|
}
|
||||||
|
|
||||||
loadTokens () {
|
_loadTokens (options = {}) {
|
||||||
this
|
return this
|
||||||
.getTokenRegistry()
|
.getTokenRegistry()
|
||||||
.then((tokenreg) => {
|
.then((tokenreg) => {
|
||||||
|
this._tokenreg = tokenreg;
|
||||||
|
|
||||||
this._store.dispatch(setTokenReg(tokenreg));
|
this._store.dispatch(setTokenReg(tokenreg));
|
||||||
this._store.dispatch(loadTokens());
|
this._store.dispatch(loadTokens(options));
|
||||||
|
|
||||||
return this.attachToTokens(tokenreg);
|
return this.attachToTokens(tokenreg);
|
||||||
})
|
})
|
||||||
@ -133,7 +294,7 @@ export default class Balances {
|
|||||||
}
|
}
|
||||||
|
|
||||||
attachToNewToken (tokenreg) {
|
attachToNewToken (tokenreg) {
|
||||||
if (this._tokenregSubId) {
|
if (this._tokenregSID) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,13 +310,13 @@ export default class Balances {
|
|||||||
|
|
||||||
this.handleTokensLogs(logs);
|
this.handleTokensLogs(logs);
|
||||||
})
|
})
|
||||||
.then((tokenregSubId) => {
|
.then((tokenregSID) => {
|
||||||
this._tokenregSubId = tokenregSubId;
|
this._tokenregSID = tokenregSID;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
attachToTokenMetaChange (tokenreg) {
|
attachToTokenMetaChange (tokenreg) {
|
||||||
if (this._tokenregMetaSubId) {
|
if (this._tokenMetaSID) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -172,8 +333,8 @@ export default class Balances {
|
|||||||
|
|
||||||
this.handleTokensLogs(logs);
|
this.handleTokensLogs(logs);
|
||||||
})
|
})
|
||||||
.then((tokenregMetaSubId) => {
|
.then((tokenMetaSID) => {
|
||||||
this._tokenregMetaSubId = tokenregMetaSubId;
|
this._tokenMetaSID = tokenMetaSID;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,18 +23,27 @@ import { setAddressImage } from './imagesActions';
|
|||||||
|
|
||||||
import * as ABIS from '~/contracts/abi';
|
import * as ABIS from '~/contracts/abi';
|
||||||
import { notifyTransaction } from '~/util/notifications';
|
import { notifyTransaction } from '~/util/notifications';
|
||||||
|
import { LOG_KEYS, getLogger } from '~/config';
|
||||||
import imagesEthereum from '../../../assets/images/contracts/ethereum-black-64x64.png';
|
import imagesEthereum from '../../../assets/images/contracts/ethereum-black-64x64.png';
|
||||||
|
|
||||||
|
const log = getLogger(LOG_KEYS.Balances);
|
||||||
|
|
||||||
const ETH = {
|
const ETH = {
|
||||||
name: 'Ethereum',
|
name: 'Ethereum',
|
||||||
tag: 'ETH',
|
tag: 'ETH',
|
||||||
image: imagesEthereum
|
image: imagesEthereum
|
||||||
};
|
};
|
||||||
|
|
||||||
function setBalances (_balances) {
|
function setBalances (_balances, skipNotifications = false) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const state = 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 accounts = state.personal.accounts;
|
||||||
const nextBalances = _balances;
|
const nextBalances = _balances;
|
||||||
const prevBalances = state.balances.balances;
|
const prevBalances = state.balances.balances;
|
||||||
@ -48,38 +57,55 @@ function setBalances (_balances) {
|
|||||||
|
|
||||||
const balance = Object.assign({}, balances[address]);
|
const balance = Object.assign({}, balances[address]);
|
||||||
const { tokens, txCount = balance.txCount } = nextBalances[address];
|
const { tokens, txCount = balance.txCount } = nextBalances[address];
|
||||||
const nextTokens = balance.tokens.slice();
|
|
||||||
|
|
||||||
tokens.forEach((t) => {
|
const prevTokens = balance.tokens.slice();
|
||||||
const { token, value } = t;
|
const nextTokens = [];
|
||||||
const { tag } = token;
|
|
||||||
|
|
||||||
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) {
|
// If the given token is not in the current tokens, skip
|
||||||
nextTokens.push({
|
if (!nextToken && !prevToken) {
|
||||||
token,
|
return false;
|
||||||
value
|
}
|
||||||
});
|
|
||||||
} else {
|
// No updates
|
||||||
const oldValue = nextTokens[tokenIndex].value;
|
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 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 account = accounts[address];
|
||||||
const txValue = value.minus(oldValue);
|
const txValue = value.minus(prevValue);
|
||||||
|
|
||||||
const redirectToAccount = () => {
|
const redirectToAccount = () => {
|
||||||
const route = `/account/${account.address}`;
|
const route = `/accounts/${account.address}`;
|
||||||
dispatch(push(route));
|
dispatch(push(route));
|
||||||
};
|
};
|
||||||
|
|
||||||
notifyTransaction(account, token, txValue, redirectToAccount);
|
notifyTransaction(account, token, txValue, redirectToAccount);
|
||||||
}
|
}
|
||||||
|
|
||||||
nextTokens[tokenIndex] = { token, value };
|
return nextTokens.push({
|
||||||
}
|
...prevToken,
|
||||||
});
|
value
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
balances[address] = { txCount: txCount || new BigNumber(0), tokens: nextTokens };
|
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) => {
|
return (dispatch, getState) => {
|
||||||
const { tokenreg } = getState().balances;
|
const { tokenreg } = getState().balances;
|
||||||
|
|
||||||
@ -131,7 +159,7 @@ export function loadTokens () {
|
|||||||
.call()
|
.call()
|
||||||
.then((numTokens) => {
|
.then((numTokens) => {
|
||||||
const tokenIds = range(numTokens.toNumber());
|
const tokenIds = range(numTokens.toNumber());
|
||||||
dispatch(fetchTokens(tokenIds));
|
dispatch(fetchTokens(tokenIds, options));
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.warn('balances::loadTokens', 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 || []);
|
const tokenIds = uniq(_tokenIds || []);
|
||||||
|
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const { api, images, balances } = getState();
|
const { api, images, balances } = getState();
|
||||||
const { tokenreg } = balances;
|
const { tokenreg } = balances;
|
||||||
@ -161,8 +190,9 @@ export function fetchTokens (_tokenIds) {
|
|||||||
dispatch(setAddressImage(address, image, true));
|
dispatch(setAddressImage(address, image, true));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
log.debug('fetched token', tokens);
|
||||||
dispatch(setTokens(tokens));
|
dispatch(setTokens(tokens));
|
||||||
dispatch(fetchBalances());
|
dispatch(updateTokensFilter(null, null, options));
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.warn('balances::fetchTokens', 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) => {
|
return (dispatch, getState) => {
|
||||||
const { api, personal } = getState();
|
const { api, personal } = getState();
|
||||||
const { visibleAccounts, accounts } = personal;
|
const { visibleAccounts, accounts } = personal;
|
||||||
@ -192,8 +222,7 @@ export function fetchBalances (_addresses) {
|
|||||||
balances[addr] = accountsBalances[idx];
|
balances[addr] = accountsBalances[idx];
|
||||||
});
|
});
|
||||||
|
|
||||||
dispatch(setBalances(balances));
|
dispatch(setBalances(balances, skipNotifications));
|
||||||
updateTokensFilter(addresses)(dispatch, getState);
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.warn('balances::fetchBalances', 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) => {
|
return (dispatch, getState) => {
|
||||||
const { api, balances, personal } = getState();
|
const { api, balances, personal } = getState();
|
||||||
const { visibleAccounts, accounts } = personal;
|
const { visibleAccounts, accounts } = personal;
|
||||||
@ -214,27 +243,32 @@ export function updateTokensFilter (_addresses, _tokens) {
|
|||||||
const tokenAddresses = tokens.map((t) => t.address).sort();
|
const tokenAddresses = tokens.map((t) => t.address).sort();
|
||||||
|
|
||||||
if (tokensFilter.filterFromId || tokensFilter.filterToId) {
|
if (tokensFilter.filterFromId || tokensFilter.filterToId) {
|
||||||
|
// Has the tokens addresses changed (eg. a network change)
|
||||||
const sameTokens = isEqual(tokenAddresses, tokensFilter.tokenAddresses);
|
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);
|
return queryTokensFilter(tokensFilter)(dispatch, getState);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let promise = Promise.resolve();
|
log.debug('updating the token filter', addresses, tokenAddresses);
|
||||||
|
const promises = [];
|
||||||
|
|
||||||
if (tokensFilter.filterFromId) {
|
if (tokensFilter.filterFromId) {
|
||||||
promise = promise.then(() => api.eth.uninstallFilter(tokensFilter.filterFromId));
|
promises.push(api.eth.uninstallFilter(tokensFilter.filterFromId));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tokensFilter.filterToId) {
|
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) {
|
const promise = Promise.all(promises);
|
||||||
return promise;
|
|
||||||
}
|
|
||||||
|
|
||||||
const TRANSFER_SIGNATURE = api.util.sha3('Transfer(address,address,uint256)');
|
const TRANSFER_SIGNATURE = api.util.sha3('Transfer(address,address,uint256)');
|
||||||
const topicsFrom = [ TRANSFER_SIGNATURE, addresses, null ];
|
const topicsFrom = [ TRANSFER_SIGNATURE, addresses, null ];
|
||||||
@ -269,8 +303,10 @@ export function updateTokensFilter (_addresses, _tokens) {
|
|||||||
addresses, tokenAddresses
|
addresses, tokenAddresses
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { skipNotifications } = options;
|
||||||
|
|
||||||
dispatch(setTokensFilter(nextTokensFilter));
|
dispatch(setTokensFilter(nextTokensFilter));
|
||||||
fetchTokensBalances(addresses, tokens)(dispatch, getState);
|
fetchTokensBalances(addresses, tokens, skipNotifications)(dispatch, getState);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.warn('balances::updateTokensFilter', 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) => {
|
return (dispatch, getState) => {
|
||||||
const { api, personal, balances } = getState();
|
const { api, personal, balances } = getState();
|
||||||
const { visibleAccounts, accounts } = personal;
|
const { visibleAccounts, accounts } = personal;
|
||||||
@ -348,7 +384,7 @@ export function fetchTokensBalances (_addresses = null, _tokens = null) {
|
|||||||
balances[addr] = tokensBalances[idx];
|
balances[addr] = tokensBalances[idx];
|
||||||
});
|
});
|
||||||
|
|
||||||
dispatch(setBalances(balances));
|
dispatch(setBalances(balances, skipNotifications));
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.warn('balances::fetchTokensBalances', error);
|
console.warn('balances::fetchTokensBalances', error);
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import BalancesProvider from './balances';
|
||||||
import { showSnackbar } from './snackbarActions';
|
import { showSnackbar } from './snackbarActions';
|
||||||
import { DEFAULT_NETCHAIN } from './statusReducer';
|
import { DEFAULT_NETCHAIN } from './statusReducer';
|
||||||
|
|
||||||
@ -29,6 +30,11 @@ export default class ChainMiddleware {
|
|||||||
|
|
||||||
if (newChain !== nodeStatus.netChain && nodeStatus.netChain !== DEFAULT_NETCHAIN) {
|
if (newChain !== nodeStatus.netChain && nodeStatus.netChain !== DEFAULT_NETCHAIN) {
|
||||||
store.dispatch(showSnackbar(`Switched to ${newChain}. Please reload the page.`, 60000));
|
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
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,13 +16,18 @@
|
|||||||
|
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
|
|
||||||
|
import Contracts from '~/contracts';
|
||||||
import { initialState as defaultNodeStatusState } from './statusReducer';
|
import { initialState as defaultNodeStatusState } from './statusReducer';
|
||||||
import ChainMiddleware from './chainMiddleware';
|
import ChainMiddleware from './chainMiddleware';
|
||||||
|
import { createWsApi } from '~/../test/e2e/ethapi';
|
||||||
|
|
||||||
let middleware;
|
let middleware;
|
||||||
let next;
|
let next;
|
||||||
let store;
|
let store;
|
||||||
|
|
||||||
|
const api = createWsApi();
|
||||||
|
Contracts.create(api);
|
||||||
|
|
||||||
function createMiddleware (collection = {}) {
|
function createMiddleware (collection = {}) {
|
||||||
middleware = new ChainMiddleware().toMiddleware();
|
middleware = new ChainMiddleware().toMiddleware();
|
||||||
next = sinon.stub();
|
next = sinon.stub();
|
||||||
@ -30,6 +35,7 @@ function createMiddleware (collection = {}) {
|
|||||||
dispatch: sinon.stub(),
|
dispatch: sinon.stub(),
|
||||||
getState: () => {
|
getState: () => {
|
||||||
return {
|
return {
|
||||||
|
api: api,
|
||||||
nodeStatus: Object.assign({}, defaultNodeStatusState, collection)
|
nodeStatus: Object.assign({}, defaultNodeStatusState, collection)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,8 @@
|
|||||||
|
|
||||||
import { isEqual, intersection } from 'lodash';
|
import { isEqual, intersection } from 'lodash';
|
||||||
|
|
||||||
import { fetchBalances } from './balancesActions';
|
import BalancesProvider from './balances';
|
||||||
|
import { updateTokensFilter } from './balancesActions';
|
||||||
import { attachWallets } from './walletActions';
|
import { attachWallets } from './walletActions';
|
||||||
|
|
||||||
import Contract from '~/api/contract';
|
import Contract from '~/api/contract';
|
||||||
@ -98,7 +99,10 @@ export function personalAccountsInfo (accountsInfo) {
|
|||||||
|
|
||||||
dispatch(_personalAccountsInfo(data));
|
dispatch(_personalAccountsInfo(data));
|
||||||
dispatch(attachWallets(wallets));
|
dispatch(attachWallets(wallets));
|
||||||
dispatch(fetchBalances());
|
|
||||||
|
BalancesProvider.get().fetchAllBalances({
|
||||||
|
force: true
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.warn('personalAccountsInfo', error);
|
console.warn('personalAccountsInfo', error);
|
||||||
@ -130,6 +134,18 @@ export function setVisibleAccounts (addresses) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dispatch(_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
|
||||||
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -14,9 +14,15 @@
|
|||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import BalancesProvider from './balances';
|
||||||
import { statusBlockNumber, statusCollection, statusLogs } from './statusActions';
|
import { statusBlockNumber, statusCollection, statusLogs } from './statusActions';
|
||||||
import { isEqual } from 'lodash';
|
import { isEqual } from 'lodash';
|
||||||
|
|
||||||
|
import { LOG_KEYS, getLogger } from '~/config';
|
||||||
|
|
||||||
|
const log = getLogger(LOG_KEYS.Signer);
|
||||||
|
let instance = null;
|
||||||
|
|
||||||
export default class Status {
|
export default class Status {
|
||||||
constructor (store, api) {
|
constructor (store, api) {
|
||||||
this._api = api;
|
this._api = api;
|
||||||
@ -27,20 +33,90 @@ export default class Status {
|
|||||||
this._longStatus = {};
|
this._longStatus = {};
|
||||||
this._minerSettings = {};
|
this._minerSettings = {};
|
||||||
|
|
||||||
this._longStatusTimeoutId = null;
|
this._timeoutIds = {};
|
||||||
|
this._blockNumberSubscriptionId = null;
|
||||||
|
|
||||||
this._timestamp = Date.now();
|
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 () {
|
start () {
|
||||||
this._subscribeBlockNumber();
|
log.debug('status::start');
|
||||||
this._pollStatus();
|
|
||||||
this._pollLongStatus();
|
Promise
|
||||||
this._pollLogs();
|
.all([
|
||||||
|
this._subscribeBlockNumber(),
|
||||||
|
|
||||||
|
this._pollLogs(),
|
||||||
|
this._pollLongStatus(),
|
||||||
|
this._pollStatus()
|
||||||
|
])
|
||||||
|
.then(() => {
|
||||||
|
return BalancesProvider.start();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_subscribeBlockNumber () {
|
stop () {
|
||||||
this._api
|
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) => {
|
.subscribe('eth_blockNumber', (error, blockNumber) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
return;
|
return;
|
||||||
@ -51,6 +127,10 @@ export default class Status {
|
|||||||
this._api.eth
|
this._api.eth
|
||||||
.getBlockByNumber(blockNumber)
|
.getBlockByNumber(blockNumber)
|
||||||
.then((block) => {
|
.then((block) => {
|
||||||
|
if (!block) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this._store.dispatch(statusCollection({
|
this._store.dispatch(statusCollection({
|
||||||
blockTimestamp: block.timestamp,
|
blockTimestamp: block.timestamp,
|
||||||
gasLimit: block.gasLimit
|
gasLimit: block.gasLimit
|
||||||
@ -59,6 +139,9 @@ export default class Status {
|
|||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.warn('status._subscribeBlockNumber', 'getBlockByNumber', error);
|
console.warn('status._subscribeBlockNumber', 'getBlockByNumber', error);
|
||||||
});
|
});
|
||||||
|
})
|
||||||
|
.then((blockNumberSubscriptionId) => {
|
||||||
|
this._blockNumberSubscriptionId = blockNumberSubscriptionId;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,11 +155,7 @@ export default class Status {
|
|||||||
.catch(() => false);
|
.catch(() => false);
|
||||||
}
|
}
|
||||||
|
|
||||||
_pollStatus = () => {
|
getApiStatus = () => {
|
||||||
const nextTimeout = (timeout = 1000) => {
|
|
||||||
setTimeout(() => this._pollStatus(), timeout);
|
|
||||||
};
|
|
||||||
|
|
||||||
const { isConnected, isConnecting, needsToken, secureToken } = this._api;
|
const { isConnected, isConnecting, needsToken, secureToken } = this._api;
|
||||||
|
|
||||||
const apiStatus = {
|
const apiStatus = {
|
||||||
@ -86,19 +165,23 @@ export default class Status {
|
|||||||
secureToken
|
secureToken
|
||||||
};
|
};
|
||||||
|
|
||||||
const gotConnected = !this._apiStatus.isConnected && apiStatus.isConnected;
|
return apiStatus;
|
||||||
|
}
|
||||||
|
|
||||||
if (gotConnected) {
|
_pollStatus = () => {
|
||||||
this._pollLongStatus();
|
const nextTimeout = (timeout = 1000) => {
|
||||||
}
|
if (this._timeoutIds.status) {
|
||||||
|
clearTimeout(this._timeoutIds.status);
|
||||||
|
}
|
||||||
|
|
||||||
if (!isEqual(apiStatus, this._apiStatus)) {
|
this._timeoutIds.status = setTimeout(() => this._pollStatus(), timeout);
|
||||||
this._store.dispatch(statusCollection(apiStatus));
|
};
|
||||||
this._apiStatus = apiStatus;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isConnected) {
|
this.updateApiStatus();
|
||||||
return nextTimeout(250);
|
|
||||||
|
if (!this._api.isConnected) {
|
||||||
|
nextTimeout(250);
|
||||||
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
const { refreshStatus } = this._store.getState().nodeStatus;
|
const { refreshStatus } = this._store.getState().nodeStatus;
|
||||||
@ -110,7 +193,7 @@ export default class Status {
|
|||||||
statusPromises.push(this._api.eth.hashrate());
|
statusPromises.push(this._api.eth.hashrate());
|
||||||
}
|
}
|
||||||
|
|
||||||
Promise
|
return Promise
|
||||||
.all(statusPromises)
|
.all(statusPromises)
|
||||||
.then(([ syncing, ...statusResults ]) => {
|
.then(([ syncing, ...statusResults ]) => {
|
||||||
const status = statusResults.length === 0
|
const status = statusResults.length === 0
|
||||||
@ -125,11 +208,11 @@ export default class Status {
|
|||||||
this._store.dispatch(statusCollection(status));
|
this._store.dispatch(statusCollection(status));
|
||||||
this._status = status;
|
this._status = status;
|
||||||
}
|
}
|
||||||
|
|
||||||
nextTimeout();
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('_pollStatus', error);
|
console.error('_pollStatus', error);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
nextTimeout();
|
nextTimeout();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -140,7 +223,7 @@ export default class Status {
|
|||||||
* from the UI
|
* from the UI
|
||||||
*/
|
*/
|
||||||
_pollMinerSettings = () => {
|
_pollMinerSettings = () => {
|
||||||
Promise
|
return Promise
|
||||||
.all([
|
.all([
|
||||||
this._api.eth.coinbase(),
|
this._api.eth.coinbase(),
|
||||||
this._api.parity.extraData(),
|
this._api.parity.extraData(),
|
||||||
@ -175,21 +258,21 @@ export default class Status {
|
|||||||
*/
|
*/
|
||||||
_pollLongStatus = () => {
|
_pollLongStatus = () => {
|
||||||
if (!this._api.isConnected) {
|
if (!this._api.isConnected) {
|
||||||
return;
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextTimeout = (timeout = 30000) => {
|
const nextTimeout = (timeout = 30000) => {
|
||||||
if (this._longStatusTimeoutId) {
|
if (this._timeoutIds.longStatus) {
|
||||||
clearTimeout(this._longStatusTimeoutId);
|
clearTimeout(this._timeoutIds.longStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._longStatusTimeoutId = setTimeout(this._pollLongStatus, timeout);
|
this._timeoutIds.longStatus = setTimeout(() => this._pollLongStatus(), timeout);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Poll Miner settings just in case
|
// Poll Miner settings just in case
|
||||||
this._pollMinerSettings();
|
const minerPromise = this._pollMinerSettings();
|
||||||
|
|
||||||
Promise
|
const mainPromise = Promise
|
||||||
.all([
|
.all([
|
||||||
this._api.parity.netPeers(),
|
this._api.parity.netPeers(),
|
||||||
this._api.web3.clientVersion(),
|
this._api.web3.clientVersion(),
|
||||||
@ -225,21 +308,31 @@ export default class Status {
|
|||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('_pollLongStatus', error);
|
console.error('_pollLongStatus', error);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
nextTimeout(60000);
|
||||||
});
|
});
|
||||||
|
|
||||||
nextTimeout(60000);
|
return Promise.all([ minerPromise, mainPromise ]);
|
||||||
}
|
}
|
||||||
|
|
||||||
_pollLogs = () => {
|
_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;
|
const { devLogsEnabled } = this._store.getState().nodeStatus;
|
||||||
|
|
||||||
if (!devLogsEnabled) {
|
if (!devLogsEnabled) {
|
||||||
nextTimeout();
|
nextTimeout();
|
||||||
return;
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
Promise
|
return Promise
|
||||||
.all([
|
.all([
|
||||||
this._api.parity.devLogs(),
|
this._api.parity.devLogs(),
|
||||||
this._api.parity.devLogsLevels()
|
this._api.parity.devLogsLevels()
|
||||||
@ -249,11 +342,12 @@ export default class Status {
|
|||||||
devLogs: devLogs.slice(-1024),
|
devLogs: devLogs.slice(-1024),
|
||||||
devLogsLevels
|
devLogsLevels
|
||||||
}));
|
}));
|
||||||
nextTimeout();
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('_pollLogs', error);
|
console.error('_pollLogs', error);
|
||||||
nextTimeout();
|
})
|
||||||
|
.then(() => {
|
||||||
|
return nextTimeout();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -38,10 +38,10 @@ export default function (api, browserHistory) {
|
|||||||
const middleware = initMiddleware(api, browserHistory);
|
const middleware = initMiddleware(api, browserHistory);
|
||||||
const store = applyMiddleware(...middleware)(storeCreation)(reducers);
|
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 PersonalProvider(store, api).start();
|
||||||
new SignerProvider(store, api).start();
|
new SignerProvider(store, api).start();
|
||||||
new StatusProvider(store, api).start();
|
|
||||||
|
|
||||||
store.dispatch(loadWallet(api));
|
store.dispatch(loadWallet(api));
|
||||||
setupWorker(store);
|
setupWorker(store);
|
||||||
|
@ -17,133 +17,34 @@
|
|||||||
import { uniq } from 'lodash';
|
import { uniq } from 'lodash';
|
||||||
|
|
||||||
import Api from './api';
|
import Api from './api';
|
||||||
|
import { LOG_KEYS, getLogger } from '~/config';
|
||||||
|
|
||||||
|
const log = getLogger(LOG_KEYS.Signer);
|
||||||
const sysuiToken = window.localStorage.getItem('sysuiToken');
|
const sysuiToken = window.localStorage.getItem('sysuiToken');
|
||||||
|
|
||||||
export default class SecureApi extends Api {
|
export default class SecureApi extends Api {
|
||||||
|
_isConnecting = false;
|
||||||
|
_needsToken = false;
|
||||||
|
_tokens = [];
|
||||||
|
|
||||||
|
_dappsInterface = null;
|
||||||
|
_dappsPort = 8080;
|
||||||
|
_signerPort = 8180;
|
||||||
|
|
||||||
constructor (url, nextToken) {
|
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._url = url;
|
||||||
this._isConnecting = true;
|
|
||||||
this._needsToken = false;
|
|
||||||
|
|
||||||
this._dappsPort = 8080;
|
// Try tokens from localstorage, from hash and 'initial'
|
||||||
this._dappsInterface = null;
|
|
||||||
this._signerPort = 8180;
|
|
||||||
|
|
||||||
// Try tokens from localstorage, then from hash
|
|
||||||
this._tokens = uniq([sysuiToken, nextToken, 'initial'])
|
this._tokens = uniq([sysuiToken, nextToken, 'initial'])
|
||||||
.filter((token) => token)
|
.filter((token) => token)
|
||||||
.map((token) => ({ value: token, tried: false }));
|
.map((token) => ({ value: token, tried: false }));
|
||||||
|
|
||||||
this._tryNextToken();
|
// When the transport is closed, try to reconnect
|
||||||
}
|
transport.on('close', this.connect, this);
|
||||||
|
this.connect();
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get dappsPort () {
|
get dappsPort () {
|
||||||
@ -151,17 +52,19 @@ export default class SecureApi extends Api {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get dappsUrl () {
|
get dappsUrl () {
|
||||||
let hostname;
|
return `http://${this.hostname}:${this.dappsPort}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
get hostname () {
|
||||||
if (window.location.hostname === 'home.parity') {
|
if (window.location.hostname === 'home.parity') {
|
||||||
hostname = 'dapps.parity';
|
return 'dapps.parity';
|
||||||
} else if (!this._dappsInterface || this._dappsInterface === '0.0.0.0') {
|
|
||||||
hostname = window.location.hostname;
|
|
||||||
} else {
|
|
||||||
hostname = this._dappsInterface;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return `http://${hostname}:${this._dappsPort}`;
|
if (!this._dappsInterface || this._dappsInterface === '0.0.0.0') {
|
||||||
|
return window.location.hostname;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._dappsInterface;
|
||||||
}
|
}
|
||||||
|
|
||||||
get signerPort () {
|
get signerPort () {
|
||||||
@ -183,4 +86,279 @@ export default class SecureApi extends Api {
|
|||||||
get secureToken () {
|
get secureToken () {
|
||||||
return this._transport.token;
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
|
|
||||||
import Push from 'push.js';
|
import Push from 'push.js';
|
||||||
import BigNumber from 'bignumber.js';
|
import BigNumber from 'bignumber.js';
|
||||||
import { noop } from 'lodash';
|
|
||||||
|
|
||||||
import { fromWei } from '~/api/util/wei';
|
import { fromWei } from '~/api/util/wei';
|
||||||
|
|
||||||
@ -33,13 +32,34 @@ export function notifyTransaction (account, token, _value, onClick) {
|
|||||||
? ethereumIcon
|
? ethereumIcon
|
||||||
: (token.image || unkownIcon);
|
: (token.image || unkownIcon);
|
||||||
|
|
||||||
Push.create(`${name}`, {
|
let _notification = null;
|
||||||
body: `You just received ${value.toFormat()} ${token.tag.toUpperCase()}`,
|
|
||||||
icon: {
|
Push
|
||||||
x16: icon,
|
.create(`${name}`, {
|
||||||
x32: icon
|
body: `You just received ${value.toFormat(3)} ${token.tag.toUpperCase()}`,
|
||||||
},
|
icon: {
|
||||||
timeout: 20000,
|
x16: icon,
|
||||||
onClick: onClick || noop
|
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;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
@ -59,6 +59,10 @@ class Status extends Component {
|
|||||||
renderConsensus () {
|
renderConsensus () {
|
||||||
const { upgradeStore } = this.props;
|
const { upgradeStore } = this.props;
|
||||||
|
|
||||||
|
if (!upgradeStore || !upgradeStore.consensusCapability) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (upgradeStore.consensusCapability === 'capable') {
|
if (upgradeStore.consensusCapability === 'capable') {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@ -67,7 +71,9 @@ class Status extends Component {
|
|||||||
defaultMessage='Capable' />
|
defaultMessage='Capable' />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (upgradeStore.consensusCapability.capableUntil) {
|
}
|
||||||
|
|
||||||
|
if (upgradeStore.consensusCapability.capableUntil) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
@ -78,7 +84,9 @@ class Status extends Component {
|
|||||||
} } />
|
} } />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (upgradeStore.consensusCapability.incapableSince) {
|
}
|
||||||
|
|
||||||
|
if (upgradeStore.consensusCapability.incapableSince) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
|
@ -41,9 +41,9 @@ class Connection extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { isConnected, needsToken } = this.props;
|
const { isConnecting, isConnected, needsToken } = this.props;
|
||||||
|
|
||||||
if (isConnected) {
|
if (!isConnecting && isConnected) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,7 +51,7 @@ export default class Parity extends Component {
|
|||||||
Object.keys(LOG_KEYS).map((logKey) => {
|
Object.keys(LOG_KEYS).map((logKey) => {
|
||||||
const log = LOG_KEYS[logKey];
|
const log = LOG_KEYS[logKey];
|
||||||
|
|
||||||
const logger = LogLevel.getLogger(log.path);
|
const logger = LogLevel.getLogger(log.key);
|
||||||
const level = logger.getLevel();
|
const level = logger.getLevel();
|
||||||
|
|
||||||
nextState[logKey] = { level, log };
|
nextState[logKey] = { level, log };
|
||||||
@ -133,11 +133,11 @@ export default class Parity extends Component {
|
|||||||
|
|
||||||
return Object.keys(logLevels).map((logKey) => {
|
return Object.keys(logLevels).map((logKey) => {
|
||||||
const { level, log } = logLevels[logKey];
|
const { level, log } = logLevels[logKey];
|
||||||
const { path, desc } = log;
|
const { key, desc } = log;
|
||||||
|
|
||||||
const onChange = (_, index) => {
|
const onChange = (_, index) => {
|
||||||
const nextLevel = Object.values(selectValues)[index].value;
|
const nextLevel = Object.values(selectValues)[index].value;
|
||||||
LogLevel.getLogger(path).setLevel(nextLevel);
|
LogLevel.getLogger(key).setLevel(nextLevel);
|
||||||
this.loadLogLevels();
|
this.loadLogLevels();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -37,6 +37,14 @@ export default class MiningSettings extends Component {
|
|||||||
const { nodeStatus } = this.props;
|
const { nodeStatus } = this.props;
|
||||||
const { coinbase, defaultExtraData, extraData, gasFloorTarget, minGasPrice } = nodeStatus;
|
const { coinbase, defaultExtraData, extraData, gasFloorTarget, minGasPrice } = nodeStatus;
|
||||||
|
|
||||||
|
const extradata = extraData
|
||||||
|
? decodeExtraData(extraData)
|
||||||
|
: '';
|
||||||
|
|
||||||
|
const defaultExtradata = defaultExtraData
|
||||||
|
? decodeExtraData(defaultExtraData)
|
||||||
|
: '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div { ...this._testInherit() }>
|
<div { ...this._testInherit() }>
|
||||||
<ContainerTitle title='mining settings' />
|
<ContainerTitle title='mining settings' />
|
||||||
@ -53,9 +61,9 @@ export default class MiningSettings extends Component {
|
|||||||
<Input
|
<Input
|
||||||
label='extradata'
|
label='extradata'
|
||||||
hint='extra data for mined blocks'
|
hint='extra data for mined blocks'
|
||||||
value={ decodeExtraData(extraData) }
|
value={ extradata }
|
||||||
onSubmit={ this.onExtraDataChange }
|
onSubmit={ this.onExtraDataChange }
|
||||||
defaultValue={ decodeExtraData(defaultExtraData) }
|
defaultValue={ defaultExtradata }
|
||||||
allowCopy
|
allowCopy
|
||||||
floatCopy
|
floatCopy
|
||||||
{ ...this._test('extra-data') }
|
{ ...this._test('extra-data') }
|
||||||
|
@ -95,9 +95,15 @@ export default class Status extends Component {
|
|||||||
|
|
||||||
renderSettings () {
|
renderSettings () {
|
||||||
const { nodeStatus } = this.props;
|
const { nodeStatus } = this.props;
|
||||||
const { rpcSettings, netPeers } = nodeStatus;
|
const { rpcSettings, netPeers, netPort = '' } = nodeStatus;
|
||||||
const peers = `${netPeers.active}/${netPeers.connected}/${netPeers.max}`;
|
const peers = `${netPeers.active}/${netPeers.connected}/${netPeers.max}`;
|
||||||
|
|
||||||
|
if (!rpcSettings) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rpcPort = rpcSettings.port || '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div { ...this._test('settings') }>
|
<div { ...this._test('settings') }>
|
||||||
<ContainerTitle title='network settings' />
|
<ContainerTitle title='network settings' />
|
||||||
@ -121,7 +127,7 @@ export default class Status extends Component {
|
|||||||
allowCopy
|
allowCopy
|
||||||
readOnly
|
readOnly
|
||||||
label='network port'
|
label='network port'
|
||||||
value={ nodeStatus.netPort.toString() }
|
value={ netPort.toString() }
|
||||||
{ ...this._test('network-port') } />
|
{ ...this._test('network-port') } />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -146,7 +152,7 @@ export default class Status extends Component {
|
|||||||
allowCopy
|
allowCopy
|
||||||
readOnly
|
readOnly
|
||||||
label='rpc port'
|
label='rpc port'
|
||||||
value={ rpcSettings.port.toString() }
|
value={ rpcPort.toString() }
|
||||||
{ ...this._test('rpc-port') } />
|
{ ...this._test('rpc-port') } />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -47,8 +47,8 @@ export function mockHttp (requests) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function mockWs (requests) {
|
export function mockWs (requests) {
|
||||||
const scope = { requests: 0, body: {} };
|
|
||||||
let mockServer = new MockWsServer(TEST_WS_URL);
|
let mockServer = new MockWsServer(TEST_WS_URL);
|
||||||
|
const scope = { requests: 0, body: {}, server: mockServer };
|
||||||
|
|
||||||
scope.isDone = () => scope.requests === requests.length;
|
scope.isDone = () => scope.requests === requests.length;
|
||||||
scope.stop = () => {
|
scope.stop = () => {
|
||||||
|
Loading…
Reference in New Issue
Block a user