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 }