Merge pull request #2 from kafkasl/development

Autobounty v1.0 - not working, but Pedro and I have the fixes, to be merged shortly.
This commit is contained in:
Andy Tudhope 2018-03-19 19:39:28 +02:00 committed by GitHub
commit 93c5a1005d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 638 additions and 96 deletions

5
.gitignore vendored
View File

@ -1,2 +1,7 @@
node_modules
npm-debug.log
.ssh
*.log
package-lock.json

View File

@ -3,6 +3,8 @@ FROM node:7-onbuild
ENV PORT 8080
EXPOSE 8080
ENV NAME autobounty
ENV STANDARD_BOUNTY 0.001
ENV WEBHOOK_SECRET test
# Set this variable to the name of your production config file (without the extension)
ENV NODE_ENV development
# Set this variable to the value of the secret field of the Github webhook
ENV WEBHOOK_SECRET ''

55
bot/github.js Normal file
View File

@ -0,0 +1,55 @@
'use strict'
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;
// Make the URL generic removing the name of the label
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) });
}
// Returns all the bounty labelNames of a given issue (Github API v3)
const getLabels = function (req) {
if (config.debug) {
return getLabelsMock(req);
} else {
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));
}
// temporary data holder
const body = [];
// on every content chunk, push it to the data array
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);
});
});
// handle connection errors of the request
request.on('error', (err) => reject(err))
});
}
}
module.exports = {
getLabels: getLabels
}

178
bot/index.js Normal file
View File

@ -0,0 +1,178 @@
const winston = require('winston');
const ethers = require('ethers');
const Wallet = ethers.Wallet;
const providers = ethers.providers;
const utils = ethers.utils;
const prices = require('./prices');
const config = require('../config');
const github = require('./github');
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [
new winston.transports.File({ filename: './log/error.log', level: 'error' }),
new winston.transports.File({ filename: './log/info.log', level: 'info' }),
// new winston.transports.File({ filename: './log/combined.log' })
]
});
const needsFunding = function (req) {
if (req.body.action !== 'edited' || !req.body.hasOwnProperty('comment')) {
return false
} else if (req.body.comment.user.login !== config.githubUsername) {
return false
} else if (!hasAddress(req)) {
return false;
}
return true
}
const hasAddress = function (req) {
const commentBody = req.body.comment.body;
if (commentBody.search('Contract address:') === -1) {
return false;
} else {
return true;
}
}
const getAddress = function (req) {
const commentBody = req.body.comment.body;
return commentBody.substring(commentBody.search("Contract address:") + 19, commentBody.search("Contract address:") + 61)
}
const getLabelMock = function (req) {
return new Promise((resolve, reject) => {
github.getLabels(req)
.then(labels => {
const bountyLabels = labels.filter(name => config.bountyLabels.hasOwnProperty(name));
if (bountyLabels.length === 1) {
resolve(bountyLabels[0]);
} else {
reject('Error getting bounty labels: ' + bountyLabels);
}
})
.catch((err) => {
reject(err)
});
});
}
const getLabel = function (req) {
if (config.debug) {
return getLabelMock(req);
}
return new Promise((resolve, reject) => {
github.getLabels(req)
.then(labels => {
const bountyLabels = labels.filter(name => config.bountyLabels.hasOwnProperty(name));
if (bountyLabels.length === 1) {
resolve(bountyLabels[0]);
} else {
reject('Error getting bounty labels');
}
}).catch(err => {
reject(err);
});
});
}
const getAmountMock = function (req) {
return new Promise((resolve, reject) => {
resolve(10);
});
}
const getAmount = function (req) {
return new Promise((resolve, reject) => {
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];
resolve(amountToPayDollar / tokenPrice);
})
.catch((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("====================================================");
}
const info = function (msg) {
logger.info(msg);
}
const error = function (errorMessage) {
logger.error("[ERROR] Request processing failed: " + errorMessage);
}
const sendTransaction = function (to, amount, gasPrice) {
var chainId = providers.Provider.chainId.ropsten;
var chainName = providers.networks.ropsten;
if (config.realTransaction) {
chainId = providers.Provider.chainId.homestead;
chainName = providers.networks.homestead;
}
const wallet = new Wallet(config.privateKey);
const provider = ethers.providers.getDefaultProvider(chainName);
wallet.provider = provider;
var transaction = {
gasLimit: config.gasLimit,
gasPrice: gasPrice,
to: to,
value: amount,
chainId: chainId
};
return new Promise((resolve, reject) => {
wallet.sendTransaction(transaction)
.then(function(hash) {
logTransaction(hash);
resolve(hash);
}).catch(function(err) {
reject(err);
});
});
}
module.exports = {
needsFunding: needsFunding,
getAddress: getAddress,
getAmount: getAmount,
getGasPrice: prices.getGasPrice,
sendTransaction: sendTransaction,
info: info,
logTransaction: logTransaction,
error: error
}

73
bot/prices.js Normal file
View File

@ -0,0 +1,73 @@
"use strict"
const https = require("https");
const config = require("../config");
const getGasPrice = function () {
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 request = lib.get(url, (response) => {
// handle http errors
if (response.statusCode < 200 || response.statusCode > 299) {
reject('Failed to load page, status code: ' + response.statusCode);
}
// temporary data holder
const body = [];
// on every content chunk, push it to the data array
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);
});
});
// handle connection errors of the request
request.on('error', (err) => reject(err));
})
};
const getTokenPriceMock = function () {
return new Promise((resolve, reject) => resolve(0.35));
}
const getTokenPrice = function (token) {
if (config.debug) {
return getTokenPriceMock();
}
const currency = 'USD'
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 request = lib.get(url, (response) => {
// handle http errors
if (response.statusCode < 200 || response.statusCode > 299) {
reject('Failed to load page, status code: ' + response.statusCode);
}
// temporary data holder
const body = [];
// on every content chunk, push it to the data array
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);
});
});
// handle connection errors of the request
request.on('error', (err) => reject(err))
})
}
module.exports = {
getGasPrice: getGasPrice,
getTokenPrice: getTokenPrice
}

