diff --git a/bot/github.js b/bot/github.js index 975c1d9..0604f26 100644 --- a/bot/github.js +++ b/bot/github.js @@ -1,52 +1,52 @@ 'use strict' -const https = require('https'); -const config = require('../config'); +const https = require('https') +const config = require('../config') // Returns the url for getting the labels of a request (Github API v3) // req has req.issue.labels_url const getLabelsURL = function (req) { - let url = req.body.issue.labels_url; + let url = req.body.issue.labels_url // Make the URL generic removing the name of the label - return url.replace('{/name}', ''); + return url.replace('{/name}', '') } // Returns the bounty labelNames of the request, only for testing motives const getLabelsMock = function (req) { - return new Promise((resolve, reject) => { resolve(req.body.issue.labels) }); + return new Promise((resolve, reject) => { resolve(req.body.issue.labels) }) } // Returns all the bounty labelNames of a given issue (Github API v3) const getLabels = function (req) { if (config.debug) { - return getLabelsMock(req); + return getLabelsMock(req) } else { - const path = getLabelsURL(req).replace('https://api.github.com', ''); + const path = getLabelsURL(req).replace('https://api.github.com', '') const options = { hostname: 'api.github.com', path: path, headers: { 'User-Agent': config.githubUsername } - }; + } return new Promise((resolve, reject) => { const request = https.get(options, (response) => { // handle http errors if (response.statusCode < 200 || response.statusCode > 299) { - bot.error(response, 'Failed to load page, status code: ' + response.statusCode); - reject(new Error('Failed to load page, status code: ' + response.statusCode)); + bot.error(response, 'Failed to load page, status code: ' + response.statusCode) + reject(new Error('Failed to load page, status code: ' + response.statusCode)) } // temporary data holder - const body = []; + const body = [] // on every content chunk, push it to the data array - response.on('data', (chunk) => body.push(chunk)); + response.on('data', (chunk) => body.push(chunk)) // we are done, resolve promise with those joined chunks response.on('end', () => { - const labels = JSON.parse(body.join('')).map(labelObj => labelObj.name); - resolve(labels); - }); - }); + const labels = JSON.parse(body.join('')).map(labelObj => labelObj.name) + resolve(labels) + }) + }) // handle connection errors of the request request.on('error', (err) => reject(err)) - }); + }) } } diff --git a/bot/index.js b/bot/index.js index 051f500..f482844 100644 --- a/bot/index.js +++ b/bot/index.js @@ -1,16 +1,16 @@ -const winston = require('winston'); +const winston = require('winston') -const ethers = require('ethers'); -const Wallet = ethers.Wallet; -const Contract = ethers.Contract; -const providers = ethers.providers; -const utils = ethers.utils; +const ethers = require('ethers') +const Wallet = ethers.Wallet +const Contract = ethers.Contract +const providers = ethers.providers +const utils = ethers.utils -const prices = require('./prices'); -const config = require('../config'); -const github = require('./github'); +const prices = require('./prices') +const config = require('../config') +const github = require('./github') -const contractAddressString = 'Contract address:'; +const contractAddressString = 'Contract address:' const logger = winston.createLogger({ @@ -21,7 +21,7 @@ const logger = winston.createLogger({ new winston.transports.File({ filename: './log/info.log', level: 'info' }), // new winston.transports.File({ filename: './log/combined.log' }) ] -}); +}) const needsFunding = function (req) { @@ -30,22 +30,22 @@ const needsFunding = function (req) { } else if (req.body.comment.user.login !== config.githubUsername) { return false } else if (!hasAddress(req)) { - return false; + return false } return true } const hasAddress = function (req) { - const commentBody = req.body.comment.body; + const commentBody = req.body.comment.body if (commentBody.search(contractAddressString) === -1) { - return false; + return false } else { - return true; + return true } } const getAddress = function (req) { - const commentBody = req.body.comment.body; + const commentBody = req.body.comment.body const index = commentBody.search(contractAddressString) + 19 return commentBody.substring(index, index + 42) } @@ -54,76 +54,76 @@ const getLabel = function (req) { return new Promise((resolve, reject) => { github.getLabels(req) .then(labels => { - const bountyLabels = labels.filter(label => config.bountyLabels.hasOwnProperty(label.name)); + const bountyLabels = labels.filter(label => config.bountyLabels.hasOwnProperty(label.name)) if (bountyLabels.length === 1) { - resolve(bountyLabels[0]); + resolve(bountyLabels[0]) } else { - reject('Error getting bounty labels'); + reject('Error getting bounty labels') } }).catch(err => { - reject(err); - }); - }); + reject(err) + }) + }) } const getAmountMock = function (req) { return new Promise((resolve, reject) => { - resolve(10); - }); + resolve(10) + }) } const getAmount = function (req) { return new Promise((resolve, reject) => { - const labelPromise = getLabel(req); - const tokenPricePromise = prices.getTokenPrice(config.token); + const labelPromise = getLabel(req) + const tokenPricePromise = prices.getTokenPrice(config.token) Promise.all([labelPromise, tokenPricePromise]) .then(function (values) { - let label = values[0]; - let tokenPrice = values[1]; - let amountToPayDollar = config.priceHour * config.bountyLabels[label.name]; - resolve(amountToPayDollar / tokenPrice); + let label = values[0] + let tokenPrice = values[1] + let amountToPayDollar = config.priceHour * config.bountyLabels[label.name] + resolve(amountToPayDollar / tokenPrice) }) .catch((err) => { - reject(err); - }); - }); + reject(err) + }) + }) } // Logging functions const logTransaction = function (tx) { - logger.info("[OK] Succesfully funded bounty with transaction " + tx.hash); - logger.info(" * From: " + tx.from); - logger.info(" * To: " + tx.to); - logger.info(" * Amount: " + tx.value); - logger.info(" * Gas Price: " + tx.gasPrice); - logger.info("===================================================="); + logger.info("[OK] Succesfully funded bounty with transaction " + tx.hash) + logger.info(" * From: " + tx.from) + logger.info(" * To: " + tx.to) + logger.info(" * Amount: " + tx.value) + logger.info(" * Gas Price: " + tx.gasPrice) + logger.info("====================================================") } const info = function (msg) { - logger.info(msg); + logger.info(msg) } const error = function (errorMessage) { - logger.error("[ERROR] Request processing failed: " + errorMessage); + logger.error("[ERROR] Request processing failed: " + errorMessage) } const sendTransaction = async function (to, amount, gasPrice) { - var chainId = providers.Provider.chainId.ropsten; - var chainName = providers.networks.ropsten; + var chainId = providers.Provider.chainId.ropsten + var chainName = providers.networks.ropsten if (config.realTransaction) { - chainId = providers.Provider.chainId.homestead; - chainName = providers.networks.homestead; + chainId = providers.Provider.chainId.homestead + chainName = providers.networks.homestead } - const wallet = new Wallet(config.privateKey); - const provider = ethers.providers.getDefaultProvider(chainName); + const wallet = new Wallet(config.privateKey) + const provider = ethers.providers.getDefaultProvider(chainName) - wallet.provider = provider; + wallet.provider = provider if (config.token === 'ETH') { const transaction = { gasLimit: config.gasLimit, @@ -131,22 +131,22 @@ const sendTransaction = async function (to, amount, gasPrice) { to: to, value: amount, chainId: chainId - }; + } - return await wallet.sendTransaction(transaction); + return await wallet.sendTransaction(transaction) } else { - let hash = null; + let hash = null - async function getAddress() { return wallet.address; } - async function sign(transaction) { return wallet.sign(transaction); } + async function getAddress() { return wallet.address } + async function sign(transaction) { return wallet.sign(transaction) } - async function resolveName(addressOrName) { return await provider.resolveName(addressOrName); } - async function estimateGas(transaction) { return await provider.estimateGas(transaction); } - async function getGasPrice() { return await provider.getGasPrice(); } - async function getTransactionCount(blockTag) { return await provider.getTransactionCount(blockTag); } + async function resolveName(addressOrName) { return await provider.resolveName(addressOrName) } + async function estimateGas(transaction) { return await provider.estimateGas(transaction) } + async function getGasPrice() { return await provider.getGasPrice() } + async function getTransactionCount(blockTag) { return await provider.getTransactionCount(blockTag) } async function sendTransaction(transaction) { - hash = await provider.sendTransaction(transaction); - return hash; + hash = await provider.sendTransaction(transaction) + return hash } const customSigner = { @@ -161,13 +161,13 @@ const sendTransaction = async function (to, amount, gasPrice) { sign: sign } - const tokenContract = config.tokenContracts[config.token]; - const contractAddress = tokenContract.address; - const contract = new Contract(contractAddress, tokenContract.abi, customSigner); - const bigNumberAmount = ethers.utils.bigNumberify(amount); - await contract.transfer(to, bigNumberAmount); + const tokenContract = config.tokenContracts[config.token] + const contractAddress = tokenContract.address + const contract = new Contract(contractAddress, tokenContract.abi, customSigner) + const bigNumberAmount = ethers.utils.bigNumberify(amount) + await contract.transfer(to, bigNumberAmount) - return hash; + return hash } } diff --git a/bot/prices.js b/bot/prices.js index 7215730..63cc7f6 100644 --- a/bot/prices.js +++ b/bot/prices.js @@ -1,67 +1,67 @@ "use strict" -const https = require("https"); -const config = require("../config"); +const https = require("https") +const config = require("../config") const getGasPrice = function () { - const url = 'https://ethgasstation.info/json/ethgasAPI.json'; + const url = 'https://ethgasstation.info/json/ethgasAPI.json' // return new pending promise return new Promise((resolve, reject) => { // select http or https module, depending on reqested url - const lib = url.startsWith('https') ? require('https') : require('http'); + const lib = url.startsWith('https') ? require('https') : require('http') const request = lib.get(url, (response) => { // handle http errors if (response.statusCode < 200 || response.statusCode > 299) { - reject('Failed to load page, status code: ' + response.statusCode); + reject('Failed to load page, status code: ' + response.statusCode) } // temporary data holder - const body = []; + const body = [] // on every content chunk, push it to the data array - response.on('data', (chunk) => body.push(chunk)); + response.on('data', (chunk) => body.push(chunk)) // we are done, resolve promise with those joined chunks response.on('end', () => { // safeLowWait returns GWei (10^10 Wei). - let jsonBody = JSON.parse(body.join('')); - let gasPriceWei = parseInt(jsonBody['safeLowWait']) * Math.pow(10, 10); - resolve(gasPriceWei); - }); - }); + let jsonBody = JSON.parse(body.join('')) + let gasPriceWei = parseInt(jsonBody['safeLowWait']) * Math.pow(10, 10) + resolve(gasPriceWei) + }) + }) // handle connection errors of the request - request.on('error', (err) => reject(err)); + request.on('error', (err) => reject(err)) }) -}; +} const getTokenPriceMock = function () { - return new Promise((resolve, reject) => resolve(0.35)); + return new Promise((resolve, reject) => resolve(0.35)) } const getTokenPrice = function (token) { if (config.debug) { - return getTokenPriceMock(); + return getTokenPriceMock() } const currency = 'USD' - const url = 'https://min-api.cryptocompare.com/data/price?fsym=' + token + '&tsyms=' + currency; + const url = 'https://min-api.cryptocompare.com/data/price?fsym=' + token + '&tsyms=' + currency // return new pending promise return new Promise((resolve, reject) => { // select http or https module, depending on reqested url - const lib = url.startsWith('https') ? require('https') : require('http'); + const lib = url.startsWith('https') ? require('https') : require('http') const request = lib.get(url, (response) => { // handle http errors if (response.statusCode < 200 || response.statusCode > 299) { - reject('Failed to load page, status code: ' + response.statusCode); + reject('Failed to load page, status code: ' + response.statusCode) } // temporary data holder - const body = []; + const body = [] // on every content chunk, push it to the data array - response.on('data', (chunk) => body.push(chunk)); + response.on('data', (chunk) => body.push(chunk)) // we are done, resolve promise with those joined chunks response.on('end', () => { - let jsonBody = JSON.parse(body.join('')); - let tokenPrice = parseFloat(jsonBody[currency]); - resolve(tokenPrice); - }); - }); + let jsonBody = JSON.parse(body.join('')) + let tokenPrice = parseFloat(jsonBody[currency]) + resolve(tokenPrice) + }) + }) // handle connection errors of the request request.on('error', (err) => reject(err)) }) diff --git a/config/default.js b/config/default.js index 4a49130..22b0cf3 100644 --- a/config/default.js +++ b/config/default.js @@ -30,7 +30,7 @@ const ERC20_ABI = [ "payable": false, "type": "function" } -]; +] const TOKEN_CONTRACTS = { 'STT': { diff --git a/config/index.js b/config/index.js index 1be4521..03ad5f2 100644 --- a/config/index.js +++ b/config/index.js @@ -1,4 +1,4 @@ -var _ = require("lodash"); -var defaults = require("./default.js"); -var config = require("./" + (process.env.NODE_ENV || "default") + ".js"); -module.exports = _.merge({}, defaults, config); +var _ = require("lodash") +var defaults = require("./default.js") +var config = require("./" + (process.env.NODE_ENV || "default") + ".js") +module.exports = _.merge({}, defaults, config) diff --git a/index.js b/index.js index fcdac68..255d4f2 100644 --- a/index.js +++ b/index.js @@ -7,9 +7,9 @@ * Depends on commiteth version as of 2017-06-10. */ -const config = require('./config'); -const bot = require('./bot'); -const crypto = require('crypto'); +const config = require('./config') +const bot = require('./bot') +const crypto = require('crypto') var express = require('express'), @@ -17,104 +17,104 @@ var express = require('express'), helmet = require('helmet'), app = express(), bodyParser = require('body-parser'), - jsonParser = bodyParser.json(); + jsonParser = bodyParser.json() -app.use(cors()); -app.use(helmet()); +app.use(cors()) +app.use(helmet()) // Receive a POST request at the url specified by an env. var. app.post(`${config.urlEndpoint}`, jsonParser, function (req, res, next) { if (!req.body || !req.body.action) { - return res.sendStatus(400); + return res.sendStatus(400) } else if (!bot.needsFunding(req)) { - return res.sendStatus(204); + return res.sendStatus(204) } - validation = validateRequest(req); + validation = validateRequest(req) if (validation.correct) { setTimeout(() => { processRequest(req) .then(() => { - bot.info('issue well funded: ' + req.body.issue.url); + bot.info('issue well funded: ' + req.body.issue.url) }) .catch((err) => { - bot.error('Error processing request: ' + req.body.issue.url); - bot.error('Error: ' + err); - bot.error('Dump: ', req.body); - }); - }, config.delayInMiliSeconds); + bot.error('Error processing request: ' + req.body.issue.url) + bot.error('Error: ' + err) + bot.error('Dump: ', req.body) + }) + }, config.delayInMiliSeconds) } else { - bot.error('Error validating issue: ' + req.body.issue.url); - bot.error('Error: ' + validation.error); + bot.error('Error validating issue: ' + req.body.issue.url) + bot.error('Error: ' + validation.error) } - return res.sendStatus(200); -}); + return res.sendStatus(200) +}) const validateRequest = function (req) { - validation = {correct: false, error: ''}; - webhookSecret = process.env.WEBHOOK_SECRET; + validation = {correct: false, error: ''} + webhookSecret = process.env.WEBHOOK_SECRET if(!webhookSecret) { validation.error = 'Github Webhook Secret key not found. ' + - 'Please set env variable WEBHOOK_SECRET to github\'s webhook secret value'; + 'Please set env variable WEBHOOK_SECRET to github\'s webhook secret value' } else { - const blob = JSON.stringify(req.body); - const hmac = crypto.createHmac('sha1', webhookSecret); - const ourSignature = `sha1=${hmac.update(blob).digest('hex')}`; + const blob = JSON.stringify(req.body) + const hmac = crypto.createHmac('sha1', webhookSecret) + const ourSignature = `sha1=${hmac.update(blob).digest('hex')}` - const theirSignature = req.get('X-Hub-Signature'); + const theirSignature = req.get('X-Hub-Signature') - const bufferA = Buffer.from(ourSignature, 'utf8'); - const bufferB = Buffer.from(theirSignature, 'utf8'); + const bufferA = Buffer.from(ourSignature, 'utf8') + const bufferB = Buffer.from(theirSignature, 'utf8') - const safe = crypto.timingSafeEqual(bufferA, bufferB); + const safe = crypto.timingSafeEqual(bufferA, bufferB) if (safe) { - validation.correct = true; + validation.correct = true } else { validation.error = 'Invalid signature. Check that WEBHOOK_SECRET ' + - 'env variable matches github\'s webhook secret value'; + 'env variable matches github\'s webhook secret value' } } - return validation; + return validation } const processRequest = function (req) { - // const wallet = bot.wallet; + // const wallet = bot.wallet - const from = config.sourceAddress; - const to = bot.getAddress(req); + const from = config.sourceAddress + const to = bot.getAddress(req) // Asynchronous requests for Gas Price and Amount - const amountPromise = bot.getAmount(req); - const gasPricePromise = bot.getGasPrice(); + const amountPromise = bot.getAmount(req) + const gasPricePromise = bot.getGasPrice() return new Promise((resolve, reject) => { Promise.all([amountPromise, gasPricePromise]) .then(function (results) { - const amount = results[0]; - const gasPrice = results[1]; + const amount = results[0] + const gasPrice = results[1] bot.sendTransaction(to, amount, gasPrice) .then(function (hash) { - bot.logTransaction(hash); - resolve(); + bot.logTransaction(hash) + resolve() }) .catch(function (err) { - reject(err); - }); + reject(err) + }) }) .catch(function (err) { - reject(err); - }); - }); + reject(err) + }) + }) } const port = process.env.PORT || 8181 app.listen(port, function () { - bot.info('Autobounty listening on port', port); -}); + bot.info('Autobounty listening on port', port) +}) diff --git a/test/test_bot.js b/test/test_bot.js index af2b57e..a9d0583 100644 --- a/test/test_bot.js +++ b/test/test_bot.js @@ -1,7 +1,7 @@ -const chai = require('chai'); -const expect = require('chai').expect; -const assert = require('chai').assert; -const should = require('chai').should; +const chai = require('chai') +const expect = require('chai').expect +const assert = require('chai').assert +const should = require('chai').should const config = require('../config') const bot = require('../bot') @@ -15,42 +15,42 @@ let requests = [ { body: { action: 'edited', comment: { body: 'Editing my comment', user: { login: 'RandomUser' } } } }, { body: { action: 'edited', comment: { body: sob_comment, user: { login: 'status-open-bounty' } } } }, { body: { action: 'created', issue: { labels: ['bounty', 'bounty-s'] }, comment: { body: sob_comment, user: { login: 'status-open-bounty' } } } } -]; +] describe('Bot behavior', function () { describe('#needsFunding()', function () { it('should return false because the comment is not from status-open-bounty', function () { - assert.isFalse(bot.needsFunding(requests[0])); - }); + assert.isFalse(bot.needsFunding(requests[0])) + }) it('should return false because a user is editing a comment', function () { - assert.isFalse(bot.needsFunding(requests[1])); - }); + assert.isFalse(bot.needsFunding(requests[1])) + }) it('should return false because status-open-bounty edited a comment', function () { - assert.isFalse(bot.needsFunding(requests[2])); - }); + assert.isFalse(bot.needsFunding(requests[2])) + }) it('should return true, it is all right and we should fund', function () { - assert.isTrue(bot.needsFunding(requests[3])); - }); - }); + assert.isTrue(bot.needsFunding(requests[3])) + }) + }) describe('#getAddress', function () { it('should return the address from a status-open-bounty bot comment', function () { - assert.equal(bot.getAddress(requests[3]), '0x3645fe42b1a744ad98cc032c22472388806f86f9'); - }); - }); + assert.equal(bot.getAddress(requests[3]), '0x3645fe42b1a744ad98cc032c22472388806f86f9') + }) + }) describe('#getAmount', function () { it('should return the amount for the issue given the price per hour and the bounty label for this issue', (done) => { bot.getAmount(requests[3]) .then(function (amount) { - let label = 'bounty-s'; - let tokenPrice = 0.35; - let priceInDollars = config.priceHour * config.bountyLabels[label]; - expected_amount = priceInDollars / tokenPrice; - assert.equal(amount, expected_amount); - done(); + let label = 'bounty-s' + let tokenPrice = 0.35 + let priceInDollars = config.priceHour * config.bountyLabels[label] + expected_amount = priceInDollars / tokenPrice + assert.equal(amount, expected_amount) + done() }) - .catch(() => { console.log('error'), done() }); - }); - }); -}); + .catch(() => { console.log('error'), done() }) + }) + }) +})