embark-area-51/lib/modules/blockchain_connector/index.js

462 lines
14 KiB
JavaScript
Raw Normal View History

2018-05-18 21:15:49 +00:00
const Web3 = require('web3');
const async = require('async');
2018-05-18 18:58:05 +00:00
const Provider = require('./provider.js');
2018-08-22 22:32:43 +00:00
const utils = require('../../utils/utils');
const WEB3_READY = 'blockchain:ready';
2018-05-18 20:51:03 +00:00
2018-08-22 22:32:43 +00:00
// TODO: consider another name, this is the blockchain connector
class BlockchainConnector {
constructor(embark, options) {
2018-07-20 12:21:23 +00:00
const self = this;
2018-05-18 20:51:03 +00:00
this.plugins = options.plugins;
this.logger = embark.logger;
this.events = embark.events;
this.config = embark.config;
2018-05-18 21:15:49 +00:00
this.web3 = options.web3;
2018-05-23 15:52:07 +00:00
this.isDev = options.isDev;
2018-05-23 13:54:13 +00:00
this.web3Endpoint = '';
this.isWeb3Ready = false;
2018-10-09 20:43:38 +00:00
this.wait = options.wait;
2018-05-18 18:58:05 +00:00
2018-07-20 12:21:23 +00:00
self.events.setCommandHandler("blockchain:web3:isReady", (cb) => {
cb(self.isWeb3Ready);
});
2018-08-22 22:32:43 +00:00
self.events.setCommandHandler("blockchain:object", (cb) => {
cb(self);
});
2018-05-18 21:15:49 +00:00
if (!this.web3) {
this.initWeb3();
2018-05-23 13:54:13 +00:00
} else {
this.isWeb3Ready = true;
2018-05-18 21:15:49 +00:00
}
2018-10-09 20:43:38 +00:00
2018-07-20 12:21:23 +00:00
this.registerServiceCheck();
this.registerRequests();
2018-07-31 14:46:19 +00:00
this.registerAPIRequests();
2018-05-23 15:16:56 +00:00
this.registerWeb3Object();
2018-07-20 12:21:23 +00:00
this.registerEvents();
}
2018-05-22 21:34:54 +00:00
initWeb3(cb) {
if (!cb) {
cb = function(){};
}
if (this.isWeb3Ready) {
2018-07-17 13:13:12 +00:00
this.events.emit(WEB3_READY);
return cb();
}
2018-05-18 18:58:05 +00:00
const self = this;
2018-05-18 21:15:49 +00:00
this.web3 = new Web3();
2018-05-18 18:58:05 +00:00
2018-10-09 20:43:38 +00:00
if (self.wait) {
self.wait = false;
2018-10-09 20:43:38 +00:00
return cb();
}
let {type, host, port, accounts, protocol, coverage} = this.config.contractsConfig.deployment;
2018-10-09 20:43:38 +00:00
if (!BlockchainConnector.ACCEPTED_TYPES.includes(type)) {
this.logger.error(__("contracts config error: unknown deployment type %s", type));
this.logger.error(__("Accepted types are: %s", BlockchainConnector.ACCEPTED_TYPES.join(', ')));
}
if (type === 'vm') {
const sim = self._getSimulator();
self.provider = sim.provider(self.config.contractsConfig.deployment);
if (coverage) {
// Here we patch the sendAsync method on the provider. The goal behind this is to force pure/constant/view calls to become
// transactions, so that we can pull in execution traces and account for those executions in code coverage.
//
// Instead of a simple call, here's what happens:
//
// 1) A transaction is sent with the same payload, and a pre-defined gas price;
// 2) We wait for the transaction to be mined by asking for the receipt;
// 3) Once we get the receipt back, we dispatch the real call and pass the original callback;
//
// This will still allow tests to get the return value from the call and run contracts unmodified.
self.provider.realSendAsync = self.provider.sendAsync.bind(self.provider);
self.provider.sendAsync = function(payload, cb) {
if(payload.method !== 'eth_call') {
return self.provider.realSendAsync(payload, cb);
}
self.events.request('reporter:toggleGasListener');
let newParams = Object.assign({}, payload.params[0], {gasPrice: '0x77359400'});
let newPayload = {
id: payload.id + 1,
method: 'eth_sendTransaction',
params: [newParams],
jsonrpc: payload.jsonrpc
};
self.provider.realSendAsync(newPayload, (_err, response) => {
let txHash = response.result;
self.web3.eth.getTransactionReceipt(txHash, (_err, _res) => {
self.events.request('reporter:toggleGasListener');
self.provider.realSendAsync(payload, cb);
});
});
};
}
self.web3.setProvider(self.provider);
self._emitWeb3Ready();
2018-10-10 15:34:56 +00:00
return cb();
2018-05-18 21:15:49 +00:00
}
2018-06-15 18:35:50 +00:00
2018-10-09 20:43:38 +00:00
protocol = (type === "rpc") ? protocol : 'ws';
2018-10-09 20:43:38 +00:00
this.web3Endpoint = utils.buildUrl(protocol, host, port);
const providerOptions = {
web3: this.web3,
2018-10-09 20:43:38 +00:00
accountsConfig: accounts,
blockchainConfig: this.config.blockchainConfig,
logger: this.logger,
isDev: this.isDev,
2018-10-09 20:43:38 +00:00
type: type,
web3Endpoint: self.web3Endpoint
};
this.provider = new Provider(providerOptions);
2018-07-20 12:21:23 +00:00
self.events.request("processes:launch", "blockchain", () => {
self.provider.startWeb3Provider(() => {
this.getNetworkId()
2018-07-31 13:59:27 +00:00
.then(id => {
let networkId = self.config.blockchainConfig.networkId;
if (!networkId && constants.blockchain.networkIds[self.config.blockchainConfig.networkType]) {
networkId = constants.blockchain.networkIds[self.config.blockchainConfig.networkType];
2018-07-31 13:59:27 +00:00
}
2018-08-23 14:29:39 +00:00
if (networkId && id.toString() !== networkId.toString()) {
2018-07-31 13:59:27 +00:00
self.logger.warn(__('Connected to a blockchain node on network {{realId}} while your config specifies {{configId}}', {realId: id, configId: networkId}));
self.logger.warn(__('Make sure you started the right blockchain node'));
}
})
.catch(console.error);
2018-07-20 12:21:23 +00:00
self.provider.fundAccounts(() => {
2018-10-09 20:43:38 +00:00
self._emitWeb3Ready();
cb();
});
});
});
}
2018-10-09 20:43:38 +00:00
_emitWeb3Ready() {
this.isWeb3Ready = true;
this.events.emit(WEB3_READY);
this.registerWeb3Object();
this.subscribeToPendingTransactions();
2018-10-09 20:43:38 +00:00
}
_getSimulator() {
try {
return require('ganache-cli');
} catch (e) {
const moreInfo = 'For more information see https://github.com/trufflesuite/ganache-cli';
if (e.code === 'MODULE_NOT_FOUND') {
this.logger.error(__('Simulator not found; Please install it with "%s"', 'npm install ganache-cli --save'));
this.logger.error(moreInfo);
throw e;
}
this.logger.error("==============");
this.logger.error(__("Tried to load Ganache CLI (testrpc), but an error occurred. This is a problem with Ganache CLI"));
this.logger.error(moreInfo);
this.logger.error("==============");
throw e;
}
}
2018-07-20 12:21:23 +00:00
registerEvents() {
2018-07-26 17:11:38 +00:00
const self = this;
self.events.on('check:wentOffline:Ethereum', () => {
2018-09-25 16:41:22 +00:00
self.logger.warn('Ethereum went offline: stopping web3 provider...');
2018-07-26 17:11:38 +00:00
self.provider.stop();
// once the node goes back online, we can restart the provider
self.events.once('check:backOnline:Ethereum', () => {
2018-09-25 16:41:22 +00:00
self.logger.warn('Ethereum back online: starting web3 provider...');
2018-07-26 17:11:38 +00:00
self.provider.startWeb3Provider(() => {
2018-09-25 16:41:22 +00:00
self.logger.warn('web3 provider restarted after ethereum node came back online');
2018-07-26 17:11:38 +00:00
});
});
});
2018-05-18 21:15:49 +00:00
}
onReady(callback) {
if (this.isWeb3Ready) {
return callback();
}
this.events.once(WEB3_READY, () => {
callback();
});
}
2018-05-18 21:15:49 +00:00
registerServiceCheck() {
const self = this;
2018-05-30 15:02:01 +00:00
const NO_NODE = 'noNode';
2018-05-18 21:15:49 +00:00
this.events.request("services:register", 'Ethereum', function (cb) {
async.waterfall([
function checkNodeConnection(next) {
if (!self.provider || !self.provider.connected()) {
2018-07-20 12:21:23 +00:00
return next(NO_NODE, {name: "No Blockchain node found", status: 'off'});
}
next();
},
function checkVersion(next) {
// TODO: web3_clientVersion method is currently not implemented in web3.js 1.0
self.web3._requestManager.send({method: 'web3_clientVersion', params: []}, (err, version) => {
if (err) {
2018-07-20 12:21:23 +00:00
self.isWeb3Ready = false;
return next(null, {name: "Ethereum node not found", status: 'off'});
}
if (version.indexOf("/") < 0) {
2018-07-20 12:21:23 +00:00
self.events.emit(WEB3_READY);
self.isWeb3Ready = true;
return next(null, {name: version, status: 'on'});
}
let nodeName = version.split("/")[0];
let versionNumber = version.split("/")[1].split("-")[0];
let name = nodeName + " " + versionNumber + " (Ethereum)";
2018-07-20 12:21:23 +00:00
self.events.emit(WEB3_READY);
self.isWeb3Ready = true;
return next(null, {name: name, status: 'on'});
});
}
], (err, statusObj) => {
2018-05-30 15:02:01 +00:00
if (err && err !== NO_NODE) {
return cb(err);
2018-05-18 21:15:49 +00:00
}
cb(statusObj);
2018-05-18 21:15:49 +00:00
});
}, 5000, 'off');
2018-05-18 20:51:03 +00:00
}
2018-05-18 21:15:49 +00:00
registerRequests() {
2018-05-18 22:31:47 +00:00
const self = this;
this.events.setCommandHandler("blockchain:reset", function(cb) {
self.isWeb3Ready = false;
self.initWeb3(cb);
});
this.events.setCommandHandler("blockchain:get", function(cb) {
cb(self.web3);
});
2018-05-18 22:31:47 +00:00
this.events.setCommandHandler("blockchain:defaultAccount:get", function(cb) {
2018-07-13 20:41:15 +00:00
cb(self.defaultAccount());
2018-05-18 22:31:47 +00:00
});
this.events.setCommandHandler("blockchain:defaultAccount:set", function(account, cb) {
self.setDefaultAccount(account);
cb();
});
2018-10-10 16:09:56 +00:00
this.events.setCommandHandler("blockchain:getAccounts", function(cb) {
self.getAccounts(cb);
});
this.events.setCommandHandler("blockchain:getBalance", function(address, cb) {
self.getBalance(address, cb);
});
this.events.setCommandHandler("blockchain:block:byNumber", function(blockNumber, cb) {
self.getBlock(blockNumber, cb);
});
this.events.setCommandHandler("blockchain:gasPrice", function(cb) {
self.getGasPrice(cb);
});
this.events.setCommandHandler("blockchain:networkId", function(cb) {
self.getNetworkId().then(cb);
});
2018-07-13 20:41:15 +00:00
this.events.setCommandHandler("blockchain:contract:create", function(params, cb) {
cb(self.ContractObject(params));
});
2018-05-18 22:31:47 +00:00
}
2018-07-31 14:46:19 +00:00
registerAPIRequests() {
const self = this;
let plugin = self.plugins.createPlugin('blockchain', {});
plugin.registerAPICall(
'get',
2018-08-01 09:28:25 +00:00
'/embark-api/blockchain/accounts',
2018-07-31 14:46:19 +00:00
(req, res) => {
let results = [];
self.getAccounts((err, addresses) => {
async.eachOf(addresses, (address, index, eachCb) => {
let result = {address, index};
results.push(result);
async.waterfall([
function(callback) {
self.getTransactionCount(address, (err, count) => {
if (err) {
result.transactionCount = 0;
} else {
result.transactionCount = count;
}
callback();
});
},
function(callback) {
self.getBalance(address, (err, balance) => {
if (err) {
result.balance = 0;
} else {
result.balance = self.web3.utils.fromWei(balance);
}
callback();
});
}
], function () {
eachCb();
});
}, function () {
res.send(results);
});
});
}
);
}
2018-05-18 22:31:47 +00:00
defaultAccount() {
return this.web3.eth.defaultAccount;
}
setDefaultAccount(account) {
this.web3.eth.defaultAccount = account;
}
getAccounts(cb) {
this.web3.eth.getAccounts(cb);
}
2018-07-31 14:46:19 +00:00
getTransactionCount(address, cb) {
this.web3.eth.getTransactionCount(address, cb);
}
2018-10-10 16:09:56 +00:00
getBalance(address, cb) {
this.web3.eth.getBalance(address, cb);
}
2018-05-18 22:41:04 +00:00
getCode(address, cb) {
this.web3.eth.getCode(address, cb);
}
2018-05-18 22:50:20 +00:00
getBlock(blockNumber, cb) {
this.web3.eth.getBlock(blockNumber, cb);
}
getGasPrice(cb) {
2018-07-20 12:21:23 +00:00
const self = this;
this.onReady(() => {
self.web3.eth.getGasPrice(cb);
});
}
getNetworkId() {
return this.web3.eth.net.getId();
}
ContractObject(params) {
2018-07-13 20:41:15 +00:00
return new this.web3.eth.Contract(params.abi, params.address);
}
2018-05-19 00:26:21 +00:00
deployContractObject(contractObject, params) {
return contractObject.deploy({arguments: params.arguments, data: params.data});
}
estimateDeployContractGas(deployObject, cb) {
return deployObject.estimateGas().then((gasValue) => {
cb(null, gasValue);
}).catch(cb);
}
deployContractFromObject(deployContractObject, params, cb) {
const self = this;
let hash;
let calledBacked = false;
function callback(err, receipt) {
if (calledBacked) {
return;
}
if (!err && !receipt.contractAddress) {
return; // Not deployed yet. Need to wait
}
if (interval) {
clearInterval(interval);
}
calledBacked = true;
cb(err, receipt);
}
// This interval is there to compensate for the event that sometimes doesn't get triggered when using WebSocket
// FIXME The issue somehow only happens when the blockchain node is started in the same terminal
const interval = setInterval(() => {
if (!hash) {
return; // Wait until we receive the hash
}
self.web3.eth.getTransactionReceipt(hash, (err, receipt) => {
if (!err && !receipt) {
return; // Transaction is not yet complete
}
callback(err, receipt);
});
}, 500);
deployContractObject.send({
2018-05-19 00:26:21 +00:00
from: params.from, gas: params.gas, gasPrice: params.gasPrice
}, function (err, transactionHash) {
if (err) {
return callback(err);
}
hash = transactionHash;
}).on('receipt', function (receipt) {
if (receipt.contractAddress !== undefined) {
callback(null, receipt);
}
}).then(function (_contract) {
if (!hash) {
return; // Somehow we didn't get the receipt yet... Interval will catch it
}
self.web3.eth.getTransactionReceipt(hash, callback);
}).catch(callback);
}
determineDefaultAccount(cb) {
const self = this;
self.getAccounts(function (err, accounts) {
if (err) {
self.logger.error(err);
return cb(new Error(err));
}
let accountConfig = self.config.blockchainConfig.account;
let selectedAccount = accountConfig && accountConfig.address;
const defaultAccount = selectedAccount || accounts[0];
self.setDefaultAccount(defaultAccount);
cb(null, defaultAccount);
});
}
2018-05-23 15:16:56 +00:00
registerWeb3Object() {
// doesn't feel quite right, should be a cmd or plugin method
// can just be a command without a callback
2018-08-29 10:03:06 +00:00
this.events.emit("runcode:register", "web3", this.web3, false);
2018-05-23 15:16:56 +00:00
}
2018-05-18 20:51:03 +00:00
}
2018-10-09 20:43:38 +00:00
BlockchainConnector.ACCEPTED_TYPES = ['rpc', 'ws', 'vm'];
2018-07-27 19:36:16 +00:00
module.exports = BlockchainConnector;