46
config/default.js Normal file
View File

@ -0,0 +1,46 @@
// Work hours per label
const BOUNTY_LABELS = {
'bounty-xs': 1,
'bounty-s': 10,
'bounty-m': 100,
'bounty-l': 1000,
'bounty-xl': 10000
}
module.exports = {
// Debug mode for testing the bot
debug: true,
// URL where the bot is listening (e.g. '/funding')
urlEndpoint: '/',
// URL for the signer
signerPath: 'https://ropsten.infura.io',
// Address with the funding for the bounties
sourceAddress: '0x26a4D114B98C4b0B0118426F10fCc1112AA2864d',
// Private key for ether.js wallet
privateKey: '',
// Token of the currency for fetching real time prices (e.g. 'SNT')
token: 'SNT',
// Limit for the gas used in a transaction (e.g. 92000)
gasLimit: 92000,
// Price per hour you will pay in dolars (e.g. 35)
priceHour: 1,
// Delay before funding a bounty (e.g. 3600000)
delayInMiliSeconds: 10000,
// Bounty Labels for the issues and the correspondent hours (e.g. {'bounty-xs': 3})
bountyLabels: BOUNTY_LABELS,
// username for the bot which has to comment for starting the process (e.g. status-bounty-)
githubUsername: 'status-open-bounty',
// Activate real transactions
realTransaction: false
}

4
config/index.js Normal file
View File

@ -0,0 +1,4 @@
var _ = require("lodash");
var defaults = require("./default.js");
var config = require("./" + (process.env.NODE_ENV || "default") + ".js");
module.exports = _.merge({}, defaults, config);

174
index.js
View File

