miner.js
1 const async = require('async'); 2 const NetcatClient = require('netcat/client'); 3 4 //Constants 5 const minerStart = 'miner_start'; 6 const minerStop = 'miner_stop'; 7 const getHashRate = 'miner_getHashrate'; 8 const getCoinbase = 'eth_coinbase'; 9 const getBalance = 'eth_getBalance'; 10 const newBlockFilter = 'eth_newBlockFilter'; 11 const pendingBlockFilter = 'eth_newPendingTransactionFilter'; 12 const getChanges = 'eth_getFilterChanges'; 13 const getBlockCount = 'eth_getBlockTransactionCountByNumber'; 14 15 class GethMiner { 16 constructor(options) { 17 const self = this; 18 // TODO: Find a way to load mining config from YML. 19 // In the meantime, just set an empty config object 20 this.config = {}; 21 this.datadir = options.datadir; 22 self.interval = null; 23 self.callback = null; 24 self.started = null; 25 26 self.commandQueue = async.queue((task, callback) => { 27 self.callback = callback; 28 self.client.send(JSON.stringify({"jsonrpc": "2.0", "method": task.method, "params": task.params || [], "id": 1})); 29 }, 1); 30 31 const defaults = { 32 interval_ms: 15000, 33 initial_ether: 15000000000000000000, 34 mine_pending_txns: true, 35 mine_periodically: false, 36 mine_normally: false, 37 threads: 1 38 }; 39 40 for (let key in defaults) { 41 if (this.config[key] === undefined) { 42 this.config[key] = defaults[key]; 43 } 44 } 45 46 const isWin = process.platform === "win32"; 47 48 let ipcPath; 49 if (isWin) { 50 ipcPath = '\\\\.\\pipe\\geth.ipc'; 51 } else { 52 ipcPath = this.datadir + '/geth.ipc'; 53 } 54 55 this.client = new NetcatClient(); 56 this.client.unixSocket(ipcPath) 57 .enc('utf8') 58 .connect() 59 .on('data', (response) => { 60 try { 61 response = JSON.parse(response); 62 } catch (e) { 63 console.error(e); 64 return; 65 } 66 if (self.callback) { 67 self.callback(response.error, response.result); 68 } 69 }); 70 71 if (this.config.mine_normally) { 72 this.startMiner(); 73 return; 74 } 75 76 self.stopMiner(() => { 77 self.fundAccount(function (err) { 78 if (err) { 79 console.error(err); 80 return; 81 } 82 if (self.config.mine_periodically) self.start_periodic_mining(); 83 if (self.config.mine_pending_txns) self.start_transaction_mining(); 84 }); 85 }); 86 87 } 88 89 sendCommand(method, params, callback) { 90 if (typeof params === 'function') { 91 callback = params; 92 params = []; 93 } 94 if (!callback) { 95 callback = function () { 96 }; 97 } 98 this.commandQueue.push({method, params: params || []}, callback); 99 } 100 101 startMiner(callback) { 102 if (this.started) { 103 if (callback) { 104 callback(); 105 } 106 return; 107 } 108 this.started = true; 109 this.sendCommand(minerStart, callback); 110 } 111 112 stopMiner(callback) { 113 if (!this.started) { 114 if (callback) { 115 callback(); 116 } 117 return; 118 } 119 this.started = false; 120 this.sendCommand(minerStop, callback); 121 } 122 123 getCoinbase(callback) { 124 if (this.coinbase) { 125 return callback(null, this.coinbase); 126 } 127 this.sendCommand(getCoinbase, (err, result) => { 128 if (err) { 129 return callback(err); 130 } 131 this.coinbase = result; 132 if (!this.coinbase) { 133 return callback('Failed getting coinbase account'); 134 } 135 callback(null, this.coinbase); 136 }); 137 } 138 139 accountFunded(callback) { 140 const self = this; 141 self.getCoinbase((err, coinbase) => { 142 if (err) { 143 return callback(err); 144 } 145 self.sendCommand(getBalance, [coinbase, 'latest'], (err, result) => { 146 if (err) { 147 return callback(err); 148 } 149 callback(null, parseInt(result, 16) >= self.config.initial_ether); 150 }); 151 }); 152 } 153 154 watchBlocks(filterCommand, callback, delay) { 155 const self = this; 156 self.sendCommand(filterCommand, (err, filterId) => { 157 if (err) { 158 return callback(err); 159 } 160 self.interval = setInterval(() => { 161 self.sendCommand(getChanges, [filterId], (err, changes) => { 162 if (err) { 163 console.error(err); 164 return; 165 } 166 if (!changes || !changes.length) { 167 return; 168 } 169 callback(null, changes); 170 }); 171 }, delay || 1000); 172 }); 173 } 174 175 mineUntilFunded(callback) { 176 const self = this; 177 this.startMiner(); 178 self.watchBlocks(newBlockFilter, (err) => { 179 if (err) { 180 console.error(err); 181 return; 182 } 183 self.accountFunded((err, funded) => { 184 if (funded) { 185 clearTimeout(self.interval); 186 self.stopMiner(); 187 callback(); 188 } 189 }); 190 }); 191 } 192 193 fundAccount(callback) { 194 const self = this; 195 196 self.accountFunded((err, funded) => { 197 if (err) { 198 return callback(err); 199 } 200 if (funded) { 201 return callback(); 202 } 203 204 console.log("== Funding account"); 205 self.mineUntilFunded(callback); 206 }); 207 } 208 209 pendingTransactions(callback) { 210 const self = this; 211 self.sendCommand(getBlockCount, ['pending'], (err, hexCount) => { 212 if (err) { 213 return callback(err); 214 } 215 callback(null, parseInt(hexCount, 16)); 216 }); 217 } 218 219 start_periodic_mining() { 220 const self = this; 221 const WAIT = 'wait'; 222 let last_mined_ms = Date.now(); 223 let timeout_set = false; 224 let next_block_in_ms; 225 226 self.startMiner(); 227 self.watchBlocks(newBlockFilter, (err) => { 228 if (err) { 229 console.error(err); 230 return; 231 } 232 if (timeout_set) { 233 return; 234 } 235 async.waterfall([ 236 function checkPendingTransactions(next) { 237 if (!self.config.mine_pending_txns) { 238 return next(); 239 } 240 self.pendingTransactions((err, count) => { 241 if (err) { 242 return next(err); 243 } 244 if (count) { 245 return next(WAIT); 246 } 247 next(); 248 }); 249 }, 250 function stopMiner(next) { 251 timeout_set = true; 252 253 const now = Date.now(); 254 const ms_since_block = now - last_mined_ms; 255 last_mined_ms = now; 256 257 if (ms_since_block > self.config.interval_ms) { 258 next_block_in_ms = 0; 259 } else { 260 next_block_in_ms = (self.config.interval_ms - ms_since_block); 261 } 262 self.stopMiner(); 263 console.log("== Looking for next block in " + next_block_in_ms + "ms"); 264 next(); 265 }, 266 function startAfterTimeout(next) { 267 setTimeout(function () { 268 console.log("== Looking for next block"); 269 timeout_set = false; 270 self.startMiner(); 271 next(); 272 }, next_block_in_ms); 273 } 274 ], (err) => { 275 if (err === WAIT) { 276 return; 277 } 278 if (err) { 279 console.error(err); 280 } 281 }); 282 }); 283 } 284 285 start_transaction_mining() { 286 const self = this; 287 const pendingTrasactionsMessage = "== Pending transactions! Looking for next block..."; 288 self.watchBlocks(pendingBlockFilter, (err) => { 289 if (err) { 290 console.error(err); 291 return; 292 } 293 self.sendCommand(getHashRate, (err, result) => { 294 if (result > 0) return; 295 296 console.log(pendingTrasactionsMessage); 297 self.startMiner(); 298 }); 299 }, 2000); 300 301 if (self.config.mine_periodically) return; 302 303 self.watchBlocks(newBlockFilter, (err) => { 304 if (err) { 305 console.error(err); 306 return; 307 } 308 self.pendingTransactions((err, count) => { 309 if (err) { 310 console.error(err); 311 return; 312 } 313 if (!count) { 314 console.log("== No transactions left. Stopping miner..."); 315 self.stopMiner(); 316 } else { 317 console.log(pendingTrasactionsMessage); 318 self.startMiner(); 319 } 320 }); 321 }, 2000); 322 } 323 } 324 325 module.exports = GethMiner;