giveth-bridge
Version:
Mainnet -> sidechain Giveth Bridge.
437 lines (363 loc) • 19.8 kB
JavaScript
;
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;