49fdd23d58
* Move secureApi to shell * Extract isTestnet test * Use mobx + subscriptions for status * Re-add status indicator * Add lerna * Move intial packages to js/packages * Move 3rdparty/{email,sms}-verification to correct location * Move package.json & README to library src * Move tests for library packages * Move views & dapps to packages * Move i18n to root * Move shell to actual src (main app) * Remove ~ references * Change ~ to root (explicit imports) * Finalise convert of ~ * Move views into dapps as well * Move dapps to packages/ * Fix references * Update css * Update test spec locations * Update tests * Case fix * Skip flakey tests * Update enzyme * Skip previously ignored tests * Allow empty api for hw * Re-add theme for embed
562 lines
15 KiB
JavaScript
562 lines
15 KiB
JavaScript
// 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 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);
|
|
});
|
|
}
|
|
}
|