// 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 . import Abi from '@parity/abi'; let nextSubscriptionId = 0; export default 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._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.mul(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) => { statecb(null, { state: 'checkRequest', requestId }); return this._pollCheckRequest(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 { ...options, data }; } _addOptionsTo (options = {}) { return { to: this._address, ...options }; } _bindFunction = (func) => { func.contract = this; func.call = (_options = {}, values = []) => { const rawTokens = !!_options.rawTokens; const options = { ..._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, idx) => idx > 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); }); } }