UNPKG

@parity/api

Version:

The Parity Promise-based API library for interfacing with Ethereum over RPC

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