49fdd23d58
* Move secureApi to shell * Extract isTestnet test * Use mobx + subscriptions for status * Re-add status indicator * Add lerna * Move intial packages to js/packages * Move 3rdparty/{email,sms}-verification to correct location * Move package.json & README to library src * Move tests for library packages * Move views & dapps to packages * Move i18n to root * Move shell to actual src (main app) * Remove ~ references * Change ~ to root (explicit imports) * Finalise convert of ~ * Move views into dapps as well * Move dapps to packages/ * Fix references * Update css * Update test spec locations * Update tests * Case fix * Skip flakey tests * Update enzyme * Skip previously ignored tests * Allow empty api for hw * Re-add theme for embed
349 lines
8.5 KiB
JavaScript
349 lines
8.5 KiB
JavaScript
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
|
|
// This file is part of Parity.
|
|
|
|
// Parity is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
|
|
// Parity is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
import { throttle } from 'lodash';
|
|
|
|
import { padRight } from '@parity/api/util/format';
|
|
import Contracts from '@parity/shared/contracts';
|
|
|
|
import { fetchBalances, fetchTokensBalances, queryTokensFilter } from './balancesActions';
|
|
import { loadTokens, fetchTokens } from './tokensActions';
|
|
|
|
let instance = null;
|
|
|
|
export default class Balances {
|
|
constructor (store, api) {
|
|
this._api = api;
|
|
this._store = store;
|
|
|
|
this._tokenreg = null;
|
|
this._tokenregSID = null;
|
|
this._tokenMetaSID = null;
|
|
|
|
this._blockNumberSID = null;
|
|
this._accountsInfoSID = null;
|
|
|
|
// Throtthled load tokens (no more than once
|
|
// every minute)
|
|
this.loadTokens = throttle(
|
|
this._loadTokens,
|
|
60 * 1000,
|
|
{ leading: true, trailing: true }
|
|
);
|
|
|
|
// Throttled `_fetchBalances` function
|
|
// that gets called max once every 40s
|
|
this.longThrottledFetch = throttle(
|
|
this._fetchBalances,
|
|
40 * 1000,
|
|
{ leading: false, trailing: true }
|
|
);
|
|
|
|
this.shortThrottledFetch = throttle(
|
|
this._fetchBalances,
|
|
2 * 1000,
|
|
{ leading: false, trailing: true }
|
|
);
|
|
|
|
// Fetch all tokens every 2 minutes
|
|
this.throttledTokensFetch = throttle(
|
|
this._fetchTokens,
|
|
2 * 60 * 1000,
|
|
{ leading: false, trailing: true }
|
|
);
|
|
|
|
// Unsubscribe previous instance if it exists
|
|
if (instance) {
|
|
Balances.stop();
|
|
}
|
|
}
|
|
|
|
static get (store = {}) {
|
|
if (!instance && store) {
|
|
const { api } = store.getState();
|
|
|
|
return Balances.instantiate(store, api);
|
|
}
|
|
|
|
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 () {
|
|
return this._api
|
|
.subscribe('parity_allAccountsInfo', (error, accountsInfo) => {
|
|
if (error) {
|
|
return;
|
|
}
|
|
|
|
this.fetchAllBalances();
|
|
})
|
|
.then((accountsInfoSID) => {
|
|
this._accountsInfoSID = accountsInfoSID;
|
|
})
|
|
.catch((error) => {
|
|
console.warn('_subscribeAccountsInfo', error);
|
|
});
|
|
}
|
|
|
|
subscribeBlockNumber () {
|
|
return this._api
|
|
.subscribe('eth_blockNumber', (error) => {
|
|
if (error) {
|
|
return console.warn('_subscribeBlockNumber', error);
|
|
}
|
|
|
|
this._store.dispatch(queryTokensFilter());
|
|
return this.fetchAllBalances();
|
|
})
|
|
.then((blockNumberSID) => {
|
|
this._blockNumberSID = blockNumberSID;
|
|
})
|
|
.catch((error) => {
|
|
console.warn('_subscribeBlockNumber', error);
|
|
});
|
|
}
|
|
|
|
fetchAllBalances (options = {}) {
|
|
// If it's a network change, reload the tokens
|
|
// ( and then fetch the tokens balances ) and fetch
|
|
// the accounts balances
|
|
if (options.changedNetwork) {
|
|
this.loadTokens({ skipNotifications: true });
|
|
this.loadTokens.flush();
|
|
|
|
this.fetchBalances({
|
|
force: true,
|
|
skipNotifications: true
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
this.fetchTokensBalances(options);
|
|
this.fetchBalances(options);
|
|
}
|
|
|
|
fetchTokensBalances (options) {
|
|
const { skipNotifications = false, force = false } = options;
|
|
|
|
this.throttledTokensFetch(skipNotifications);
|
|
|
|
if (force) {
|
|
this.throttledTokensFetch.flush();
|
|
}
|
|
}
|
|
|
|
fetchBalances (options) {
|
|
const { skipNotifications = false, force = false } = options;
|
|
const { syncing } = this._store.getState().nodeStatus;
|
|
|
|
// If syncing, only retrieve balances once every
|
|
// few seconds
|
|
if (syncing || syncing === null) {
|
|
this.shortThrottledFetch.cancel();
|
|
this.longThrottledFetch(skipNotifications);
|
|
|
|
if (force) {
|
|
this.longThrottledFetch.flush();
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
this.longThrottledFetch.cancel();
|
|
this.shortThrottledFetch(skipNotifications);
|
|
|
|
if (force) {
|
|
this.shortThrottledFetch.flush();
|
|
}
|
|
}
|
|
|
|
_fetchBalances (skipNotifications = false) {
|
|
this._store.dispatch(fetchBalances(null, skipNotifications));
|
|
}
|
|
|
|
_fetchTokens (skipNotifications = false) {
|
|
this._store.dispatch(fetchTokensBalances(null, null, skipNotifications));
|
|
}
|
|
|
|
getTokenRegistry () {
|
|
return Contracts.get(this._api).tokenReg.getContract();
|
|
}
|
|
|
|
_loadTokens (options = {}) {
|
|
return this
|
|
.getTokenRegistry()
|
|
.then((tokenreg) => {
|
|
this._tokenreg = tokenreg;
|
|
|
|
this._store.dispatch(loadTokens(options));
|
|
|
|
return this.attachToTokens(tokenreg);
|
|
})
|
|
.catch((error) => {
|
|
console.warn('balances::loadTokens', error);
|
|
});
|
|
}
|
|
|
|
attachToTokens (tokenreg) {
|
|
return Promise
|
|
.all([
|
|
this.attachToTokenMetaChange(tokenreg),
|
|
this.attachToNewToken(tokenreg)
|
|
]);
|
|
}
|
|
|
|
attachToNewToken (tokenreg) {
|
|
if (this._tokenregSID) {
|
|
return Promise.resolve();
|
|
}
|
|
|
|
return tokenreg.instance.Registered
|
|
.subscribe({
|
|
fromBlock: 0,
|
|
toBlock: 'latest',
|
|
skipInitFetch: true
|
|
}, (error, logs) => {
|
|
if (error) {
|
|
return console.error('balances::attachToNewToken', 'failed to attach to tokenreg Registered', error.toString(), error.stack);
|
|
}
|
|
|
|
this.handleTokensLogs(logs);
|
|
})
|
|
.then((tokenregSID) => {
|
|
this._tokenregSID = tokenregSID;
|
|
});
|
|
}
|
|
|
|
attachToTokenMetaChange (tokenreg) {
|
|
if (this._tokenMetaSID) {
|
|
return Promise.resolve();
|
|
}
|
|
|
|
return tokenreg.instance.MetaChanged
|
|
.subscribe({
|
|
fromBlock: 0,
|
|
toBlock: 'latest',
|
|
topics: [ null, padRight(this._api.util.asciiToHex('IMG'), 32) ],
|
|
skipInitFetch: true
|
|
}, (error, logs) => {
|
|
if (error) {
|
|
return console.error('balances::attachToTokenMetaChange', 'failed to attach to tokenreg MetaChanged', error.toString(), error.stack);
|
|
}
|
|
|
|
this.handleTokensLogs(logs);
|
|
})
|
|
.then((tokenMetaSID) => {
|
|
this._tokenMetaSID = tokenMetaSID;
|
|
});
|
|
}
|
|
|
|
handleTokensLogs (logs) {
|
|
const tokenIds = logs.map((log) => log.params.id.value.toNumber());
|
|
|
|
this._store.dispatch(fetchTokens(tokenIds));
|
|
}
|
|
}
|