Fix slow balances (#6471)

* Update token updates

* Update token info fetching

* Update logger

* Minor fixes to updates and notifications for balances

* Use Pubsub

* Fix timeout.

* Use pubsub for status.

* Fix signer subscription.

* Process tokens in chunks.

* Fix tokens loaded by chunks

* Linting

* Dispatch tokens asap

* Fix chunks processing.

* Better filter options

* Parallel log fetching.

* Fix signer polling.

* Fix initial block query.

* Token balances updates : the right(er) way

* Better tokens info fetching

* Fixes in token data fetching

* Only fetch what's needed (tokens)

* Fix linting issues

* Revert "Transaction permissioning (#6441)"

This reverts commit eed0e8b03a.

* Revert "Revert "Transaction permissioning (#6441)""

This reverts commit 8f96415e58dde652e5828706eb2639d43416f448.

* Update wasm-tests.

* Fixing balances fetching

* Fix requests tracking in UI

* Fix request watching

* Update the Logger

* PR Grumbles Fixes

* PR Grumbles fixes

* Linting...
This commit is contained in:
Nicolas Gotchac
2017-09-10 18:03:35 +02:00
committed by Gav Wood
parent ee14a3fb31
commit f1a050366f
51 changed files with 1819 additions and 857 deletions

View File

@@ -71,10 +71,15 @@ export default class Api extends EventEmitter {
}
}
get isPubSub () {
return !!this._pubsub;
}
get pubsub () {
if (!this._pubsub) {
if (!this.isPubSub) {
throw Error('Pubsub is only available with a subscribing-supported transport injected!');
}
return this._pubsub;
}

View File

@@ -25,7 +25,7 @@ export default class Eth extends PubsubBase {
}
newHeads (callback) {
return this.addListener('eth', 'newHeads', callback);
return this.addListener('eth', 'newHeads', callback, null);
}
logs (callback) {

View File

@@ -267,7 +267,7 @@ export default class Parity extends PubsubBase {
// parity accounts API (only secure API or configured to be exposed)
allAccountsInfo (callback) {
return this._addListener(this._api, 'parity_allAccountsInfo', (error, data) => {
return this.addListener(this._api, 'parity_allAccountsInfo', (error, data) => {
error
? callback(error)
: callback(null, outAccountInfo(data));
@@ -275,7 +275,7 @@ export default class Parity extends PubsubBase {
}
getDappAddresses (callback, dappId) {
return this._addListener(this._api, 'parity_getDappAddresses', (error, data) => {
return this.addListener(this._api, 'parity_getDappAddresses', (error, data) => {
error
? callback(error)
: callback(null, outAddresses(data));
@@ -283,7 +283,7 @@ export default class Parity extends PubsubBase {
}
getDappDefaultAddress (callback, dappId) {
return this._addListener(this._api, 'parity_getDappDefaultAddress', (error, data) => {
return this.addListener(this._api, 'parity_getDappDefaultAddress', (error, data) => {
error
? callback(error)
: callback(null, outAddress(data));
@@ -291,7 +291,7 @@ export default class Parity extends PubsubBase {
}
getNewDappsAddresses (callback) {
return this._addListener(this._api, 'parity_getDappDefaultAddress', (error, addresses) => {
return this.addListener(this._api, 'parity_getDappDefaultAddress', (error, addresses) => {
error
? callback(error)
: callback(null, addresses ? addresses.map(outAddress) : null);
@@ -299,7 +299,7 @@ export default class Parity extends PubsubBase {
}
getNewDappsDefaultAddress (callback) {
return this._addListener(this._api, 'parity_getNewDappsDefaultAddress', (error, data) => {
return this.addListener(this._api, 'parity_getNewDappsDefaultAddress', (error, data) => {
error
? callback(error)
: callback(null, outAddress(data));
@@ -307,7 +307,7 @@ export default class Parity extends PubsubBase {
}
listRecentDapps (callback) {
return this._addListener(this._api, 'parity_listRecentDapps', (error, data) => {
return this.addListener(this._api, 'parity_listRecentDapps', (error, data) => {
error
? callback(error)
: callback(null, outRecentDapps(data));
@@ -315,7 +315,7 @@ export default class Parity extends PubsubBase {
}
listGethAccounts (callback) {
return this._addListener(this._api, 'parity_listGethAccounts', (error, data) => {
return this.addListener(this._api, 'parity_listGethAccounts', (error, data) => {
error
? callback(error)
: callback(null, outAddresses(data));
@@ -323,15 +323,15 @@ export default class Parity extends PubsubBase {
}
listVaults (callback) {
return this._addListener(this._api, 'parity_listVaults', callback);
return this.addListener(this._api, 'parity_listVaults', callback);
}
listOpenedVaults (callback) {
return this._addListener(this._api, 'parity_listOpenedVaults', callback);
return this.addListener(this._api, 'parity_listOpenedVaults', callback);
}
getVaultMeta (callback, vaultName) {
return this._addListener(this._api, 'parity_getVaultMeta', (error, data) => {
return this.addListener(this._api, 'parity_getVaultMeta', (error, data) => {
error
? callback(error)
: callback(null, outVaultMeta(data));
@@ -339,7 +339,7 @@ export default class Parity extends PubsubBase {
}
deriveAddressHash (callback, address, password, hash, shouldSave) {
return this._addListener(this._api, 'parity_deriveAddressHash', (error, data) => {
return this.addListener(this._api, 'parity_deriveAddressHash', (error, data) => {
error
? callback(error)
: callback(null, outAddress(data));
@@ -347,10 +347,18 @@ export default class Parity extends PubsubBase {
}
deriveAddressIndex (callback, address, password, index, shouldSave) {
return this._addListener(this._api, 'parity_deriveAddressIndex', (error, data) => {
return this.addListener(this._api, 'parity_deriveAddressIndex', (error, data) => {
error
? callback(error)
: callback(null, outAddress(data));
}, [inAddress(address), password, inDeriveIndex(index), !!shouldSave]);
}
nodeHealth (callback) {
return this.addListener(this._api, 'parity_nodeHealth', (error, data) => {
error
? callback(error)
: callback(null, data);
});
}
}

View File

@@ -16,6 +16,7 @@
import Eth from './eth';
import Parity from './parity';
import Signer from './signer';
import Net from './net';
import { isFunction } from '../util/types';
@@ -29,6 +30,7 @@ export default class Pubsub {
this._eth = new Eth(transport);
this._net = new Net(transport);
this._parity = new Parity(transport);
this._signer = new Signer(transport);
}
get net () {
@@ -43,8 +45,35 @@ export default class Pubsub {
return this._parity;
}
get signer () {
return this._signer;
}
unsubscribe (subscriptionIds) {
// subscriptions are namespace independent. Thus we can simply removeListener from any.
return this._parity.removeListener(subscriptionIds);
}
subscribeAndGetResult (f, callback) {
return new Promise((resolve, reject) => {
let isFirst = true;
let onSubscription = (error, data) => {
const p1 = error ? Promise.reject(error) : Promise.resolve(data);
const p2 = p1.then(callback);
if (isFirst) {
isFirst = false;
p2
.then(resolve)
.catch(reject);
}
};
try {
f.call(this, onSubscription).catch(reject);
} catch (err) {
reject(err);
}
});
}
}

View File

@@ -20,11 +20,12 @@ export default class PubsubBase {
this._transport = transport;
}
addListener (module, eventName, callback, eventParams) {
return eventParams
? this._transport.subscribe(module, callback, eventName, eventParams)
: this._transport.subscribe(module, callback, eventName, []);
// this._transport.subscribe(module, callback, eventName); After Patch from tomac is merged to master! => eth_subscribe does not support empty array as params
addListener (module, eventName, callback, eventParams = []) {
if (eventName) {
return this._transport.subscribe(module, callback, eventParams ? [eventName, eventParams] : [eventName]);
}
return this._transport.subscribe(module, callback, eventParams);
}
removeListener (subscriptionIds) {

View File

@@ -0,0 +1,16 @@
// 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/>.
export default from './signer';

View File

@@ -0,0 +1,37 @@
// 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 PubsubBase from '../pubsubBase';
import { outSignerRequest } from '../../format/output';
export default class Net extends PubsubBase {
constructor (transport) {
super(transport);
this._api = {
subscribe: 'signer_subscribePending',
unsubscribe: 'signer_unsubscribePending',
subscription: 'signer_pending'
};
}
pendingRequests (callback) {
return this.addListener(this._api, null, (error, data) => {
error
? callback(error)
: callback(null, data.map(outSignerRequest));
});
}
}

View File

@@ -44,6 +44,15 @@ export default class Parity {
.execute('parity_addReservedPeer', enode);
}
call (requests, blockNumber = 'latest') {
return this._transport
.execute(
'parity_call',
requests.map((options) => inOptions(options)),
inBlockNumber(blockNumber)
);
}
chainStatus () {
return this._transport
.execute('parity_chainStatus')

View File

@@ -24,6 +24,13 @@ export default class Eth {
this._lastBlock = new BigNumber(-1);
this._pollTimerId = null;
// Try to restart subscription if transport is closed
this._api.transport.on('close', () => {
if (this.isStarted) {
this.start();
}
});
}
get isStarted () {
@@ -33,31 +40,56 @@ export default class Eth {
start () {
this._started = true;
return this._blockNumber();
if (this._api.isPubSub) {
return Promise.all([
this._pollBlockNumber(false),
this._api.pubsub
.subscribeAndGetResult(
callback => this._api.pubsub.eth.newHeads(callback),
() => {
return this._api.eth
.blockNumber()
.then(blockNumber => {
this.updateBlock(blockNumber);
return blockNumber;
});
}
)
]);
}
// fallback to polling
return this._pollBlockNumber(true);
}
_blockNumber = () => {
const nextTimeout = (timeout = 1000) => {
this._pollTimerId = setTimeout(() => {
this._blockNumber();
}, timeout);
_pollBlockNumber = (doTimeout) => {
const nextTimeout = (timeout = 1000, forceTimeout = doTimeout) => {
if (forceTimeout) {
this._pollTimerId = setTimeout(() => {
this._pollBlockNumber(doTimeout);
}, timeout);
}
};
if (!this._api.transport.isConnected) {
nextTimeout(500);
nextTimeout(500, true);
return;
}
return this._api.eth
.blockNumber()
.then((blockNumber) => {
if (!blockNumber.eq(this._lastBlock)) {
this._lastBlock = blockNumber;
this._updateSubscriptions('eth_blockNumber', null, blockNumber);
}
this.updateBlock(blockNumber);
nextTimeout();
})
.catch(() => nextTimeout());
}
updateBlock (blockNumber) {
if (!blockNumber.eq(this._lastBlock)) {
this._lastBlock = blockNumber;
this._updateSubscriptions('eth_blockNumber', null, blockNumber);
}
}
}

View File

@@ -29,7 +29,8 @@ function stubApi (blockNumber) {
return {
_calls,
transport: {
isConnected: true
isConnected: true,
on: () => {}
},
eth: {
blockNumber: () => {

View File

@@ -23,6 +23,13 @@ export default class Personal {
this._lastDefaultAccount = '0x0';
this._pollTimerId = null;
// Try to restart subscription if transport is closed
this._api.transport.on('close', () => {
if (this.isStarted) {
this.start();
}
});
}
get isStarted () {
@@ -32,20 +39,42 @@ export default class Personal {
start () {
this._started = true;
let defaultAccount = null;
if (this._api.isPubSub) {
defaultAccount = this._api.pubsub
.subscribeAndGetResult(
callback => this._api.pubsub.parity.defaultAccount(callback),
(defaultAccount) => {
this.updateDefaultAccount(defaultAccount);
return defaultAccount;
}
);
} else {
defaultAccount = this._defaultAccount();
}
return Promise.all([
this._defaultAccount(),
defaultAccount,
this._listAccounts(),
this._accountsInfo(),
this._loggingSubscribe()
]);
}
updateDefaultAccount (defaultAccount) {
if (this._lastDefaultAccount !== defaultAccount) {
this._lastDefaultAccount = defaultAccount;
this._updateSubscriptions('parity_defaultAccount', null, defaultAccount);
}
}
// FIXME: Because of the different API instances, the "wait for valid changes" approach
// doesn't work. Since the defaultAccount is critical to operation, we poll in exactly
// same way we do in ../eth (ala eth_blockNumber) and update. This should be moved
// to pub-sub as it becomes available
_defaultAccount = (timerDisabled = false) => {
const nextTimeout = (timeout = 1000) => {
const nextTimeout = (timeout = 3000) => {
if (!timerDisabled) {
this._pollTimerId = setTimeout(() => {
this._defaultAccount();
@@ -61,11 +90,7 @@ export default class Personal {
return this._api.parity
.defaultAccount()
.then((defaultAccount) => {
if (this._lastDefaultAccount !== defaultAccount) {
this._lastDefaultAccount = defaultAccount;
this._updateSubscriptions('parity_defaultAccount', null, defaultAccount);
}
this.updateDefaultAccount(defaultAccount);
nextTimeout();
})
.catch(() => nextTimeout());

View File

@@ -22,6 +22,13 @@ export default class Signer {
this._api = api;
this._updateSubscriptions = updateSubscriptions;
this._started = false;
// Try to restart subscription if transport is closed
this._api.transport.on('close', () => {
if (this.isStarted) {
this.start();
}
});
}
get isStarted () {
@@ -31,30 +38,50 @@ export default class Signer {
start () {
this._started = true;
if (this._api.isPubSub) {
const subscription = this._api.pubsub
.subscribeAndGetResult(
callback => this._api.pubsub.signer.pendingRequests(callback),
requests => {
this.updateSubscriptions(requests);
return requests;
}
);
return Promise.all([
this._listRequests(false),
subscription
]);
}
return Promise.all([
this._listRequests(true),
this._loggingSubscribe()
]);
}
updateSubscriptions (requests) {
return this._updateSubscriptions('signer_requestsToConfirm', null, requests);
}
_listRequests = (doTimeout) => {
const nextTimeout = (timeout = 1000) => {
if (doTimeout) {
const nextTimeout = (timeout = 1000, forceTimeout = doTimeout) => {
if (forceTimeout) {
setTimeout(() => {
this._listRequests(true);
this._listRequests(doTimeout);
}, timeout);
}
};
if (!this._api.transport.isConnected) {
nextTimeout(500);
nextTimeout(500, true);
return;
}
return this._api.signer
.requestsToConfirm()
.then((requests) => {
this._updateSubscriptions('signer_requestsToConfirm', null, requests);
this.updateSubscriptions(requests);
nextTimeout();
})
.catch(() => nextTimeout());

View File

@@ -15,7 +15,11 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import EventEmitter from 'eventemitter3';
import { Logging } from '../subscriptions';
import logger from './logger';
const LOGGER_ENABLED = process.env.NODE_ENV !== 'production';
export default class JsonRpcBase extends EventEmitter {
constructor () {
@@ -75,6 +79,14 @@ export default class JsonRpcBase extends EventEmitter {
}
execute (method, ...params) {
let start;
let logId;
if (LOGGER_ENABLED) {
start = Date.now();
logId = logger.log({ method, params });
}
return this._middlewareList.then((middlewareList) => {
for (const middleware of middlewareList) {
const res = middleware.handle(method, params);
@@ -93,7 +105,18 @@ export default class JsonRpcBase extends EventEmitter {
}
}
return this._execute(method, params);
const result = this._execute(method, params);
if (!LOGGER_ENABLED) {
return result;
}
return result
.then((result) => {
logger.set(logId, { result, time: Date.now() - start });
return result;
});
});
}

View File

@@ -0,0 +1,150 @@
// 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 MethodDecodingStore from '~/ui/MethodDecoding/methodDecodingStore';
const LOGGER_ENABLED = process.env.NODE_ENV !== 'production';
let logger;
if (LOGGER_ENABLED) {
class Logger {
_logs = {};
_id = 0;
log ({ method, params }) {
const logId = this._id++;
this._logs[logId] = { method, params, date: Date.now() };
return logId;
}
set (logId, data) {
this._logs[logId] = {
...this._logs[logId],
...data
};
}
static sorter (logA, logB) {
return logA.date - logB.date;
}
get calls () {
const calls = this.methods['eth_call'] || [];
const decoding = MethodDecodingStore.get(window.secureApi);
const contracts = {};
const progress = Math.round(calls.length / 20);
return calls
.reduce((promise, call, index) => {
const { data, to } = call.params[0];
contracts[to] = contracts[to] || [];
return promise
.then(() => decoding.lookup(null, { data, to }))
.then((lookup) => {
if (!lookup.name) {
contracts[to].push(data);
return;
}
const inputs = lookup.inputs.map((input) => {
if (/bytes/.test(input.type)) {
return '0x' + input.value.map((v) => v.toString(16).padStart(2, 0)).join('');
}
return input.value;
});
const called = `${lookup.name}(${inputs.join(', ')})`;
contracts[to].push(called);
if (index % progress === 0) {
console.warn(`progress: ${Math.round(100 * index / calls.length)}%`);
}
});
}, Promise.resolve())
.then(() => {
return Object.keys(contracts)
.map((address) => {
const count = contracts[address].length;
return {
count,
calls: contracts[address],
to: address
};
})
.sort((cA, cB) => cB.count - cA.count);
});
}
get logs () {
return Object.values(this._logs).sort(Logger.sorter);
}
get methods () {
return this.logs.reduce((methods, log) => {
methods[log.method] = methods[log.method] || [];
methods[log.method].push(log);
return methods;
}, {});
}
get stats () {
const logs = this.logs;
const methods = this.methods;
const start = logs[0].date;
const end = logs[logs.length - 1].date;
// Duration in seconds
const duration = (end - start) / 1000;
const speed = logs.length / duration;
const sortedMethods = Object.keys(methods)
.map((method) => {
const methodLogs = methods[method].sort(Logger.sorter);
const methodSpeed = methodLogs.length / duration;
return {
speed: methodSpeed,
count: methodLogs.length,
logs: methodLogs,
method
};
})
.sort((mA, mB) => mB.count - mA.count);
return {
methods: sortedMethods,
speed
};
}
}
logger = new Logger();
if (window) {
window._logger = logger;
}
}
export default logger;

View File

@@ -29,7 +29,7 @@ export default class Ws extends JsonRpcBase {
this._url = url;
this._token = token;
this._messages = {};
this._subscriptions = { 'eth_subscription': [], 'parity_subscription': [], 'shh_subscription': [] };
this._subscriptions = {};
this._sessionHash = null;
this._connecting = false;
@@ -209,6 +209,7 @@ export default class Ws extends JsonRpcBase {
// initial pubsub ACK
if (id && msg.subscription) {
// save subscription to map subId -> messageId
this._subscriptions[msg.subscription] = this._subscriptions[msg.subscription] || {};
this._subscriptions[msg.subscription][res] = id;
// resolve promise with messageId because subId's can collide (eth/parity)
msg.resolve(id);
@@ -223,7 +224,7 @@ export default class Ws extends JsonRpcBase {
}
// pubsub format
if (method.includes('subscription')) {
if (this._subscriptions[method]) {
const messageId = this._messages[this._subscriptions[method][params.subscription]];
if (messageId) {
@@ -302,6 +303,16 @@ export default class Ws extends JsonRpcBase {
}
_methodsFromApi (api) {
if (api.subscription) {
const { subscribe, unsubscribe, subscription } = api;
return {
method: subscribe,
uMethod: unsubscribe,
subscription
};
}
const method = `${api}_subscribe`;
const uMethod = `${api}_unsubscribe`;
const subscription = `${api}_subscription`;
@@ -309,7 +320,7 @@ export default class Ws extends JsonRpcBase {
return { method, uMethod, subscription };
}
subscribe (api, callback, ...params) {
subscribe (api, callback, params) {
return new Promise((resolve, reject) => {
const id = this.id;
const { method, uMethod, subscription } = this._methodsFromApi(api);