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;