UNPKG

giveth-bridge

Version:

Mainnet -> sidechain Giveth Bridge.

437 lines (363 loc) 19.8 kB
'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); exports.Tx = undefined; var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); var _web = require('web3'); var _web2 = _interopRequireDefault(_web); var _winston = require('winston'); var _winston2 = _interopRequireDefault(_winston); var _v = require('uuid/v4'); var _v2 = _interopRequireDefault(_v); var _GivethBridge = require('./GivethBridge'); var _GivethBridge2 = _interopRequireDefault(_GivethBridge); var _ForeignGivethBridge = require('./ForeignGivethBridge'); var _ForeignGivethBridge2 = _interopRequireDefault(_ForeignGivethBridge); var _gasPrice = require('./gasPrice'); var _gasPrice2 = _interopRequireDefault(_gasPrice); var _utils = require('./utils'); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } var BridgeData = { homeContractAddress: '', foreignContractAddress: '', homeBlockLastRelayed: 0, foreignBlockLastRelayed: 0 }; var Tx = exports.Tx = function Tx(txHash, toHomeBridge) { var data = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; _classCallCheck(this, Tx); this.txHash = txHash; this.toHomeBridge = toHomeBridge; // to-send - received event, need to submit tx // pending - tx submitted // confirmed - tx confirmend and correct number of blocks have passed for the network (config values) // failed - tx submitted and failed and correct number of blocks have passed for the network (config values) // failed-send - tx failed on send this.status = 'to-send'; Object.assign(this, data); }; var Relayer = function () { function Relayer(homeWeb3, foreignWeb3, nonceTracker, config, db) { _classCallCheck(this, Relayer); this.homeWeb3 = homeWeb3; this.foreignWeb3 = foreignWeb3; this.account = homeWeb3.eth.accounts.wallet[0]; this.nonceTracker = nonceTracker; this.homeBridge = new _GivethBridge2.default(this.homeWeb3, this.foreignWeb3, config.homeBridge, config.foreignBridge); this.foreignBridge = new _ForeignGivethBridge2.default(this.foreignWeb3, config.foreignBridge); this.db = db; this.config = config; this.pollingPromise; this.bridgeData; } /* istanbul ignore next */ _createClass(Relayer, [{ key: 'start', value: function start() { var _this = this; this.loadBridgeData().then(function () { // It is possible to have created txs, but not yet relayed // them, if the server was restarted in the middle of a relay // so do it now _this.relayUnsentTxs(); var intervalId = setInterval(function () { if (_this.pollingPromise) { _winston2.default.debug('Already polling, running after previous round finishes'); _this.pollingPromise.finally(function () { _winston2.default.debug('polling round finished. starting next'); _this.poll(); }); } else { _this.poll(); } }, _this.config.pollTime); _this.poll(); }); } }, { key: 'sendForeignTx', value: function sendForeignTx(tx, gasPrice) { var _this2 = this; var sender = tx.sender, mainToken = tx.mainToken, amount = tx.amount, data = tx.data, homeTx = tx.homeTx; if (!tx.sideToken) { this.updateTxData(Object.assign({}, tx, { status: 'failed-send', error: 'No sideToken for mainToken' })); return Promise.resolve(); } var nonce = void 0; var txHash = void 0; return this.nonceTracker.obtainNonce().then(function (n) { nonce = n; return _this2.foreignBridge.bridge.deposit(sender, mainToken, amount, homeTx, data, { from: _this2.account.address, nonce: nonce, gasPrice: gasPrice, $extraGas: 100000 }).on('transactionHash', function (transactionHash) { txHash = transactionHash; _this2.nonceTracker.releaseNonce(nonce); _this2.updateTxData(Object.assign({}, tx, { txHash: txHash, status: 'pending' })); }); }).catch(function (error, receipt) { _winston2.default.debug('ForeignBridge tx error ->', error, receipt, txHash); // if we have a txHash, then we will pick up the failure in the Verifyer if (!txHash) { _this2.nonceTracker.releaseNonce(nonce, false, false); _this2.updateTxData(Object.assign({}, tx, { error: error, status: 'failed-send' })); } }); } }, { key: 'sendHomeTx', value: function sendHomeTx(tx, gasPrice) { var _this3 = this; var recipient = tx.recipient, token = tx.token, amount = tx.amount, foreignTx = tx.foreignTx; var nonce = void 0; var txHash = void 0; return this.nonceTracker.obtainNonce(true).then(function (n) { nonce = n; return _this3.homeBridge.bridge.authorizePayment('', foreignTx, recipient, token, amount, 0, { from: _this3.account.address, nonce: nonce, gasPrice: gasPrice, $extraGas: 100000 }).on('transactionHash', function (transactionHash) { txHash = transactionHash; _this3.nonceTracker.releaseNonce(nonce, true, true); _this3.updateTxData(Object.assign(tx, { txHash: txHash, status: 'pending' })); }); }).catch(function (error, receipt) { _winston2.default.debug('HomeBridge tx error ->', error, receipt, txHash); // if we have a homeTxHash, then we will pick up the failure in the Verifyer if (!txHash) { _this3.nonceTracker.releaseNonce(nonce, true, false); _this3.updateTxData(Object.assign({}, tx, { status: 'failed-send', error: error })); } }); } }, { key: 'poll', value: function poll() { var _this4 = this; if (!this.bridgeData) return this.loadBridgeData().then(function () { return _this4.poll(); }); var homeFromBlock = void 0; var homeToBlock = void 0; var homeGasPrice = void 0; var foreignFromBlock = void 0; var foreignToBlock = void 0; var foreignGasPrice = void 0; this.pollingPromise = Promise.all([this.homeWeb3.eth.getBlockNumber(), this.foreignWeb3.eth.getBlockNumber(), (0, _gasPrice2.default)(this.config, true), (0, _gasPrice2.default)(this.config, false)]).then(function (_ref) { var _ref2 = _slicedToArray(_ref, 4), homeBlock = _ref2[0], foreignBlock = _ref2[1], homeGP = _ref2[2], foreignGP = _ref2[3]; _winston2.default.debug('Fetched homeBlock:', homeBlock, 'foreignBlock:', foreignBlock); var _bridgeData = _this4.bridgeData, homeBlockLastRelayed = _bridgeData.homeBlockLastRelayed, foreignBlockLastRelayed = _bridgeData.foreignBlockLastRelayed; homeGasPrice = homeGP; foreignGasPrice = foreignGP; homeFromBlock = homeBlockLastRelayed ? homeBlockLastRelayed + 1 : 0; homeToBlock = homeBlock - _this4.config.homeConfirmations; foreignFromBlock = foreignBlockLastRelayed ? foreignBlockLastRelayed + 1 : 0; foreignToBlock = foreignBlock - _this4.config.foreignConfirmations; return Promise.all([_this4.homeBridge.getRelayTransactions(homeFromBlock, homeToBlock), _this4.foreignBridge.getRelayTransactions(foreignFromBlock, foreignToBlock)]).then(function () { var _ref4 = _asyncToGenerator( /*#__PURE__*/regeneratorRuntime.mark(function _callee(_ref3) { var _ref5 = _slicedToArray(_ref3, 2), _ref5$ = _ref5[0], toForeignTxs = _ref5$ === undefined ? [] : _ref5$, _ref5$2 = _ref5[1], toHomeTxs = _ref5$2 === undefined ? [] : _ref5$2; var insertedForeignTxs, foreignPromises, insertedHomeTxs, homePromises; return regeneratorRuntime.wrap(function _callee$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: _context.next = 2; return Promise.all(toForeignTxs.map(function (t) { return _this4.insertTxDataIfNew(t, false); })); case 2: insertedForeignTxs = _context.sent; foreignPromises = insertedForeignTxs.filter(function (tx) { return tx !== undefined; }).map(function (tx) { return _this4.sendForeignTx(tx, foreignGasPrice); }); _context.next = 6; return Promise.all(toHomeTxs.map(function (t) { return _this4.insertTxDataIfNew(t, true); })); case 6: insertedHomeTxs = _context.sent; homePromises = insertedHomeTxs.filter(function (tx) { return tx !== undefined; }).map(function (tx) { return _this4.sendHomeTx(tx, homeGasPrice); }); if (!_this4.config.isTest) { _context.next = 10; break; } return _context.abrupt('return', Promise.all([].concat(_toConsumableArray(foreignPromises), _toConsumableArray(homePromises)))); case 10: case 'end': return _context.stop(); } } }, _callee, _this4); })); return function (_x2) { return _ref4.apply(this, arguments); }; }()).then(function () { _this4.bridgeData.homeBlockLastRelayed = homeToBlock; _this4.bridgeData.foreignBlockLastRelayed = foreignToBlock; _this4.updateBridgeData(_this4.bridgeData); }).catch(function (err) { _winston2.default.error('Error occured ->', err); _this4.bridgeData.homeBlockLastRelayed = homeFromBlock; _this4.bridgeData.foreignBlockLastRelayed = foreignFromBlock; _this4.updateBridgeData(_this4.bridgeData); }); }).catch(function (err) { // catch error fetching block or gasPrice _winston2.default.error('Error occured fetching blockNumbers or gasPrice ->', err); }).finally(function () { return _this4.pollingPromise = undefined; }); return this.pollingPromise; } }, { key: 'loadBridgeData', value: function loadBridgeData() { var _this5 = this; var bridgeData = Object.assign({}, BridgeData); return new Promise(function (resolve, reject) { _this5.db.bridge.findOne({}, function (err, doc) { if (err) { _winston2.default.error('Error loading bridge-config.db'); reject(err); process.exit(); } if (!doc) { doc = { homeContractAddress: _this5.config.homeBridge, foreignContractAddress: _this5.config.foreignBridge, homeBlockLastRelayed: _this5.config.homeBridgeDeployBlock, foreignBlockLastRelayed: _this5.config.foreignBridgeDeployBlock }; _this5.updateBridgeData(doc); } _this5.bridgeData = Object.assign(bridgeData, doc); resolve(_this5.bridgeData); }); }); } }, { key: 'relayUnsentTxs', value: function relayUnsentTxs() { var _this6 = this; return Promise.all([(0, _gasPrice2.default)(this.config, true), (0, _gasPrice2.default)(this.config, false)]).then(function (_ref6) { var _ref7 = _slicedToArray(_ref6, 2), homeGP = _ref7[0], foreignGP = _ref7[1]; return new Promise(function (resolve) { _this6.db.txs.find({ status: 'to-send' }, function (err, docs) { if (err) { _winston2.default.error('Error loading to-send txs'); resolve(); } var promises = docs.map(function (tx) { return tx.toHomeBridge ? _this6.sendHomeTx(tx, homeGP) : _this6.sendForeignTx(tx, foreignGP); }); Promise.all([].concat(_toConsumableArray(promises))).then(function () { return resolve(); }); }); }); }).catch(function (err) { _winston2.default.error('Error sending unsent txs', err); (0, _utils.sendEmail)(_this6.config, 'Error sending unsent txs \n\n' + JSON.stringify(err, null, 2)); }); } /** * Checks that this is a new tx. If new, we persist the tx and return the persisted object * with a generated _id. If this is a duplicate, we will send an error email for further * investigation and return undefined * * @param {*} data * @param {*} toHomeBridge */ }, { key: 'insertTxDataIfNew', value: function insertTxDataIfNew(data, toHomeBridge) { var _this7 = this; var tx = new Tx(undefined, toHomeBridge, data); var query = toHomeBridge ? { foreignTx: tx.foreignTx } : { homeTx: tx.homeTx }; return new Promise(function (resolve, reject) { _this7.db.txs.find(query, function (err, docs) { if (err || docs.length > 0) { (0, _utils.sendEmail)(_this7.config, 'Ignoring duplicate tx. NEED TO INVESTIGATE\n\n ' + JSON.stringify(tx, null, 2) + '\n\n' + JSON.stringify(err, null, 2)); _winston2.default.error('Ignoring duplicate tx ->', err, tx); resolve(); return; } _this7.db.txs.insert(tx, function (err, doc) { if (err) { _winston2.default.error('Error inserting bridge-txs.db ->', err, data); reject(error); } resolve(doc); }); }); }); } }, { key: 'updateTxData', value: function updateTxData(data) { var _id = data._id; if (!_id) throw new Error('Attempting to update txData without an _id'); this.db.txs.update({ _id: _id }, data, {}, function (err) { if (err) { _winston2.default.error('Error updating bridge-txs.db ->', err, data); } }); } }, { key: 'updateBridgeData', value: function updateBridgeData(data) { this.db.bridge.update({ _id: data._id }, data, { upsert: true }, function (err) { if (err) _winston2.default.error('Error updating bridge-config.db ->', err, data); }); } }]); return Relayer; }(); exports.default = Relayer;