Refactored Provider events.

This commit is contained in:
Richard Moore 2018-07-12 02:49:09 -04:00
parent ac4211d0c6
commit 27402fafe6
No known key found for this signature in database
GPG Key ID: 525F70A6FCABC295
4 changed files with 416 additions and 597 deletions

View File

@ -2,7 +2,6 @@
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
var provider_1 = require("./provider"); var provider_1 = require("./provider");
exports.Provider = provider_1.Provider; exports.Provider = provider_1.Provider;
exports.ProviderSigner = provider_1.ProviderSigner;
var etherscan_provider_1 = require("./etherscan-provider"); var etherscan_provider_1 = require("./etherscan-provider");
exports.EtherscanProvider = etherscan_provider_1.EtherscanProvider; exports.EtherscanProvider = etherscan_provider_1.EtherscanProvider;
var fallback_provider_1 = require("./fallback-provider"); var fallback_provider_1 = require("./fallback-provider");
@ -25,7 +24,6 @@ exports.getDefaultProvider = getDefaultProvider;
exports.default = { exports.default = {
Provider: provider_1.Provider, Provider: provider_1.Provider,
getDefaultProvider: getDefaultProvider, getDefaultProvider: getDefaultProvider,
ProviderSigner: provider_1.ProviderSigner,
FallbackProvider: fallback_provider_1.FallbackProvider, FallbackProvider: fallback_provider_1.FallbackProvider,
EtherscanProvider: etherscan_provider_1.EtherscanProvider, EtherscanProvider: etherscan_provider_1.EtherscanProvider,
InfuraProvider: infura_provider_1.InfuraProvider, InfuraProvider: infura_provider_1.InfuraProvider,

View File

@ -1,14 +1,4 @@
'use strict'; 'use strict';
var __extends = (this && this.__extends) || (function () {
var extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return function (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
var __importStar = (this && this.__importStar) || function (mod) { var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod; if (mod && mod.__esModule) return mod;
var result = {}; var result = {};
@ -17,64 +7,19 @@ var __importStar = (this && this.__importStar) || function (mod) {
return result; return result;
}; };
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
//import inherits = require('inherits'); var networks_1 = require("./networks");
var wallet_1 = require("../wallet/wallet");
var address_1 = require("../utils/address"); var address_1 = require("../utils/address");
var bignumber_1 = require("../utils/bignumber"); var bignumber_1 = require("../utils/bignumber");
var bytes_1 = require("../utils/bytes"); var bytes_1 = require("../utils/bytes");
var utf8_1 = require("../utils/utf8");
var rlp_1 = require("../utils/rlp");
var hash_1 = require("../utils/hash"); var hash_1 = require("../utils/hash");
var networks_1 = require("./networks");
var properties_1 = require("../utils/properties"); var properties_1 = require("../utils/properties");
var rlp_1 = require("../utils/rlp");
var transaction_1 = require("../utils/transaction"); var transaction_1 = require("../utils/transaction");
var utf8_1 = require("../utils/utf8");
var web_1 = require("../utils/web"); var web_1 = require("../utils/web");
var errors = __importStar(require("../utils/errors")); var errors = __importStar(require("../utils/errors"));
; ;
; ;
function timeoutFunction(setup, cancelled, timeout) {
var timer = null;
var done = false;
return new Promise(function (resolve, reject) {
function cancelTimer() {
if (timer == null) {
return;
}
clearTimeout(timer);
timer = null;
}
function complete(result) {
cancelTimer();
if (done) {
return;
}
resolve(result);
done = true;
}
setup(complete, function (error) {
cancelTimer();
if (done) {
return;
}
reject(error);
done = true;
});
if (typeof (timeout) === 'number' && timeout > 0) {
timer = setTimeout(function () {
cancelTimer();
if (done) {
return;
}
if (cancelled) {
cancelled();
}
reject(new Error('timeout'));
done = true;
}, timeout);
}
});
}
;
////////////////////////////// //////////////////////////////
// Request and Response Checking // Request and Response Checking
// @TODO: not any? // @TODO: not any?
@ -125,7 +70,7 @@ function arrayOf(check) {
} }
function checkHash(hash) { function checkHash(hash) {
if (typeof (hash) === 'string' && bytes_1.hexDataLength(hash) === 32) { if (typeof (hash) === 'string' && bytes_1.hexDataLength(hash) === 32) {
return hash; return hash.toLowerCase();
} }
errors.throwError('invalid hash', errors.INVALID_ARGUMENT, { arg: 'hash', value: hash }); errors.throwError('invalid hash', errors.INVALID_ARGUMENT, { arg: 'hash', value: hash });
return null; return null;
@ -375,133 +320,55 @@ function checkLog(log) {
} }
////////////////////////////// //////////////////////////////
// Event Serializeing // Event Serializeing
function recurse(object, convertFunc) { function serializeTopics(topics) {
if (Array.isArray(object)) { return topics.map(function (topic) {
var result = []; if (typeof (topic) === 'string') {
object.forEach(function (object) { return topic;
result.push(recurse(object, convertFunc)); }
}); else if (Array.isArray(topic)) {
return result; topic.forEach(function (topic) {
} if (topic !== null && bytes_1.hexDataLength(topic) !== 32) {
return convertFunc(object); errors.throwError('invalid topic', errors.INVALID_ARGUMENT, { argument: 'topic', value: topic });
}
});
return topic.join(',');
}
return errors.throwError('invalid topic value', errors.INVALID_ARGUMENT, { argument: 'topic', value: topic });
}).join('&');
} }
function getEventString(object) { function deserializeTopics(data) {
try { return data.split(/&/g).map(function (topic) {
return 'address:' + address_1.getAddress(object); var comps = topic.split(',');
} if (comps.length === 1) {
catch (error) { } if (comps[0] === '') {
if (object === 'block' || object === 'pending' || object === 'error') { return null;
return object;
}
else if (bytes_1.hexDataLength(object) === 32) {
return 'tx:' + object;
}
else if (Array.isArray(object)) {
// Replace null in the structure with '0x'
var stringified = recurse(object, function (object) {
if (object == null) {
object = '0x';
} }
return object; return topic;
});
try {
return 'topic:' + rlp_1.encode(stringified);
} }
catch (error) { return comps;
console.log(error); });
}
}
try {
throw new Error();
}
catch (e) {
console.log(e.stack);
}
throw new Error('invalid event - ' + object);
} }
function parseEventString(event) { function getEventTag(eventName) {
if (event.substring(0, 3) === 'tx:') { if (typeof (eventName) === 'string') {
return { type: 'transaction', hash: event.substring(3) }; if (bytes_1.hexDataLength(eventName) === 20) {
} return 'address:' + address_1.getAddress(eventName);
else if (event === 'block' || event === 'pending' || event === 'error') {
return { type: event };
}
else if (event.substring(0, 8) === 'address:') {
return { type: 'address', address: event.substring(8) };
}
else if (event.substring(0, 6) === 'topic:') {
try {
var object = recurse(rlp_1.decode(event.substring(6)), function (object) {
if (object === '0x') {
object = null;
}
return object;
});
return { type: 'topic', topic: object };
} }
catch (error) { eventName = eventName.toLowerCase();
console.log(error); if (eventName === 'block' || eventName === 'pending' || eventName === 'error') {
return eventName;
}
else if (bytes_1.hexDataLength(eventName) === 32) {
return 'tx:' + eventName;
} }
} }
throw new Error('invalid event string'); else if (Array.isArray(eventName)) {
return 'filter::' + serializeTopics(eventName);
}
else if (eventName && typeof (eventName) === 'object') {
return 'filter:' + (eventName.address || '') + ':' + serializeTopics(eventName.topics || []);
}
throw new Error('invalid event - ' + eventName);
} }
//////////////////////////////
// Provider Object
/* @TODO:
type Event = {
eventName: string,
listener: any, // @TODO: Function any: any
type: string,
}
*/
// @TODO: Perhaps allow a SignDigestAsyncFunc?
// Enable a simple signing function and provider to provide a full Signer
var ProviderSigner = /** @class */ (function (_super) {
__extends(ProviderSigner, _super);
function ProviderSigner(address, signDigest, provider) {
var _this = _super.call(this) || this;
errors.checkNew(_this, ProviderSigner);
properties_1.defineReadOnly(_this, '_addressPromise', Promise.resolve(address));
properties_1.defineReadOnly(_this, 'signDigest', signDigest);
properties_1.defineReadOnly(_this, 'provider', provider);
return _this;
}
ProviderSigner.prototype.getAddress = function () {
return this._addressPromise;
};
ProviderSigner.prototype.signMessage = function (message) {
return Promise.resolve(bytes_1.joinSignature(this.signDigest(bytes_1.arrayify(hash_1.hashMessage(message)))));
};
ProviderSigner.prototype.sendTransaction = function (transaction) {
var _this = this;
transaction = properties_1.shallowCopy(transaction);
if (transaction.chainId == null) {
transaction.chainId = this.provider.getNetwork().then(function (network) {
return network.chainId;
});
}
if (transaction.from == null) {
transaction.from = this.getAddress();
}
if (transaction.gasLimit == null) {
transaction.gasLimit = this.provider.estimateGas(transaction);
}
if (transaction.gasPrice == null) {
transaction.gasPrice = this.provider.getGasPrice();
}
return properties_1.resolveProperties(transaction).then(function (tx) {
var signedTx = transaction_1.serialize(tx, _this.signDigest);
return _this._addressPromise.then(function (address) {
if (transaction_1.parse(signedTx).from !== address) {
errors.throwError('signing address does not match expected address', errors.UNKNOWN_ERROR, { address: transaction_1.parse(signedTx).from, expectedAddress: address, signedTransaction: signedTx });
}
return _this.provider.sendTransaction(signedTx);
});
});
};
return ProviderSigner;
}(wallet_1.Signer));
exports.ProviderSigner = ProviderSigner;
var Provider = /** @class */ (function () { var Provider = /** @class */ (function () {
function Provider(network) { function Provider(network) {
var _this = this; var _this = this;
@ -526,7 +393,7 @@ var Provider = /** @class */ (function () {
// Balances being watched for changes // Balances being watched for changes
this._balances = {}; this._balances = {};
// Events being listened to // Events being listened to
this._events = {}; this._events = [];
this._pollingInterval = 4000; this._pollingInterval = 4000;
// We use this to track recent emitted events; for example, if we emit a "block" of 100 // We use this to track recent emitted events; for example, if we emit a "block" of 100
// and we get a `getBlock(100)` request which would result in null, we should retry // and we get a `getBlock(100)` request which would result in null, we should retry
@ -564,48 +431,56 @@ var Provider = /** @class */ (function () {
// Sweep balances and remove addresses we no longer have events for // Sweep balances and remove addresses we no longer have events for
var newBalances = {}; var newBalances = {};
// Find all transaction hashes we are waiting on // Find all transaction hashes we are waiting on
Object.keys(_this._events).forEach(function (eventName) { _this._events.forEach(function (event) {
var event = parseEventString(eventName); var comps = event.tag.split(':');
if (event.type === 'transaction') { switch (comps[0]) {
_this.getTransactionReceipt(event.hash).then(function (receipt) { case 'tx': {
if (!receipt || receipt.blockNumber == null) { var hash_2 = comps[1];
return; _this.getTransactionReceipt(hash_2).then(function (receipt) {
} if (!receipt || receipt.blockNumber == null) {
_this._emitted['t:' + event.hash.toLowerCase()] = receipt.blockNumber; return;
_this.emit(event.hash, receipt); }
return null; _this._emitted['t:' + hash_2] = receipt.blockNumber;
}).catch(function (error) { }); _this.emit(hash_2, receipt);
} }).catch(function (error) { _this.emit('error', error); });
else if (event.type === 'address') { break;
if (_this._balances[event.address]) {
newBalances[event.address] = _this._balances[event.address];
} }
_this.getBalance(event.address, 'latest').then(function (balance) { case 'address': {
var lastBalance = this._balances[event.address]; var address_2 = comps[1];
if (lastBalance && balance.eq(lastBalance)) { if (_this._balances[address_2]) {
return; newBalances[address_2] = _this._balances[address_2];
} }
this._balances[event.address] = balance; _this.getBalance(address_2, 'latest').then(function (balance) {
this.emit(event.address, balance); var lastBalance = this._balances[address_2];
return null; if (lastBalance && balance.eq(lastBalance)) {
}).catch(function (error) { }); return;
} }
else if (event.type === 'topic') { this._balances[address_2] = balance;
_this.getLogs({ this.emit(address_2, balance);
fromBlock: _this._lastBlockNumber + 1, }).catch(function (error) { _this.emit('error', error); });
toBlock: blockNumber, break;
topics: event.topic }
}).then(function (logs) { case 'filter': {
if (logs.length === 0) { var address = comps[1];
return; var topics = deserializeTopics(comps[2]);
} var filter_1 = {
logs.forEach(function (log) { address: address,
_this._emitted['b:' + log.blockHash.toLowerCase()] = log.blockNumber; fromBlock: _this._lastBlockNumber + 1,
_this._emitted['t:' + log.transactionHash.toLowerCase()] = log.blockNumber; toBlock: blockNumber,
_this.emit(event.topic, log); topics: topics
}); };
return null; _this.getLogs(filter_1).then(function (logs) {
}).catch(function (error) { }); if (logs.length === 0) {
return;
}
logs.forEach(function (log) {
_this._emitted['b:' + log.blockHash] = log.blockNumber;
_this._emitted['t:' + log.transactionHash] = log.blockNumber;
_this.emit(filter_1, log);
});
}).catch(function (error) { _this.emit('error', error); });
break;
}
} }
}); });
_this._lastBlockNumber = blockNumber; _this._lastBlockNumber = blockNumber;
@ -615,7 +490,7 @@ var Provider = /** @class */ (function () {
this.doPoll(); this.doPoll();
}; };
Provider.prototype.resetEventsBlock = function (blockNumber) { Provider.prototype.resetEventsBlock = function (blockNumber) {
this._lastBlockNumber = this.blockNumber; this._lastBlockNumber = blockNumber;
this._doPoll(); this._doPoll();
}; };
Object.defineProperty(Provider.prototype, "network", { Object.defineProperty(Provider.prototype, "network", {
@ -679,17 +554,14 @@ var Provider = /** @class */ (function () {
// this will be used once we move to the WebSocket or other alternatives to polling // this will be used once we move to the WebSocket or other alternatives to polling
Provider.prototype.waitForTransaction = function (transactionHash, timeout) { Provider.prototype.waitForTransaction = function (transactionHash, timeout) {
var _this = this; var _this = this;
var complete = null; return web_1.poll(function () {
var setup = function (resolve) { return _this.getTransactionReceipt(transactionHash).then(function (receipt) {
complete = function (receipt) { if (receipt == null) {
resolve(receipt); return undefined;
}; }
_this.once(transactionHash, complete); return receipt;
}; });
var cancelled = function () { }, { onceBlock: this });
_this.removeListener(transactionHash, complete);
};
return timeoutFunction(setup, cancelled, timeout);
}; };
Provider.prototype.getBlockNumber = function () { Provider.prototype.getBlockNumber = function () {
var _this = this; var _this = this;
@ -800,7 +672,7 @@ var Provider = /** @class */ (function () {
if (hash != null && tx.hash !== hash) { if (hash != null && tx.hash !== hash) {
errors.throwError('Transaction hash mismatch from Proivder.sendTransaction.', errors.UNKNOWN_ERROR, { expectedHash: tx.hash, returnedHash: hash }); errors.throwError('Transaction hash mismatch from Proivder.sendTransaction.', errors.UNKNOWN_ERROR, { expectedHash: tx.hash, returnedHash: hash });
} }
this._emitted['t:' + tx.hash.toLowerCase()] = 'pending'; this._emitted['t:' + tx.hash] = 'pending';
result.wait = function (timeout) { result.wait = function (timeout) {
return _this.waitForTransaction(hash, timeout).then(function (receipt) { return _this.waitForTransaction(hash, timeout).then(function (receipt) {
if (receipt.status === 0) { if (receipt.status === 0) {
@ -856,7 +728,7 @@ var Provider = /** @class */ (function () {
return web_1.poll(function () { return web_1.poll(function () {
return _this.perform('getBlock', { blockHash: blockHash }).then(function (block) { return _this.perform('getBlock', { blockHash: blockHash }).then(function (block) {
if (block == null) { if (block == null) {
if (_this._emitted['b:' + blockHash.toLowerCase()] == null) { if (_this._emitted['b:' + blockHash] == null) {
return null; return null;
} }
return undefined; return undefined;
@ -899,7 +771,7 @@ var Provider = /** @class */ (function () {
return web_1.poll(function () { return web_1.poll(function () {
return _this.perform('getTransaction', params).then(function (result) { return _this.perform('getTransaction', params).then(function (result) {
if (result == null) { if (result == null) {
if (_this._emitted['t:' + transactionHash.toLowerCase()] == null) { if (_this._emitted['t:' + transactionHash] == null) {
return null; return null;
} }
return undefined; return undefined;
@ -919,7 +791,7 @@ var Provider = /** @class */ (function () {
return web_1.poll(function () { return web_1.poll(function () {
return _this.perform('getTransactionReceipt', params).then(function (result) { return _this.perform('getTransactionReceipt', params).then(function (result) {
if (result == null) { if (result == null) {
if (_this._emitted['t:' + transactionHash.toLowerCase()] == null) { if (_this._emitted['t:' + transactionHash] == null) {
return null; return null;
} }
return undefined; return undefined;
@ -1076,117 +948,118 @@ var Provider = /** @class */ (function () {
}; };
Provider.prototype._stopPending = function () { Provider.prototype._stopPending = function () {
}; };
Provider.prototype.on = function (eventName, listener) { Provider.prototype._addEventListener = function (eventName, listener, once) {
var key = getEventString(eventName); this._events.push({
if (!this._events[key]) { tag: getEventTag(eventName),
this._events[key] = []; listener: listener,
} once: once,
this._events[key].push({ eventName: eventName, listener: listener, type: 'on' }); });
if (key === 'pending') { if (eventName === 'pending') {
this._startPending(); this._startPending();
} }
this.polling = true; this.polling = true;
};
Provider.prototype.on = function (eventName, listener) {
this._addEventListener(eventName, listener, false);
return this; return this;
}; };
Provider.prototype.once = function (eventName, listener) { Provider.prototype.once = function (eventName, listener) {
var key = getEventString(eventName); this._addEventListener(eventName, listener, true);
if (!this._events[key]) {
this._events[key] = [];
}
this._events[key].push({ eventName: eventName, listener: listener, type: 'once' });
if (key === 'pending') {
this._startPending();
}
this.polling = true;
return this; return this;
}; };
Provider.prototype.addEventListener = function (eventName, listener) {
return this.on(eventName, listener);
};
Provider.prototype.emit = function (eventName) { Provider.prototype.emit = function (eventName) {
var _this = this;
var args = []; var args = [];
for (var _i = 1; _i < arguments.length; _i++) { for (var _i = 1; _i < arguments.length; _i++) {
args[_i - 1] = arguments[_i]; args[_i - 1] = arguments[_i];
} }
var result = false; var result = false;
var key = getEventString(eventName); var eventTag = getEventTag(eventName);
//var args = Array.prototype.slice.call(arguments, 1); this._events = this._events.filter(function (event) {
var listeners = this._events[key]; if (event.tag !== eventTag) {
if (!listeners) { return true;
return result;
}
for (var i = 0; i < listeners.length; i++) {
var listener = listeners[i];
if (listener.type === 'once') {
listeners.splice(i, 1);
i--;
} }
try { setTimeout(function () {
listener.listener.apply(this, args); event.listener.apply(_this, args);
result = true; }, 0);
} result = true;
catch (error) { return !(event.once);
console.log('Event Listener Error: ' + error.message); });
}
}
if (listeners.length === 0) {
delete this._events[key];
if (key === 'pending') {
this._stopPending();
}
}
if (this.listenerCount() === 0) {
this.polling = false;
}
return result; return result;
}; };
// @TODO: type EventName
Provider.prototype.listenerCount = function (eventName) { Provider.prototype.listenerCount = function (eventName) {
if (!eventName) { if (!eventName) {
var result = 0; return this._events.length;
for (var key in this._events) {
result += this._events[key].length;
}
return result;
} }
var listeners = this._events[getEventString(eventName)]; var eventTag = getEventTag(eventName);
if (!listeners) { return this._events.filter(function (event) {
return 0; return (event.tag === eventTag);
} }).length;
return listeners.length;
}; };
Provider.prototype.listeners = function (eventName) { Provider.prototype.listeners = function (eventName) {
var listeners = this._events[getEventString(eventName)]; var eventTag = getEventTag(eventName);
if (!listeners) { return this._events.filter(function (event) {
return []; return (event.tag === eventTag);
} }).map(function (event) {
var result = []; return event.listener;
for (var i = 0; i < listeners.length; i++) { });
result.push(listeners[i].listener);
}
return result;
}; };
Provider.prototype.removeAllListeners = function (eventName) { Provider.prototype.removeAllListeners = function (eventName) {
delete this._events[getEventString(eventName)]; var eventTag = getEventTag(eventName);
if (this.listenerCount() === 0) { this._events = this._events.filter(function (event) {
return (event.tag !== eventTag);
});
if (eventName === 'pending') {
this._stopPending();
}
if (this._events.length === 0) {
this.polling = false; this.polling = false;
} }
return this; return this;
}; };
Provider.prototype.removeListener = function (eventName, listener) { Provider.prototype.removeListener = function (eventName, listener) {
var eventNameString = getEventString(eventName); var found = false;
var listeners = this._events[eventNameString]; var eventTag = getEventTag(eventName);
if (!listeners) { this._events = this._events.filter(function (event) {
return this; if (event.tag !== eventTag) {
} return true;
for (var i = 0; i < listeners.length; i++) {
if (listeners[i].listener === listener) {
listeners.splice(i, 1);
break;
} }
if (found) {
return true;
}
found = false;
return false;
});
if (eventName === 'pending' && this.listenerCount('pending') === 0) {
this._stopPending();
} }
if (listeners.length === 0) { if (this.listenerCount() === 0) {
this.removeAllListeners(eventName); this.polling = false;
} }
return this; return this;
}; };
return Provider; return Provider;
}()); }());
exports.Provider = Provider; exports.Provider = Provider;
// See: https://github.com/isaacs/inherits/blob/master/inherits_browser.js
function inherits(ctor, superCtor) {
ctor.super_ = superCtor;
ctor.prototype = Object.create(superCtor.prototype, {
constructor: {
value: ctor,
enumerable: false,
writable: true,
configurable: true
}
});
}
function inheritable(parent) {
return function (child) {
inherits(child, parent);
properties_1.defineReadOnly(child, 'inherits', inheritable(child));
};
}
properties_1.defineReadOnly(Provider, 'inherits', inheritable(Provider));

View File

@ -1,6 +1,6 @@
'use strict'; 'use strict';
import { Provider, ProviderSigner } from './provider'; import { Provider } from './provider';
import { Network } from './networks'; import { Network } from './networks';
@ -22,8 +22,6 @@ export {
Provider, Provider,
getDefaultProvider, getDefaultProvider,
ProviderSigner,
FallbackProvider, FallbackProvider,
EtherscanProvider, EtherscanProvider,
@ -38,8 +36,6 @@ export default {
Provider, Provider,
getDefaultProvider, getDefaultProvider,
ProviderSigner,
FallbackProvider, FallbackProvider,
EtherscanProvider, EtherscanProvider,

View File

@ -1,18 +1,15 @@
'use strict'; 'use strict';
//import inherits = require('inherits'); import { getNetwork, Network, Networkish } from './networks';
import { Signer } from '../wallet/wallet';
import { getAddress, getContractAddress } from '../utils/address'; import { getAddress, getContractAddress } from '../utils/address';
import { BigNumber, bigNumberify, BigNumberish } from '../utils/bignumber'; import { BigNumber, bigNumberify, BigNumberish } from '../utils/bignumber';
import { arrayify, Arrayish, hexDataLength, hexDataSlice, hexlify, hexStripZeros, isHexString, joinSignature, stripZeros } from '../utils/bytes'; import { Arrayish, hexDataLength, hexDataSlice, hexlify, hexStripZeros, isHexString, stripZeros } from '../utils/bytes';
import { toUtf8String } from '../utils/utf8'; import { namehash } from '../utils/hash';
import { decode as rlpDecode, encode as rlpEncode } from '../utils/rlp';
import { hashMessage, namehash } from '../utils/hash';
import { getNetwork, Network, Networkish } from './networks';
import { defineReadOnly, resolveProperties, shallowCopy } from '../utils/properties'; import { defineReadOnly, resolveProperties, shallowCopy } from '../utils/properties';
import { parse as parseTransaction, serialize as serializeTransaction, SignDigestFunc, Transaction } from '../utils/transaction'; import { encode as rlpEncode } from '../utils/rlp';
import { parse as parseTransaction, Transaction } from '../utils/transaction';
import { toUtf8String } from '../utils/utf8';
import { poll } from '../utils/web'; import { poll } from '../utils/web';
import * as errors from '../utils/errors'; import * as errors from '../utils/errors';
@ -89,10 +86,9 @@ export type Filter = {
fromBlock?: BlockTag, fromBlock?: BlockTag,
toBlock?: BlockTag, toBlock?: BlockTag,
address?: string, address?: string,
topics?: Array<any> topics?: Array<string | Array<string>>,
} }
// @TODO: Some of these are not options; force them?
export interface Log { export interface Log {
blockNumber?: number; blockNumber?: number;
blockHash?: string; blockHash?: string;
@ -103,59 +99,14 @@ export interface Log {
transactionLogIndex?: number, transactionLogIndex?: number,
address: string; address: string;
data?: string; data: string;
topics?: Array<string>; topics: Array<string>;
transactionHash?: string; transactionHash?: string;
logIndex?: number; logIndex?: number;
} }
export type Listener = (...args: Array<any>) => void;
//////////////////////////////
// Request and Response Checking
type ResolveFunc = (result: any) => void;
type RejectFunc = (error: Error) => void;
type SetupFunc = (resolve: ResolveFunc, reject: RejectFunc) => void;
type CancelledFunc = () => void;
function timeoutFunction(setup: SetupFunc, cancelled: CancelledFunc, timeout: number): Promise<any> {
var timer: any = null;
var done = false;
return new Promise(function(resolve, reject) {
function cancelTimer() {
if (timer == null) { return; }
clearTimeout(timer);
timer = null;
}
function complete(result: any): void {
cancelTimer();
if (done) { return; }
resolve(result);
done = true;
}
setup(complete, (error: Error) => {
cancelTimer();
if (done) { return; }
reject(error);
done = true;
});
if (typeof(timeout) === 'number' && timeout > 0) {
timer = setTimeout(function() {
cancelTimer();
if (done) { return; }
if (cancelled) { cancelled(); }
reject(new Error('timeout'));
done = true;
}, timeout);
}
});
};
////////////////////////////// //////////////////////////////
// Request and Response Checking // Request and Response Checking
@ -208,7 +159,7 @@ function arrayOf(check: CheckFunc): CheckFunc {
function checkHash(hash: any): string { function checkHash(hash: any): string {
if (typeof(hash) === 'string' && hexDataLength(hash) === 32) { if (typeof(hash) === 'string' && hexDataLength(hash) === 32) {
return hash; return hash.toLowerCase();
} }
errors.throwError('invalid hash', errors.INVALID_ARGUMENT, { arg: 'hash', value: hash }); errors.throwError('invalid hash', errors.INVALID_ARGUMENT, { arg: 'hash', value: hash });
return null; return null;
@ -509,74 +460,58 @@ function checkLog(log: any): any {
////////////////////////////// //////////////////////////////
// Event Serializeing // Event Serializeing
function recurse(object: any, convertFunc: (object: any) => any): any { function serializeTopics(topics: Array<string | Array<string>>): string {
if (Array.isArray(object)) { return topics.map((topic) => {
var result: any = []; if (typeof(topic) === 'string') {
object.forEach(function(object) { return topic;
result.push(recurse(object, convertFunc)); } else if (Array.isArray(topic)) {
}); topic.forEach((topic) => {
return result; if (topic !== null && hexDataLength(topic) !== 32) {
} errors.throwError('invalid topic', errors.INVALID_ARGUMENT, { argument: 'topic', value: topic });
return convertFunc(object); }
}
function getEventString(object: any): string {
try {
return 'address:' + getAddress(object);
} catch (error) { }
if (object === 'block' || object === 'pending' || object === 'error') {
return object;
} else if (hexDataLength(object) === 32) {
return 'tx:' + object;
} else if (Array.isArray(object)) {
// Replace null in the structure with '0x'
let stringified: any = recurse(object, function(object: any) {
if (object == null) { object = '0x'; }
return object;
});
try {
return 'topic:' + rlpEncode(stringified);
} catch (error) {
console.log(error);
}
}
try {
throw new Error();
} catch(e) {
console.log(e.stack);
}
throw new Error('invalid event - ' + object);
}
function parseEventString(event: string): { type: string, address?: string, hash?: string, topic?: any } {
if (event.substring(0, 3) === 'tx:') {
return { type: 'transaction', hash: event.substring(3) };
} else if (event === 'block' || event === 'pending' || event === 'error') {
return { type: event };
} else if (event.substring(0, 8) === 'address:') {
return { type: 'address', address: event.substring(8) };
} else if (event.substring(0, 6) === 'topic:') {
try {
let object = recurse(rlpDecode(event.substring(6)), function(object: any) {
if (object === '0x') { object = null; }
return object;
}); });
return { type: 'topic', topic: object }; return topic.join(',');
} catch (error) {
console.log(error);
} }
return errors.throwError('invalid topic value', errors.INVALID_ARGUMENT, { argument: 'topic', value: topic });
}).join('&');
}
function deserializeTopics(data: string): Array<string | Array<string>> {
return data.split(/&/g).map((topic) => {
let comps = topic.split(',');
if (comps.length === 1) {
if (comps[0] === '') { return null; }
return topic;
}
return comps;
});
}
function getEventTag(eventName: EventType): string {
if (typeof(eventName) === 'string') {
if (hexDataLength(eventName) === 20) {
return 'address:' + getAddress(eventName);
}
eventName = eventName.toLowerCase();
if (eventName === 'block' || eventName === 'pending' || eventName === 'error') {
return eventName;
} else if (hexDataLength(eventName) === 32) {
return 'tx:' + eventName;
}
} else if (Array.isArray(eventName)) {
return 'filter::' + serializeTopics(eventName);
} else if (eventName && typeof(eventName) === 'object') {
return 'filter:' + (eventName.address || '') + ':' + serializeTopics(eventName.topics || []);
} }
throw new Error('invalid event string'); throw new Error('invalid event - ' + eventName);
} }
////////////////////////////// //////////////////////////////
// Provider Object // Provider Object
@ -592,6 +527,8 @@ type Event = {
// @TODO: Perhaps allow a SignDigestAsyncFunc? // @TODO: Perhaps allow a SignDigestAsyncFunc?
// Enable a simple signing function and provider to provide a full Signer // Enable a simple signing function and provider to provide a full Signer
/*
export type SignDigestFunc = (digest: string) => Promise<Signature>;
export class ProviderSigner extends Signer { export class ProviderSigner extends Signer {
readonly provider: Provider; readonly provider: Provider;
readonly signDigest: SignDigestFunc; readonly signDigest: SignDigestFunc;
@ -611,7 +548,9 @@ export class ProviderSigner extends Signer {
} }
signMessage(message: Arrayish | string): Promise<string> { signMessage(message: Arrayish | string): Promise<string> {
return Promise.resolve(joinSignature(this.signDigest(arrayify(hashMessage(message))))); return this.signDigest(arrayify(hashMessage(message))).then((signature) => {
return joinSignature(signature);
});
} }
sendTransaction(transaction: TransactionRequest): Promise<TransactionResponse> { sendTransaction(transaction: TransactionRequest): Promise<TransactionResponse> {
@ -636,22 +575,50 @@ export class ProviderSigner extends Signer {
} }
return resolveProperties(transaction).then((tx) => { return resolveProperties(transaction).then((tx) => {
let signedTx = serializeTransaction(tx, this.signDigest); let unsignedTx = serializeTransaction(tx);
return this._addressPromise.then((address) => { return this.signDigest(keccak256(unsignedTx)).then((signature) => {
if (parseTransaction(signedTx).from !== address) { let signedTx = serializeTransaxction(tx, (ut) => {
errors.throwError('signing address does not match expected address', errors.UNKNOWN_ERROR, { address: parseTransaction(signedTx).from, expectedAddress: address, signedTransaction: signedTx }); if (unsignedTx !== ut) { throw new Error('this should not happen'); }
} return signature;
return this.provider.sendTransaction(signedTx); });
return this._addressPromise.then((address) => {
if (parseTransaction(signedTx).from !== address) {
errors.throwError('signing address does not match expected address', errors.UNKNOWN_ERROR, { address: parseTransaction(signedTx).from, expectedAddress: address, signedTransaction: signedTx });
}
return this.provider.sendTransaction(signedTx);
});
}); });
}); });
} }
} }
*/
export type Listener = (...args: Array<any>) => void;
/**
* EventType
* - "block"
* - "pending"
* - "error"
* - address
* - filter
* - topics array
* - transaction hash
*/
export type EventType = string | Array<string> | Filter;
type _Event = {
listener: Listener;
once: boolean;
tag: string;
}
export class Provider { export class Provider {
private _network: Network; private _network: Network;
// string => Event private _events: Array<_Event>;
private _events: any;
protected _emitted: any; protected _emitted: any;
private _pollingInterval: number; private _pollingInterval: number;
@ -700,7 +667,7 @@ export class Provider {
this._balances = {}; this._balances = {};
// Events being listened to // Events being listened to
this._events = {}; this._events = [];
this._pollingInterval = 4000; this._pollingInterval = 4000;
@ -741,43 +708,52 @@ export class Provider {
var newBalances: any = {}; var newBalances: any = {};
// Find all transaction hashes we are waiting on // Find all transaction hashes we are waiting on
Object.keys(this._events).forEach((eventName) => { this._events.forEach((event) => {
var event = parseEventString(eventName); let comps = event.tag.split(':');
switch (comps[0]) {
if (event.type === 'transaction') { case 'tx': {
this.getTransactionReceipt(event.hash).then((receipt) => { let hash = comps[1];
if (!receipt || receipt.blockNumber == null) { return; } this.getTransactionReceipt(hash).then((receipt) => {
this._emitted['t:' + event.hash.toLowerCase()] = receipt.blockNumber; if (!receipt || receipt.blockNumber == null) { return; }
this.emit(event.hash, receipt); this._emitted['t:' + hash] = receipt.blockNumber;
return null; this.emit(hash, receipt);
}).catch((error: Error) => { }); }).catch((error: Error) => { this.emit('error', error); });
break;
} else if (event.type === 'address') {
if (this._balances[event.address]) {
newBalances[event.address] = this._balances[event.address];
} }
this.getBalance(event.address, 'latest').then(function(balance) {
var lastBalance = this._balances[event.address];
if (lastBalance && balance.eq(lastBalance)) { return; }
this._balances[event.address] = balance;
this.emit(event.address, balance);
return null;
}).catch((error: Error) => { });
} else if (event.type === 'topic') { case 'address': {
this.getLogs({ let address = comps[1];
fromBlock: this._lastBlockNumber + 1, if (this._balances[address]) {
toBlock: blockNumber, newBalances[address] = this._balances[address];
topics: event.topic }
}).then((logs) => { this.getBalance(address, 'latest').then(function(balance) {
if (logs.length === 0) { return; } var lastBalance = this._balances[address];
logs.forEach((log) => { if (lastBalance && balance.eq(lastBalance)) { return; }
this._emitted['b:' + log.blockHash.toLowerCase()] = log.blockNumber; this._balances[address] = balance;
this._emitted['t:' + log.transactionHash.toLowerCase()] = log.blockNumber; this.emit(address, balance);
this.emit(event.topic, log); }).catch((error: Error) => { this.emit('error', error); });
}); break;
return null; }
}).catch((error: Error) => { });
case 'filter': {
let address = comps[1];
let topics = deserializeTopics(comps[2]);
let filter = {
address: address,
fromBlock: this._lastBlockNumber + 1,
toBlock: blockNumber,
topics: topics
}
this.getLogs(filter).then((logs) => {
if (logs.length === 0) { return; }
logs.forEach((log: Log) => {
this._emitted['b:' + log.blockHash] = log.blockNumber;
this._emitted['t:' + log.transactionHash] = log.blockNumber;
this.emit(filter, log);
});
}).catch((error: Error) => { this.emit('error', error); });
break;
}
} }
}); });
@ -791,7 +767,7 @@ export class Provider {
} }
resetEventsBlock(blockNumber: number): void { resetEventsBlock(blockNumber: number): void {
this._lastBlockNumber = this.blockNumber; this._lastBlockNumber = blockNumber;
this._doPoll(); this._doPoll();
} }
@ -845,21 +821,12 @@ export class Provider {
// this will be used once we move to the WebSocket or other alternatives to polling // this will be used once we move to the WebSocket or other alternatives to polling
waitForTransaction(transactionHash: string, timeout?: number): Promise<TransactionReceipt> { waitForTransaction(transactionHash: string, timeout?: number): Promise<TransactionReceipt> {
let complete: Listener = null return poll(() => {
return this.getTransactionReceipt(transactionHash).then((receipt) => {
var setup = (resolve: ResolveFunc) => { if (receipt == null) { return undefined; }
complete = (receipt) => { return receipt;
resolve(receipt); });
}; }, { onceBlock: this });
this.once(transactionHash, complete);
};
var cancelled = () => {
this.removeListener(transactionHash, complete);
};
return timeoutFunction(setup, cancelled, timeout);
} }
getBlockNumber(): Promise<number> { getBlockNumber(): Promise<number> {
@ -965,7 +932,7 @@ export class Provider {
errors.throwError('Transaction hash mismatch from Proivder.sendTransaction.', errors.UNKNOWN_ERROR, { expectedHash: tx.hash, returnedHash: hash }); errors.throwError('Transaction hash mismatch from Proivder.sendTransaction.', errors.UNKNOWN_ERROR, { expectedHash: tx.hash, returnedHash: hash });
} }
this._emitted['t:' + tx.hash.toLowerCase()] = 'pending'; this._emitted['t:' + tx.hash] = 'pending';
result.wait = (timeout?: number) => { result.wait = (timeout?: number) => {
return this.waitForTransaction(hash, timeout).then((receipt) => { return this.waitForTransaction(hash, timeout).then((receipt) => {
if (receipt.status === 0) { if (receipt.status === 0) {
@ -1023,7 +990,7 @@ export class Provider {
return poll(() => { return poll(() => {
return this.perform('getBlock', { blockHash: blockHash }).then((block) => { return this.perform('getBlock', { blockHash: blockHash }).then((block) => {
if (block == null) { if (block == null) {
if (this._emitted['b:' + blockHash.toLowerCase()] == null) { if (this._emitted['b:' + blockHash] == null) {
return null; return null;
} }
return undefined; return undefined;
@ -1068,7 +1035,7 @@ export class Provider {
return poll(() => { return poll(() => {
return this.perform('getTransaction', params).then((result) => { return this.perform('getTransaction', params).then((result) => {
if (result == null) { if (result == null) {
if (this._emitted['t:' + transactionHash.toLowerCase()] == null) { if (this._emitted['t:' + transactionHash] == null) {
return null; return null;
} }
return undefined; return undefined;
@ -1087,7 +1054,7 @@ export class Provider {
return poll(() => { return poll(() => {
return this.perform('getTransactionReceipt', params).then((result) => { return this.perform('getTransactionReceipt', params).then((result) => {
if (result == null) { if (result == null) {
if (this._emitted['t:' + transactionHash.toLowerCase()] == null) { if (this._emitted['t:' + transactionHash] == null) {
return null; return null;
} }
return undefined; return undefined;
@ -1099,7 +1066,7 @@ export class Provider {
}); });
} }
getLogs(filter: Filter): Promise<Array<Log>>{ getLogs(filter: Filter): Promise<Array<Log>> {
return this.ready.then(() => { return this.ready.then(() => {
return resolveProperties(filter).then((filter) => { return resolveProperties(filter).then((filter) => {
return this._resolveNames(filter, ['address']).then((filter) => { return this._resolveNames(filter, ['address']).then((filter) => {
@ -1261,127 +1228,112 @@ export class Provider {
_stopPending(): void { _stopPending(): void {
} }
on(eventName: any, listener: Listener): Provider { _addEventListener(eventName: EventType, listener: Listener, once: boolean): void {
var key = getEventString(eventName); this._events.push({
if (!this._events[key]) { this._events[key] = []; } tag: getEventTag(eventName),
this._events[key].push({eventName: eventName, listener: listener, type: 'on'}); listener: listener,
if (key === 'pending') { this._startPending(); } once: once,
});
if (eventName === 'pending') { this._startPending(); }
this.polling = true; this.polling = true;
}
on(eventName: EventType, listener: Listener): Provider {
this._addEventListener(eventName, listener, false);
return this; return this;
} }
once(eventName: any, listener: Listener): Provider { once(eventName: EventType, listener: Listener): Provider {
var key = getEventString(eventName); this._addEventListener(eventName, listener, true);
if (!this._events[key]) { this._events[key] = []; }
this._events[key].push({eventName: eventName, listener: listener, type: 'once'});
if (key === 'pending') { this._startPending(); }
this.polling = true;
return this; return this;
} }
emit(eventName: any, ...args: Array<any>): boolean { addEventListener(eventName: EventType, listener: Listener): Provider {
return this.on(eventName, listener);
}
emit(eventName: EventType, ...args: Array<any>): boolean {
let result = false; let result = false;
var key = getEventString(eventName); let eventTag = getEventTag(eventName);
this._events = this._events.filter((event) => {
//var args = Array.prototype.slice.call(arguments, 1); if (event.tag !== eventTag) { return true; }
var listeners = this._events[key]; setTimeout(() => {
if (!listeners) { return result; } event.listener.apply(this, args);
}, 0);
for (var i = 0; i < listeners.length; i++) { result = true;
var listener = listeners[i]; return !(event.once);
if (listener.type === 'once') { });
listeners.splice(i, 1);
i--;
}
try {
listener.listener.apply(this, args);
result = true;
} catch (error) {
console.log('Event Listener Error: ' + error.message);
}
}
if (listeners.length === 0) {
delete this._events[key];
if (key === 'pending') { this._stopPending(); }
}
if (this.listenerCount() === 0) { this.polling = false; }
return result; return result;
} }
// @TODO: type EventName listenerCount(eventName?: EventType): number {
listenerCount(eventName?: any): number { if (!eventName) { return this._events.length; }
if (!eventName) {
var result = 0;
for (var key in this._events) {
result += this._events[key].length;
}
return result;
}
var listeners = this._events[getEventString(eventName)]; let eventTag = getEventTag(eventName);
if (!listeners) { return 0; } return this._events.filter((event) => {
return listeners.length; return (event.tag === eventTag);
}).length;
} }
listeners(eventName: any): Array<Listener> { listeners(eventName: EventType): Array<Listener> {
var listeners = this._events[getEventString(eventName)]; let eventTag = getEventTag(eventName);
if (!listeners) { return []; } return this._events.filter((event) => {
var result = []; return (event.tag === eventTag);
for (var i = 0; i < listeners.length; i++) { }).map((event) => {
result.push(listeners[i].listener); return event.listener;
} });
return result;
} }
removeAllListeners(eventName: any): Provider { removeAllListeners(eventName: EventType): Provider {
delete this._events[getEventString(eventName)]; let eventTag = getEventTag(eventName);
if (this.listenerCount() === 0) { this.polling = false; } this._events = this._events.filter((event) => {
return (event.tag !== eventTag);
});
if (eventName === 'pending') { this._stopPending(); }
if (this._events.length === 0) { this.polling = false; }
return this; return this;
} }
removeListener(eventName: any, listener: Listener): Provider { removeListener(eventName: EventType, listener: Listener): Provider {
var eventNameString = getEventString(eventName); let found = false;
var listeners = this._events[eventNameString];
if (!listeners) { return this; }
for (var i = 0; i < listeners.length; i++) { let eventTag = getEventTag(eventName);
if (listeners[i].listener === listener) { this._events = this._events.filter((event) => {
listeners.splice(i, 1); if (event.tag !== eventTag) { return true; }
break; if (found) { return true; }
} found = false;
} return false;
});
if (listeners.length === 0) { if (eventName === 'pending' && this.listenerCount('pending') === 0) { this._stopPending(); }
this.removeAllListeners(eventName); if (this.listenerCount() === 0) { this.polling = false; }
}
return this; return this;
} }
} }
/* // See: https://github.com/isaacs/inherits/blob/master/inherits_browser.js
function inheritable(parent) { function inherits(ctor: any, superCtor: any): void {
return function(child) { ctor.super_ = superCtor
ctor.prototype = Object.create(superCtor.prototype, {
constructor: {
value: ctor,
enumerable: false,
writable: true,
configurable: true
}
});
}
function inheritable(parent: any): (child: any) => void {
return function(child: any): void {
inherits(child, parent); inherits(child, parent);
defineProperty(child, 'inherits', inheritable(child)); defineReadOnly(child, 'inherits', inheritable(child));
} }
} }
defineProperty(Provider, 'inherits', inheritable(Provider)); defineReadOnly(Provider, 'inherits', inheritable(Provider));
*/
/*
function(child) {
inherits(child, Provider);
child.inherits = function(grandchild) {
inherits(grandchild, child)
}
});
*/