giveth-bridge
Version:
Mainnet -> sidechain Giveth Bridge.
459 lines (399 loc) • 21.8 kB
JavaScript
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
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 _winston = require('winston');
var _winston2 = _interopRequireDefault(_winston);
var _givethLiquidpledging = require('giveth-liquidpledging');
var _gasPrice = require('./gasPrice');
var _gasPrice2 = _interopRequireDefault(_gasPrice);
var _utils = require('./utils');
var _ForeignGivethBridge = require('./ForeignGivethBridge');
var _ForeignGivethBridge2 = _interopRequireDefault(_ForeignGivethBridge);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return 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 _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var Verifier = function () {
function Verifier(homeWeb3, foreignWeb3, nonceTracker, config, db) {
_classCallCheck(this, Verifier);
this.homeWeb3 = homeWeb3;
this.foreignWeb3 = foreignWeb3;
this.nonceTracker = nonceTracker;
this.db = db;
this.config = config;
this.lp = new _givethLiquidpledging.LiquidPledging(foreignWeb3, config.liquidPledging);
this.foreignBridge = new _ForeignGivethBridge2.default(foreignWeb3, config.foreignBridge);
this.currentHomeBlockNumber = undefined;
this.currentForeignBlockNumber = undefined;
this.account = homeWeb3.eth.accounts.wallet[0];
}
/* istanbul ignore next */
_createClass(Verifier, [{
key: 'start',
value: function start() {
var _this = this;
var intervalId = setInterval(function () {
return _this.verify();
}, this.config.pollTime);
this.verify();
}
}, {
key: 'verify',
value: function verify() {
var _this2 = this;
return Promise.all([this.homeWeb3.eth.getBlockNumber(), this.foreignWeb3.eth.getBlockNumber()]).then(function (_ref) {
var _ref2 = _slicedToArray(_ref, 2),
homeBlockNumber = _ref2[0],
foreignBlockNumber = _ref2[1];
_this2.currentHomeBlockNumber = homeBlockNumber;
_this2.currentForeignBlockNumber = foreignBlockNumber;
return Promise.all([_this2.getFailedSendTxs(), _this2.getPendingTxs()]);
}).then(function (_ref3) {
var _ref4 = _slicedToArray(_ref3, 2),
failedTxs = _ref4[0],
pendingTxs = _ref4[1];
var failedPromises = failedTxs.map(function (tx) {
return _this2.verifyTx(tx);
});
var pendingPromises = pendingTxs.map(function (tx) {
return _this2.verifyTx(tx);
});
if (_this2.config.isTest) {
return Promise.all([].concat(_toConsumableArray(failedPromises), _toConsumableArray(pendingPromises)));
}
}).catch(function (err) {
return console.error('Failed to fetch block number ->', err);
});
}
}, {
key: 'verifyTx',
value: function verifyTx(tx) {
var _this3 = this;
var web3 = tx.toHomeBridge ? this.homeWeb3 : this.foreignWeb3;
var currentBlock = tx.toHomeBridge ? this.currentHomeBlockNumber : this.currentForeignBlockNumber;
var confirmations = tx.toHomeBridge ? this.config.homeConfirmations : this.foreignConfirmations;
// order matters here
var txHash = tx.reSendGiverTxHash || tx.reSendReceiverTxHash || tx.reSendCreateGiverTxHash || tx.txHash;
if (tx.status === 'pending') {
return web3.eth.getTransactionReceipt(txHash).then(function (receipt) {
if (!receipt) return; // not mined
// only update if we have enough confirmations
if (currentBlock - receipt.blockNumber <= confirmations) return;
if (receipt.status === true || receipt.status === '0x01' || receipt.status === '0x1' || receipt.status === 1) {
// this was a createGiver tx, we still need to transfer the funds to the giver
if (txHash === tx.reSendCreateGiverTxHash) {
// GiverAdded event topic
var _receipt$logs$find = receipt.logs.find(function (l) {
return l.topics[0] === '0xad9c62a4382fd0ddbc4a0cf6c2bc7df75b0b8beb786ff59014f39daaea7f232f';
}),
topics = _receipt$logs$find.topics;
tx.giverId = _this3.homeWeb3.utils.hexToNumber(topics[1]); // idGiver is 1st indexed param, thus 2nd topic
// we call handleFailedTx b/c this is still a failed tx. It is just multi-step b/c we needed to create a
// giver.
_winston2.default.debug('successfully created a giver ->', tx.giverId);
return _this3.handleFailedTx(tx);
}
_this3.updateTxData(Object.assign(tx, {
status: 'confirmed'
}));
return;
}
return _this3.handleFailedTx(tx);
}).catch(function (err) {
// ignore unknown tx b/c it is probably too early to check
if (!err.message.includes('unknown transaction')) {
_winston2.default.error('Failed to fetch tx receipt for tx', tx, err);
}
});
} else if (tx.status === 'failed-send') {
return this.handleFailedTx(tx);
} else {
(0, _utils.sendEmail)(this.config, 'Unknown tx status \n\n ' + JSON.stringify(tx, null, 2));
_winston2.default.error('Unknown tx status ->', tx);
}
}
}, {
key: 'handleFailedTx',
value: function handleFailedTx(tx) {
var _this4 = this;
var web3 = tx.toHomeBridge ? this.homeWeb3 : this.foreignWeb3;
var handleFailedReceiver = function handleFailedReceiver() {
return _this4.fetchAdmin(tx.receiverId).then(function (admin) {
_winston2.default.debug('handling failed receiver ->', tx.receiverId, admin, tx);
if (!admin || admin.adminType === '0') {
// giver
return _this4.sendToGiver(tx);
} else if (admin.adminType === '1') {
// delegate
if (tx.reSendCreateGiver && !tx.reSendReceiver) {
// giver failed, so try to send to receiver now
return _this4.sendToReceiver(tx, tx.receiverId);
}
return _this4.sendToGiver(tx);
} else if (admin.adminType === '2') {
// project
// check if there is a parentProject we can send to if project is canceled
return _this4.getParentProjectNotCanceled(tx.receiverId).then(function (projectId) {
if (!projectId || projectId === tx.receiverId && (!tx.reSendCreateGiver || tx.reSendReceiver) || projectId == 0) return _this4.sendToGiver(tx);
return _this4.sendToReceiver(tx, projectId);
});
} else {
// shouldn't get here
(0, _utils.sendEmail)(_this4.config, 'Unknown receiver adminType \n\n ' + JSON.stringify(tx, null, 2));
_winston2.default.error('Unknown receiver adminType ->', tx);
}
});
};
if (tx.toHomeBridge) {
// this shouldn't fail, send email as we need to investigate
(0, _utils.sendEmail)(this.config, 'AuthorizePayment tx failed toHomeBridge \n\n ' + JSON.stringify(tx, null, 2));
_winston2.default.error('AuthorizePayment tx failed toHomeBridge ->', tx);
} else {
// check that the giver is valid
// if we don't have a giverId, we don't need to fetch the admin b/c this was a
// donateAndCreateGiver call and we need to handle the failed receiver
return (tx.giverId ? this.fetchAdmin(tx.giverId) : Promise.resolve(true)).then(function (giverAdmin) {
return giverAdmin ? handleFailedReceiver() : _this4.createGiver(tx);
});
}
}
}, {
key: 'fetchAdmin',
value: function fetchAdmin(id) {
return this.lp.getPledgeAdmin(id).catch(function (e) {
// receiver may not exist, catch error and pass undefined
_winston2.default.debug('Failed to fetch pledgeAdmin for adminId ->', id, e);
});
}
}, {
key: 'sendToGiver',
value: function sendToGiver(tx) {
var _this5 = this;
_winston2.default.debug('send to Giver');
// already attempted to send to giver, notify of failure
if (tx.reSendGiver) {
this.updateTxData(Object.assign(tx, { status: 'failed' }));
(0, _utils.sendEmail)(this.config, 'ForeignBridge sendToGiver Tx failed. NEED TO TAKE ACTION \n\n' + JSON.stringify(tx, null, 2));
_winston2.default.error('ForeignBridge sendToGiver Tx failed. NEED TO TAKE ACTION ->', tx);
return;
}
if (!tx.giver && !tx.giverId) {
(0, _utils.sendEmail)(this.config, 'Tx missing giver and giverId. Can\'t sendToGiver \n\n ' + JSON.stringify(tx, null, 2));
_winston2.default.error('Tx missing giver and giverId. Cant sendToGiver ->', tx);
return;
}
if (tx.giver && !tx.giverId) return this.createGiver(tx);
var data = this.lp.$contract.methods.donate(tx.giverId, tx.giverId, tx.sideToken, tx.amount).encodeABI();
var nonce = void 0;
var txHash = void 0;
return this.nonceTracker.obtainNonce().then(function (n) {
nonce = n;
return (0, _gasPrice2.default)(_this5.config, false);
}).then(function (gasPrice) {
return _this5.foreignBridge.bridge.deposit(tx.sender, tx.mainToken, tx.amount, tx.homeTx, data, {
from: _this5.account.address,
nonce: nonce,
gasPrice: gasPrice,
$extraGas: 100000
}).on('transactionHash', function (transactionHash) {
_this5.nonceTracker.releaseNonce(nonce);
_this5.updateTxData(Object.assign(tx, {
status: 'pending',
reSend: true,
reSendGiver: true,
reSendGiverTxHash: transactionHash
}));
txHash = transactionHash;
}).catch(function (err, receipt) {
_winston2.default.debug('ForeignBridge resend tx error ->', err, receipt, txHash);
// if we have a txHash, then we will pick on the next run
if (!txHash) {
_this5.nonceTracker.releaseNonce(nonce, false, false);
_this5.updateTxData(Object.assign(tx, {
status: 'failed-send',
reSend: true,
reSendGiverTxHash: false,
reSendGiver: true,
reSendGiverError: err
}));
}
});
});
}
}, {
key: 'createGiver',
value: function createGiver(tx) {
var _this6 = this;
if (tx.reSendCreateGiver) {
this.updateTxData(Object.assign(tx, { status: 'failed' }));
(0, _utils.sendEmail)(this.config, 'ForeignBridge createGiver Tx failed. NEED TO TAKE ACTION \n\n' + JSON.stringify(tx, null, 2));
_winston2.default.error('ForeignBridge createGiver Tx failed. NEED TO TAKE ACTION ->', tx);
return;
}
var nonce = void 0;
var txHash = void 0;
return this.nonceTracker.obtainNonce().then(function (n) {
nonce = n;
return (0, _gasPrice2.default)(_this6.config, false);
}).then(function (gasPrice) {
return _this6.lp.addGiver(tx.giver || tx.sender, '', '', 259200, 0, {
from: _this6.account.address,
nonce: nonce,
gasPrice: gasPrice,
$extraGas: 100000
}).on('transactionHash', function (transactionHash) {
_this6.nonceTracker.releaseNonce(nonce);
_this6.updateTxData(Object.assign(tx, {
status: 'pending',
reSend: true,
reSendCreateGiver: true,
reSendCreateGiverTxHash: transactionHash
}));
txHash = transactionHash;
}).catch(function (err, receipt) {
_winston2.default.debug('ForeignBridge resend createGiver tx error ->', err, receipt, txHash);
// if we have a txHash, then we will pick on the next run
if (!txHash) {
_this6.nonceTracker.releaseNonce(nonce, false, false);
_this6.updateTxData(Object.assign(tx, {
status: 'failed-send',
reSend: true,
reSendCreateGiverError: err,
reSendCreateGiverTxHash: false,
reSendCreateGiver: true
}));
}
});
});
}
}, {
key: 'sendToReceiver',
value: function sendToReceiver(tx, newReceiverId) {
var _this7 = this;
if (tx.receiverId !== newReceiverId) {
if (!tx.attemptedReceiverIds) tx.attemptedReceiverIds = [tx.receiverId];
tx.attemptedReceiverIds.push(newReceiverId);
}
tx.receiverId = newReceiverId;
if (!tx.giver && !tx.giverId) {
(0, _utils.sendEmail)(this.config, 'Tx missing giver and giverId. Can\'t sendToParentProject\n\n ' + JSON.stringify(tx, null, 2));
_winston2.default.error('Tx missing giver and giverId. Cant sendToParentProject ->', tx);
return;
}
var data = void 0;
if (tx.giver) {
data = this.lp.$contract.methods.addGiverAndDonate(tx.receiverId, tx.giver, tx.sideToken, tx.amount).encodeABI();
} else {
data = this.lp.$contract.methods.donate(tx.giverId, tx.receiverId, tx.sideToken, tx.amount).encodeABI();
}
var nonce = void 0;
var txHash = void 0;
return this.nonceTracker.obtainNonce().then(function (n) {
nonce = n;
return (0, _gasPrice2.default)(_this7.config);
}).then(function (gasPrice) {
return _this7.foreignBridge.bridge.deposit(tx.sender, tx.mainToken, tx.amount, tx.homeTx, data, {
from: _this7.account.address,
nonce: nonce,
gasPrice: gasPrice,
$extraGas: 100000
}).on('transactionHash', function (transactionHash) {
_this7.nonceTracker.releaseNonce(nonce);
_this7.updateTxData(Object.assign(tx, {
status: 'pending',
reSend: true,
reSendReceiver: true,
reSendReceiverTxHash: transactionHash
}));
txHash = transactionHash;
}).catch(function (err, receipt) {
_winston2.default.debug('ForeignBridge resend tx error ->', err, receipt, txHash);
// if we have a txHash, then we will pick on the next run
if (!txHash) {
_this7.nonceTracker.releaseNonce(nonce, false, false);
_this7.updateTxData(Object.assign(tx, {
status: 'failed-send',
reSend: true,
reSendReceiver: true,
reSendReceiverTxHash: false,
reSendReceiverError: err
}));
}
});
});
}
/**
* if projectId is active, return projectId
* otherwise returns first parentProject that is active
* return undefined if no active project found
*
* @param {*} projectId
* @returns Promise(projectId)
*/
}, {
key: 'getParentProjectNotCanceled',
value: function getParentProjectNotCanceled(projectId) {
var _this8 = this;
return this.lp.isProjectCanceled(projectId).then(function (isCanceled) {
if (!isCanceled) return projectId;
return _this8.lp.getPledgeAdmin(projectId).then(function (admin) {
if (admin.parentProject) return _this8.getParentProjectNotCanceled(admin.parentProject);
return undefined;
});
}).catch(function (e) {
_winston2.default.debug('Failed to getParentProjectNotCanceled =>', projectId);
return undefined;
});
}
}, {
key: 'updateTxData',
value: function updateTxData(data) {
var _id = data._id;
this.db.txs.update({ _id: _id }, data, {}, function (err) {
if (err) {
_winston2.default.error('Error updating bridge-txs.db ->', err, data);
process.exit();
}
});
}
}, {
key: 'getFailedSendTxs',
value: function getFailedSendTxs() {
var _this9 = this;
return new Promise(function (resolve, reject) {
_this9.db.txs.find(_defineProperty({
status: 'failed-send',
$or: [{ reSend: { $exists: false } }, { reSend: false }]
}, '$or', [{ notified: { $exists: false } }, { notified: false }]), function (err, data) {
if (err) {
_winston2.default.error('Error fetching failed-send txs from db ->', err);
resolve([]);
return;
}
resolve(data);
});
});
}
}, {
key: 'getPendingTxs',
value: function getPendingTxs() {
var _this10 = this;
return new Promise(function (resolve, reject) {
// this.db.txs.find({ status: 'pending' }, (err, data) => err ? reject(err) : resolve(Array.isArray(data) ? data : [data]))
_this10.db.txs.find({ status: 'pending' }, function (err, data) {
if (err) {
_winston2.default.error('Error fetching pending txs from db ->', err);
resolve([]);
return;
}
resolve(data);
});
});
}
}]);
return Verifier;
}();
exports.default = Verifier;