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:
Nicolas Gotchac 2017-01-12 14:25:32 +01:00 committed by Jaco Greeff
parent bc2ebdc564
commit 81beec1352
22 changed files with 904 additions and 287 deletions

View File

@ -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');
} }

View File

@ -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}`);

View File

@ -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'];

View File

@ -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;
} }

View File

@ -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}`);
} }

View File

@ -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;

View File

@ -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);
}; };

View File

@ -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;
}); });
} }

View File

@ -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);

View File

@ -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
});
} }
} }
} }

View File

@ -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)
}; };
} }

View File

@ -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
});
}; };
} }

View File

@ -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();
}); });
} }
} }

View File

@ -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);

View File

@ -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);
});
});
}
} }

View File

@ -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;
});
} }

View File

@ -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

View File

@ -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;
} }

View File

@ -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();
}; };

View File

@ -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') }

View File

@ -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>

View File

@ -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 = () => {