@parity/api
Version:
The Parity Promise-based API library for interfacing with Ethereum over RPC
507 lines (506 loc) • 21.2 kB
JavaScript
"use strict";
// Copyright 2015-2019 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/>.
var Abi = require('@parity/abi').default;
var nextSubscriptionId = 0;
var Contract = /** @class */ (function () {
function Contract(api, abi) {
var _this = this;
if (!api) {
throw new Error('API instance needs to be provided to Contract');
}
if (!abi) {
throw new Error('ABI needs to be provided to Contract instance');
}
this._api = api;
this._abi = new Abi(abi);
this.getCallData = this.getCallData.bind(this);
this._pollCheckRequest = this._pollCheckRequest.bind(this);
this._pollTransactionReceipt = this._pollTransactionReceipt.bind(this);
this._bindFunction = this._bindFunction.bind(this);
this._bindEvent = this._bindEvent.bind(this);
this._subscribeToChanges = this._subscribeToChanges.bind(this);
this._unsubscribeFromChanges = this._unsubscribeFromChanges.bind(this);
this._subscribeToBlock = this._subscribeToBlock.bind(this);
this._subscribeToPendings = this._subscribeToPendings.bind(this);
this._sendSubscriptionChanges = this._sendSubscriptionChanges.bind(this);
this._subscriptions = {};
this._constructors = this._abi.constructors.map(this._bindFunction);
this._functions = this._abi.functions.map(this._bindFunction);
this._events = this._abi.events.map(this._bindEvent);
this._instance = {};
this._events.forEach(function (evt) {
_this._instance[evt.name] = evt;
_this._instance[evt.signature] = evt;
});
this._functions.forEach(function (fn) {
_this._instance[fn.name] = fn;
_this._instance[fn.signature] = fn;
});
this._subscribedToPendings = false;
this._pendingsSubscriptionId = null;
this._subscribedToBlock = false;
this._blockSubscriptionId = null;
if (api && api.patch && api.patch.contract) {
api.patch.contract(this);
}
}
Object.defineProperty(Contract.prototype, "address", {
get: function () {
return this._address;
},
enumerable: true,
configurable: true
});
Object.defineProperty(Contract.prototype, "constructors", {
get: function () {
return this._constructors;
},
enumerable: true,
configurable: true
});
Object.defineProperty(Contract.prototype, "events", {
get: function () {
return this._events;
},
enumerable: true,
configurable: true
});
Object.defineProperty(Contract.prototype, "functions", {
get: function () {
return this._functions;
},
enumerable: true,
configurable: true
});
Object.defineProperty(Contract.prototype, "receipt", {
get: function () {
return this._receipt;
},
enumerable: true,
configurable: true
});
Object.defineProperty(Contract.prototype, "instance", {
get: function () {
this._instance.address = this._address;
return this._instance;
},
enumerable: true,
configurable: true
});
Object.defineProperty(Contract.prototype, "api", {
get: function () {
return this._api;
},
enumerable: true,
configurable: true
});
Object.defineProperty(Contract.prototype, "abi", {
get: function () {
return this._abi;
},
enumerable: true,
configurable: true
});
Contract.prototype.at = function (address) {
this._address = address;
return this;
};
Contract.prototype.deployEstimateGas = function (options, values) {
var _options = this._encodeOptions(this.constructors[0], options, values);
return this._api.eth.estimateGas(_options).then(function (gasEst) {
return [gasEst, gasEst.multipliedBy(1.2)];
});
};
Contract.prototype.deploy = function (options, values, statecb, skipGasEstimate) {
var _this = this;
if (statecb === void 0) { statecb = function () { }; }
if (skipGasEstimate === void 0) { skipGasEstimate = false; }
var gasEstPromise;
if (skipGasEstimate) {
gasEstPromise = Promise.resolve(null);
}
else {
statecb(null, { state: 'estimateGas' });
gasEstPromise = this.deployEstimateGas(options, values).then(function (_a) {
var gasEst = _a[0], gas = _a[1];
return gas;
});
}
return gasEstPromise.then(function (_gas) {
if (_gas) {
options.gas = _gas.toFixed(0);
}
var gas = _gas || options.gas;
statecb(null, { state: 'postTransaction', gas: gas });
var encodedOptions = _this._encodeOptions(_this.constructors[0], options, values);
return _this._api.parity
.postTransaction(encodedOptions)
.then(function (requestId) {
if (requestId.length !== 66) {
statecb(null, { state: 'checkRequest', requestId: requestId });
return _this._pollCheckRequest(requestId);
}
return requestId;
})
.then(function (txhash) {
statecb(null, { state: 'getTransactionReceipt', txhash: txhash });
return _this._pollTransactionReceipt(txhash, gas);
})
.then(function (receipt) {
if (receipt.gasUsed.eq(gas)) {
throw new Error("Contract not deployed, gasUsed == " + gas.toFixed(0));
}
statecb(null, { state: 'hasReceipt', receipt: receipt });
_this._receipt = receipt;
_this._address = receipt.contractAddress;
return _this._address;
})
.then(function (address) {
statecb(null, { state: 'getCode' });
return _this._api.eth.getCode(_this._address);
})
.then(function (code) {
if (code === '0x') {
throw new Error('Contract not deployed, getCode returned 0x');
}
statecb(null, { state: 'completed' });
return _this._address;
});
});
};
Contract.prototype.parseEventLogs = function (logs) {
var _this = this;
return logs
.map(function (log) {
var signature = log.topics[0].substr(2);
var event = _this.events.find(function (evt) { return evt.signature === signature; });
if (!event) {
console.warn("Unable to find event matching signature " + signature);
return null;
}
try {
var decoded = event.decodeLog(log.topics, log.data);
log.params = {};
log.event = event.name;
decoded.params.forEach(function (param, index) {
var _a = param.token, type = _a.type, value = _a.value;
var key = param.name || index;
log.params[key] = { type: type, value: value };
});
return log;
}
catch (error) {
console.warn('Error decoding log', log);
console.warn(error);
return null;
}
})
.filter(function (log) { return log; });
};
Contract.prototype.parseTransactionEvents = function (receipt) {
receipt.logs = this.parseEventLogs(receipt.logs);
return receipt;
};
Contract.prototype._pollCheckRequest = function (requestId) {
return this._api.pollMethod('parity_checkRequest', requestId);
};
Contract.prototype._pollTransactionReceipt = function (txhash, gas) {
return this.api.pollMethod('eth_getTransactionReceipt', txhash, function (receipt) {
if (!receipt || !receipt.blockNumber || receipt.blockNumber.eq(0)) {
return false;
}
return true;
});
};
Contract.prototype.getCallData = function (func, options, values) {
var data = options.data;
var tokens = func
? Abi.encodeTokens(func.inputParamTypes(), values)
: null;
var call = tokens ? func.encodeCall(tokens) : null;
if (data && data.substr(0, 2) === '0x') {
data = data.substr(2);
}
return "0x" + (data || '') + (call || '');
};
Contract.prototype._encodeOptions = function (func, options, values) {
var data = this.getCallData(func, options, values);
return Object.assign({}, options, { data: data });
};
Contract.prototype._addOptionsTo = function (options) {
if (options === void 0) { options = {}; }
return Object.assign({
to: this._address
}, options);
};
Contract.prototype._bindFunction = function (func) {
var _this = this;
func.contract = this;
func.call = function (_options, values) {
if (_options === void 0) { _options = {}; }
if (values === void 0) { values = []; }
var rawTokens = !!_options.rawTokens;
var options = Object.assign({}, _options);
delete options.rawTokens;
var callParams;
try {
callParams = _this._encodeOptions(func, _this._addOptionsTo(options), values);
}
catch (error) {
return Promise.reject(error);
}
return _this._api.eth
.call(callParams)
.then(function (encoded) { return func.decodeOutput(encoded); })
.then(function (tokens) {
if (rawTokens) {
return tokens;
}
return tokens.map(function (token) { return token.value; });
})
.then(function (returns) { return (returns.length === 1 ? returns[0] : returns); })
.catch(function (error) {
console.warn(func.name + ".call", values, error);
throw error;
});
};
if (!func.constant) {
func.postTransaction = function (options, values) {
if (values === void 0) { values = []; }
var _options;
try {
_options = _this._encodeOptions(func, _this._addOptionsTo(options), values);
}
catch (error) {
return Promise.reject(error);
}
return _this._api.parity.postTransaction(_options).catch(function (error) {
console.warn(func.name + ".postTransaction", values, error);
throw error;
});
};
func.estimateGas = function (options, values) {
if (values === void 0) { values = []; }
var _options = _this._encodeOptions(func, _this._addOptionsTo(options), values);
return _this._api.eth.estimateGas(_options).catch(function (error) {
console.warn(func.name + ".estimateGas", values, error);
throw error;
});
};
}
return func;
};
Contract.prototype._bindEvent = function (event) {
var _this = this;
event.subscribe = function (options, callback, autoRemove) {
if (options === void 0) { options = {}; }
return _this._subscribe(event, options, callback, autoRemove);
};
event.unsubscribe = function (subscriptionId) {
return _this.unsubscribe(subscriptionId);
};
event.getAllLogs = function (options) {
if (options === void 0) { options = {}; }
return _this.getAllLogs(event);
};
return event;
};
Contract.prototype.getAllLogs = function (event, _options) {
var _this = this;
// Options as first parameter
if (!_options && event && event.topics) {
return this.getAllLogs(null, event);
}
var options = this._getFilterOptions(event, _options);
options.fromBlock = 0;
options.toBlock = 'latest';
return this._api.eth
.getLogs(options)
.then(function (logs) { return _this.parseEventLogs(logs); });
};
Contract.prototype._findEvent = function (eventName) {
if (eventName === void 0) { eventName = null; }
var event = eventName
? this._events.find(function (evt) { return evt.name === eventName; })
: null;
if (eventName && !event) {
var events = this._events.map(function (evt) { return evt.name; }).join(', ');
throw new Error(eventName + " is not a valid eventName, subscribe using one of " + events + " (or null to include all)");
}
return event;
};
Contract.prototype._getFilterOptions = function (event, _options) {
if (event === void 0) { event = null; }
if (_options === void 0) { _options = {}; }
var optionTopics = _options.topics || [];
var signature = (event && event.signature) || null;
// If event provided, remove the potential event signature
// as the first element of the topics
var topics = signature
? [signature].concat(optionTopics.filter(function (t, index) { return index > 0 || t !== signature; }))
: optionTopics;
var options = Object.assign({}, _options, {
address: this._address,
topics: topics
});
return options;
};
Contract.prototype._createEthFilter = function (event, _options) {
if (event === void 0) { event = null; }
var options = this._getFilterOptions(event, _options);
return this._api.eth.newFilter(options);
};
Contract.prototype.subscribe = function (eventName, options, callback, autoRemove) {
if (eventName === void 0) { eventName = null; }
if (options === void 0) { options = {}; }
try {
var event_1 = this._findEvent(eventName);
return this._subscribe(event_1, options, callback, autoRemove);
}
catch (e) {
return Promise.reject(e);
}
};
Contract.prototype._sendData = function (subscriptionId, error, logs) {
var _a = this._subscriptions[subscriptionId], autoRemove = _a.autoRemove, callback = _a.callback;
var result = true;
try {
result = callback(error, logs);
}
catch (error) {
console.warn('_sendData', subscriptionId, error);
}
if (autoRemove && result && typeof result === 'boolean') {
this.unsubscribe(subscriptionId);
}
};
Contract.prototype._subscribe = function (event, _options, callback, autoRemove) {
var _this = this;
if (event === void 0) { event = null; }
if (autoRemove === void 0) { autoRemove = false; }
var subscriptionId = nextSubscriptionId++;
var skipInitFetch = _options.skipInitFetch;
delete _options['skipInitFetch'];
return this._createEthFilter(event, _options)
.then(function (filterId) {
_this._subscriptions[subscriptionId] = {
options: _options,
autoRemove: autoRemove,
callback: callback,
filterId: filterId,
id: subscriptionId
};
if (skipInitFetch) {
_this._subscribeToChanges();
return subscriptionId;
}
return _this._api.eth.getFilterLogs(filterId).then(function (logs) {
_this._sendData(subscriptionId, null, _this.parseEventLogs(logs));
_this._subscribeToChanges();
return subscriptionId;
});
})
.catch(function (error) {
console.warn('subscribe', event, _options, error);
throw error;
});
};
Contract.prototype.unsubscribe = function (subscriptionId) {
var _this = this;
return this._api.eth
.uninstallFilter(this._subscriptions[subscriptionId].filterId)
.catch(function (error) {
console.error('unsubscribe', error);
})
.then(function () {
delete _this._subscriptions[subscriptionId];
_this._unsubscribeFromChanges();
});
};
Contract.prototype._subscribeToChanges = function () {
var subscriptions = Object.values(this._subscriptions);
var pendingSubscriptions = subscriptions.filter(function (s) { return s.options.toBlock && s.options.toBlock === 'pending'; });
var otherSubscriptions = subscriptions.filter(function (s) { return !(s.options.toBlock && s.options.toBlock === 'pending'); });
if (pendingSubscriptions.length > 0 && !this._subscribedToPendings) {
this._subscribedToPendings = true;
this._subscribeToPendings();
}
if (otherSubscriptions.length > 0 && !this._subscribedToBlock) {
this._subscribedToBlock = true;
this._subscribeToBlock();
}
};
Contract.prototype._unsubscribeFromChanges = function () {
var subscriptions = Object.values(this._subscriptions);
var pendingSubscriptions = subscriptions.filter(function (s) { return s.options.toBlock && s.options.toBlock === 'pending'; });
var otherSubscriptions = subscriptions.filter(function (s) { return !(s.options.toBlock && s.options.toBlock === 'pending'); });
if (pendingSubscriptions.length === 0 && this._subscribedToPendings) {
this._subscribedToPendings = false;
clearTimeout(this._pendingsSubscriptionId);
}
if (otherSubscriptions.length === 0 && this._subscribedToBlock) {
this._subscribedToBlock = false;
this._api.unsubscribe(this._blockSubscriptionId);
}
};
Contract.prototype._subscribeToBlock = function () {
var _this = this;
this._api
.subscribe('eth_blockNumber', function (error) {
if (error) {
console.error('::_subscribeToBlock', error, error && error.stack);
}
var subscriptions = Object.values(_this._subscriptions).filter(function (s) { return !(s.options.toBlock && s.options.toBlock === 'pending'); });
_this._sendSubscriptionChanges(subscriptions);
})
.then(function (blockSubId) {
_this._blockSubscriptionId = blockSubId;
})
.catch(function (e) {
console.error('::_subscribeToBlock', e, e && e.stack);
});
};
Contract.prototype._subscribeToPendings = function () {
var _this = this;
var subscriptions = Object.values(this._subscriptions).filter(function (s) { return s.options.toBlock && s.options.toBlock === 'pending'; });
var timeout = function () { return setTimeout(function () { return _this._subscribeToPendings(); }, 1000); };
this._sendSubscriptionChanges(subscriptions).then(function () {
_this._pendingsSubscriptionId = timeout();
});
};
Contract.prototype._sendSubscriptionChanges = function (subscriptions) {
var _this = this;
return Promise.all(subscriptions.map(function (subscription) {
return _this._api.eth.getFilterChanges(subscription.filterId);
}))
.then(function (logsArray) {
logsArray.forEach(function (logs, index) {
if (!logs || !logs.length) {
return;
}
try {
_this._sendData(subscriptions[index].id, null, _this.parseEventLogs(logs));
}
catch (error) {
console.error('_sendSubscriptionChanges', error);
}
});
})
.catch(function (error) {
console.error('_sendSubscriptionChanges', error);
});
};
return Contract;
}());
module.exports = Contract;