@ -1,90 +1,120 @@
/*
* Bot that receives a POST request (from a GitHub issue comment webhook)
* and in case it's a comment that has "@autobounty <decimal> <currency>"
* awards that bounty to the address posted earlier in the thread (by the
* commiteth bot).
* TODO tests
* REVIEW parsing, non-persisting storage of addresses, hardcoded string length.
* Depends on commiteth version as of 2017-06-10.
*/
* Bot that receives a POST request (from a GitHub issue comment webhook)
* and in case it's a comment that has "@autobounty <decimal> <currency>"
* awards that bounty to the address posted earlier in the thread (by the
* commiteth bot).
* REVIEW parsing, non-persisting storage of addresses, hardcoded string length.
* Depends on commiteth version as of 2017-06-10.
*/
const SignerProvider = require('ethjs-provider-signer');
const sign = require('ethjs-signer').sign;
const Eth = require('ethjs-query');
const config = require('./config');
const bot = require('./bot');
const crypto = require('crypto');
const address = process.env.ADDRESS;
const name = process.env.NAME;
const webhook_secret = process.env.WEBHOOK_SECRET;
const provider = new SignerProvider(process.env.NODE, {
signTransaction: (rawTx, cb) => cb(null, sign(rawTx, process.env.KEY)),
accounts: (cb) => cb(null, [address]),
});
const eth = new Eth(provider);
var express = require('express'),
cors = require('cors'),
helmet = require('helmet'),
app = express(),
bodyParser = require('body-parser'),
jsonParser = bodyParser.json();
app.use(cors());
app.use(helmet());
// Store issue ids and their bounty addresses
var issueData = {};
// Receive a POST request at the address specified by an env. var.
app.post(`/comment/${webhook_secret}`, jsonParser, function(req, res, next){
if (!req.body)
return res.sendStatus(400);
var commentBody = req.body.comment.body;
var issueId = req.body.issue.id;
var namePosition = commentBody.search("@" + name);
// Store toAddress from commiteth
if (namePosition == -1 ) {
if (req.body.comment.user.login == 'commiteth') { // TODO no existence check
issueData[issueId] = {"toAddress": commentBody.substring(commentBody.search("Contract address:") + 18, commentBody.search("Contract address:") + 60)}
console.log(issueData);
return res.status(204);
// 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);
} else if (!bot.needsFunding(req)) {
return res.sendStatus(204);
}
}
else {
var postNameWords = commentBody.substring(namePosition + 1 + name.length + 1).trim().split(' ');
var amount = 0;
if (postNameWords.length > 0) {
if(postNameWords[0] == "standard") {
amount = process.env.STANDARD_BOUNTY;
}
else {
amount = parseFloat(postNameWords[0]);
}
}
console.log("Trying to give " + amount + " ETH to " + issueData[issueId].toAddress + " for issue " + issueId);
issueData[issueId].amount = amount;
validation = validateRequest(req);
// Conduct the transaction
eth.getTransactionCount(address, (err, nonce) => {
eth.sendTransaction({
from: address, // Specified in webhook, secret
to: issueData[issueId].toAddress, // Address from earlier in the thread
gas: 100000,
value: issueData[issueId].amount,
nonce,
}, (err, txID) => {
if (err) {
console.log('Request failed', err)
return res.status(500).json(err)
}
else {
console.log('Successful request:', txID)
res.json({ txID })
}
});
});
}
if (validation.correct) {
setTimeout(() => {
processRequest(req)
.then(() => {
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);
} else {
bot.error('Error validating issue: ' + req.body.issue.url);
bot.error('Error: ' + validation.error);
}
return res.sendStatus(200);
});
const validateRequest = function (req) {
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';
} else {
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 bufferA = Buffer.from(ourSignature, 'utf8');
const bufferB = Buffer.from(theirSignature, 'utf8');
const safe = crypto.timingSafeEqual(bufferA, bufferB);
if (safe) {
validation.correct = true;
} else {
validation.error = 'Invalid signature. Check that WEBHOOK_SECRET ' +
'env variable matches github\'s webhook secret value';
}
}
return validation;
}
const processRequest = function (req) {
// const wallet = bot.wallet;
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();
return new Promise((resolve, reject) => {
Promise.all([amountPromise, gasPricePromise])
.then(function (results) {
let amount = results[0];
let gasPrice = results[1];
bot.sendTransaction(to, amount, gasPrice)
.then(function () {
resolve();
})
.catch(function (err) {
reject(err);
});
})
.catch(function (err) {
reject(err);
});
});
}
const port = process.env.PORT || 8181
app.listen(port, function(){
console.log('Autobounty listening on port', port);
app.listen(port, function () {
bot.info('Autobounty listening on port', port);
});

View File

@ -5,17 +5,24 @@
"main": "index.js",
"scripts": {
"start": "node index.js",
"test": "echo \"Error: no test specified\" && exit 1"
"test": "./node_modules/mocha/bin/mocha"
},
"author": "",
"license": "ISC",
"dependencies": {
"body-parser": "^1.17.2",
"chai": "^4.1.2",
"cors": "^2.8.1",
"eslint": "^4.15.0",
"ethers": "^2.2.6",
"ethjs-provider-signer": "^0.1.4",
"ethjs-query": "^0.2.4",
"ethjs-signer": "^0.1.1",
"express": "^4.15.2",
"web3": "^0.18.2"
"helmet": "^3.9.0",
"lodash": "^4.17.4",
"mocha": "^5.0.0",
"web3": "^0.18.2",
"winston": "^3.0.0-rc1"
}
}

119
readme.md
View File

@ -1,19 +1,100 @@
# Status OpenBounty Autobounty <img align="right" src="https://github.com/status-im/autobounty/blob/master/status.png" height="80px" />
### Massive Thanks to the Amazing Aragon Team for starting this project! Original repo **[here](https://github.com/aragon/autobounty)**
#### Status Autobounty bot for OpenBounty
A Github bot that will automatically fund issues that are labelled with **[bounty](https://github.com/status-im/status-react/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+label%3Abountyy)**.
These bounties will use [Status OpenBounty](https://openbounty.status.im/) to incentivize community members to participate and contribute to the development of Open Source Software projects like Status, Riot and Aragon.
Open source is awesome, but it is also hard work that needs to be rewarded to ensure top quality work. It's also important that everyone in the world gets a fair chance to do it.
#### 🦋 We at Status, Aragon and Riot are using [OpenBounty](https://openbounty.status.im/) to reward open source contributions outside our Core teams.
All issues tagged with **[bounty](https://github.com/status-im/status-react/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+label%3Abounty)** are eligible for a bounty on a succesfully merged Pull Request that solves the issue. Currently bounties have to be funded one after the other and manually by a real human being. This bot's purpose in life is therefore to create and automate the process of funding issues so that contributors can be rewarded accordingly.
#### The process
- An **[issue](https://github.com/status-im/status-react/issues)** is created at the repo
- Issue is labeled with **[bounty](https://github.com/status-im/status-react/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+label%3Abounty)**
- [Status OpenBounty](https://openbounty.status.im/) bot adds a bounty to the issue and it is displayed in the issue's comments
- This autobounty bot automatically funds that issue with a set amount of Ether based on another label decribing the size of the bounty based roughly on how many hours the team feels it will take to complete * the rate per hour they are willing to pay. When a successful Pull Request is merged, the contributor is paid that amount for their work autonomously, transparently and programmatically by the smart contract itself - no middle men involved at all.
# Status OpenBounty Autobounty <img align="right" src="https://github.com/status-im/autobounty/blob/master/status.png" height="80px" />
### Massive Thanks to the Amazing Aragon Team for starting this project! Original repo **[here](https://github.com/aragon/autobounty)**
#### Status Autobounty bot for OpenBounty
A Github bot that will automatically fund issues that are labelled with **[bounty](https://github.com/status-im/status-react/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+label%3Abountyy)**.
These bounties will use [Status OpenBounty](https://openbounty.status.im/) to incentivize community members to participate and contribute to the development of Open Source Software projects like Status, Riot and Aragon.
Open source is awesome, but it is also hard work that needs to be rewarded to ensure top quality work. It's also important that everyone in the world gets a fair chance to do it.
#### 🦋 We at Status, Aragon and Riot are using [OpenBounty](https://openbounty.status.im/) to reward open source contributions outside our Core teams.
All issues tagged with **[bounty](https://github.com/status-im/status-react/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+label%3Abounty)** are eligible for a bounty on a succesfully merged Pull Request that solves the issue. Currently bounties have to be funded one after the other and manually by a real human being. This bot's purpose in life is therefore to create and automate the process of funding issues so that contributors can be rewarded accordingly.
#### The process
1. An **[issue](https://github.com/status-im/status-react/issues)** is created at the repo
2. Issue is labeled with **[bounty](https://github.com/status-im/status-react/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+label%3Abounty)** and **bounty-{xs,s,m,l,xl}**
3. [Status OpenBounty](https://openbounty.status.im/) bot adds a bounty to the issue and it is displayed in the issue's comments
4. Webhook is triggered for issue comments.
5. Autobounty checks if the request needs funding (by default: if the user status-open-bounty posts a new comment). If it needs funding proceed to 6 otherwise do nothing.
6. Bot waits for X minutes (configurable parameter) to allow label corrections before proceeding.
7. Address to fund is obtained from status-open-bounty comment.
8. Amount to fund is computed as the hours of work corresponding to the given label times the configured price per hour divided by the token price obtained from etherscan.io (amount_of_work * price_per_hour / token_price).
9. The gas price for the transaction is retrieved from ethgasstation and the maximum gas used is a config param.
10. With all the information the bot funds the bounty from config param *sourceAddress*.
#### Configuration
##### Bot config
Autobounty is build using docker. Before building the image, you need to set up a configuration as follows:
The [config]() folder contains the files for configuring the bot. The description for the variables can be found in *default.js*. Create a production config file (e.g. *production.js*) uing the {default,development}.js as template to override the default ones.
**Remeber** to set the environment variable *NODE_ENV* in the dockerfile (e.g. `ENV NODE_ENV production`) and *WEBHOOK_SECRET* to the value specified in the secret field during the webhook creation (e.g. for random creation *ruby -rsecurerandom -e 'puts SecureRandom.hex(20)'*).
)
```javascript
// Debug mode for testing the bot
debug: true,
// URL where the bot is listening (e.g. '/funding')
urlEndpoint: '',
// URL for the signer (e.g. 'https://ropsten.infura.io')
signerPath: '',
// Address with the funding for the bounties
sourceAddress: '',
// Token of the currency for fetching real time prices (e.g. 'SNT')
token: '',
// Limit for the gas used in a transaction (e.g. 92000)
gasLimit: 0,
// Price per hour you will pay in dolars (e.g. 35)
priceHour: 0,
// Delay before funding a bounty (e.g. 3600000)
delayInMiliSeconds: 0,
// Bounty Labels for the issues and the correspondent houres (e.g. {'bounty-xs': 3})
bountyLabels: {},
// username for the bot which has to comment for starting the process (e.g. status-bounty-)
githubUsername: '',
// Activate real transactions
realTransaction: false
```
##### Github Webhook
Create a github webhook with the following information:
* Payload URL: IP_HOST/URL_ENDPOINT
* Content Type: application/json
* Secret: the value you set for environment variable WEBHOOK_SECRET.
* Configure the webhook to be triggered by comments in issues selecting the Issue Comment box in 'Let me select individual events'
Where *IP_HOST* is the ip of the machine running the docker image and *URL_ENDPOINT* is the configuration variable with the same name in your custom config file.
#### Build
To build and run the docker image issue the following commands:
```bash
docker build -t autobounty .
docker run -p 8080:8080 autobounty
```
#### Important Notes
* Bot always **aborts on error** and logs the cause of the error in the folder ./log. The aborted transaction must then be manually funded.
* **Only one token** can be specified on the configuration file variable *token*.
* Autobounty bot assumes that the status-open-bounty will only post a single message. In case two messages are posted the issue would be **funded two times**.
The ongoing requests are not recorded in any persistent data storage. If the machine crashes during a request processing the request will be lost.

5
restart.sh Executable file
View File

@ -0,0 +1,5 @@
CONTAINER_ID="$(docker ps -lq)"
LOG_PATH=$(pwd)/log
docker stop ${CONTAINER_ID}
docker build -t autobounty .
docker run -d -v ${LOG_PATH}:/usr/src/app/log -p 8080:8080 autobounty &

56
test/test_bot.js Normal file
View File

@ -0,0 +1,56 @@
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')
// status-open-bounty comment from https://github.com/status-im/autobounty/issues/1
let sob_comment = 'Current balance: 0.000000 ETH\nTokens: SNT: 2500.00 ANT: 25.00\nContract address: 0x3645fe42b1a744ad98cc032c22472388806f86f9\nNetwork: Mainnet\n To claim this bounty sign up at https://openbounty.status.im and make sure to update your Ethereum address in My Payment Details so that the bounty is correctly allocated.\nTo fund it, send ETH or ERC20/ERC223 tokens to the contract address.'
// Fake requests
let requests = [
{ body: { action: 'created', comment: { body: 'Creating my first comment', user: { login: 'randomUser' } } } },
{ 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]));
});
it('should return false because a user is editing a comment', function () {
assert.isFalse(bot.needsFunding(requests[1]));
});
it('should return false because status-open-bounty edited a comment', function () {
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]));
});
});
describe('#getAddress', function () {
it('should return the address from a status-open-bounty bot comment', function () {
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();
})
.catch(() => { console.log('error'), done() });
});
});
});