/ bot / index.js
index.js
  1  const winston = require('winston')
  2  
  3  const ethers = require('ethers')
  4  const { Wallet, Contract, providers } = ethers
  5  
  6  const config = require('../config')
  7  const prices = require('./prices')
  8  const github = require('./github')
  9  
 10  const winnerPrefix = 'Winner:'
 11  const contractAddressPrefix = 'Contract address: '
 12  const paidPrefix = 'Paid to:'
 13  
 14  const logger = winston.createLogger({
 15    level: 'info',
 16    format: winston.format.json(),
 17    transports: [
 18      new winston.transports.File({ filename: './log/error.log', level: 'error' }),
 19      new winston.transports.File({ filename: './log/info.log', level: 'info' }),
 20      new winston.transports.Console({
 21        format: winston.format.simple(),
 22        level: 'debug',
 23        colorize: true,
 24        stderrLevels: ['error', 'debug', 'info'],
 25        silent: process.env.NODE_ENV === 'production'
 26      })
 27    ]
 28  })
 29  
 30  function needsFunding (req) {
 31    if (req.headers['x-github-event'] !== 'issue_comment') {
 32      return false
 33    }
 34    if (req.body.action !== 'edited' || !req.body.hasOwnProperty('comment')) {
 35      return false
 36    } else if (req.body.comment.user.login !== config.githubUsername) {
 37      return false
 38    } else if (!hasAddress(req)) {
 39      return false
 40    } else if (isFunded(req)) {
 41      return false
 42    } else if (hasWinner(req)) {
 43      return false
 44    } else if (isPaid(req)) {
 45      return false
 46    }
 47    return true
 48  }
 49  
 50  function isFunded (req) {
 51    const prefix = `Tokens: ${config.token}: `
 52    const index = req.body.comment.body.search(prefix)
 53    if (index === -1) {
 54      return false
 55    }
 56    const value = Number.parseFloat(req.body.comment.body.substring(index + prefix.length))
 57    return value > 0
 58  }
 59  
 60  function isPaid (req) {
 61    return req.body.comment.body.search(paidPrefix) !== -1
 62  }
 63  
 64  function hasWinner (req) {
 65    return req.body.comment.body.search(winnerPrefix) !== -1
 66  }
 67  function hasAddress (req) {
 68    return req.body.comment.body.search(contractAddressPrefix) !== -1
 69  }
 70  
 71  function getAddress (req) {
 72    const commentBody = req.body.comment.body
 73    const index = commentBody.search(contractAddressPrefix)
 74    if (index === -1) {
 75      return undefined
 76    }
 77    const addressIndex = index + contractAddressPrefix.length + 1
 78    console.log('address: ', commentBody.substring(addressIndex, addressIndex + 42))
 79    return commentBody.substring(addressIndex, addressIndex + 42)
 80  }
 81  
 82  async function getLabel (req) {
 83    const labelNames = await github.getLabels(req)
 84    const upperCaseLabelNames = labelNames.map(l => l.toUpperCase())
 85    const bountyLabels = Object.keys(config.bountyLabels).filter(bountyLabel => upperCaseLabelNames.find(l => l === bountyLabel.toUpperCase()))
 86    if (bountyLabels.length === 1) {
 87      return bountyLabels[0]
 88    }
 89  
 90    throw new Error(`Error getting bounty labels: ${JSON.stringify(labelNames)}`)
 91  }
 92  
 93  async function getAmount (req) {
 94    const labelName = await getLabel(req)
 95    const tokenPrice = await prices.getTokenPrice(config.token)
 96  
 97    const bountyLabelHours = config.bountyLabels[labelName]
 98    if (!bountyLabelHours) {
 99      throw new Error(`Label '${labelName}' not found in config`)
100    }
101    const amountToPayDollar = config.priceHour * bountyLabelHours
102    return (amountToPayDollar / tokenPrice)
103  }
104  
105  // Logging functions
106  
107  function logTransaction (tx) {
108    info(`[OK] Succesfully funded bounty with transaction ${tx.hash}`)
109    info(` * From: ${tx.from}`)
110    info(` * To: ${tx.to}`)
111    info(` * Amount: ${tx.value}`)
112    info(` * Gas Price: ${tx.gasPrice}`)
113    info(`====================================================`)
114  }
115  
116  function info (msg) {
117    logger.info(msg)
118  }
119  
120  function error (errorMessage) {
121    logger.error(`Request processing failed: ${errorMessage}`)
122  }
123  
124  async function sendTransaction (to, amount, gasPrice) {
125    if (isNaN(amount)) {
126      throw Error('Invalid amount')
127    }
128    if (!config.privateKey.startsWith('0x')) {
129      throw Error('Private key should start with 0x')
130    }
131  
132    let transaction = null
133    let hash = null
134  
135    const network = providers.Provider.getNetwork(config.realTransaction ? 'homestead' : 'ropsten')
136    const wallet = new Wallet(config.privateKey)
137    wallet.provider = ethers.providers.getDefaultProvider(network)
138  
139    async function customSendTransaction (tx) {
140      hash = await wallet.provider.sendTransaction(tx)
141      return hash
142    }
143    async function customSignTransaction (tx) {
144      transaction = tx
145      return wallet.sign(tx)
146    }
147  
148    if (config.token === 'ETH') {
149      const transaction = {
150        gasLimit: config.gasLimit,
151        gasPrice: gasPrice,
152        to: to,
153        value: amount,
154        chainId: network.chainId
155      }
156  
157      await wallet.sendTransaction(transaction)
158    } else {
159      const customSigner = getCustomSigner(wallet, customSignTransaction, customSendTransaction)
160      const tokenContract = config.tokenContracts[config.token]
161      const contractAddress = tokenContract.address
162      const contract = new Contract(contractAddress, tokenContract.abi, customSigner)
163      const bigNumberAmount = ethers.utils.parseUnits(amount.toString(), 'ether')
164  
165      await contract.transfer(to, bigNumberAmount)
166  
167      transaction.hash = hash
168      transaction.from = wallet.address
169      transaction.value = bigNumberAmount
170    }
171  
172    return transaction
173  }
174  
175  function getCustomSigner (wallet, signTransaction, sendTransaction) {
176    const provider = wallet.provider
177  
178    async function getAddress () { return wallet.address }
179  
180    async function resolveName (addressOrName) { return provider.resolveName(addressOrName) }
181    async function estimateGas (transaction) { return provider.estimateGas(transaction) }
182    async function getGasPrice () { return provider.getGasPrice() }
183    async function getTransactionCount (blockTag) { return provider.getTransactionCount(blockTag) }
184  
185    const customSigner = {
186      getAddress: getAddress,
187      provider: {
188        resolveName: resolveName,
189        estimateGas: estimateGas,
190        getGasPrice: getGasPrice,
191        getTransactionCount: getTransactionCount,
192        sendTransaction: sendTransaction
193      },
194      sign: signTransaction
195    }
196  
197    return customSigner
198  }
199  
200  module.exports = {
201    needsFunding: needsFunding,
202    getAddress: getAddress,
203    getAmount: getAmount,
204    getGasPrice: prices.getGasPrice,
205    getTokenPrice: prices.getTokenPrice,
206    sendTransaction: sendTransaction,
207    info: info,
208    logTransaction: logTransaction,
209    error: error
210  }