/ lib / bitcoind-rpc / fees.js
fees.js
  1  /*!
  2   * lib/bitcoind-rpc/fees.js
  3   * Copyright © 2019 – Katana Cryptographic Ltd. All Rights Reserved.
  4   */
  5  
  6  import { execSync } from 'child_process'
  7  import path from 'path'
  8  
  9  import errors from '../errors.js'
 10  import Logger from '../logger.js'
 11  import network from '../bitcoin/network.js'
 12  import keysFile from '../../keys/index.js'
 13  import { createRpcClient, waitForBitcoindRpcApi } from './rpc-client.js'
 14  import latestBlock from './latest-block.js'
 15  
 16  const keys = keysFile[network.key]
 17  
 18  /**
 19   * @typedef {import('@samouraiwallet/one-dollar-fee-estimator/dist/types').Result} EstimatorData
 20   */
 21  
 22  /**
 23   * A singleton providing information about network fees
 24   */
 25  class Fees {
 26  
 27      constructor() {
 28          this.block = -1
 29          this.fees = {
 30              2: 1,
 31              4: 1,
 32              6: 1,
 33              12: 1,
 34              24: 1
 35          }
 36          /**
 37           * @type {EstimatorData}
 38           */
 39          this.estimatorData = {
 40              ready: false,
 41              lastBlock: null,
 42              fees: {
 43                  '0.1': 1,
 44                  '0.2': 1,
 45                  '0.5': 1,
 46                  '0.9': 1,
 47                  '0.99': 1,
 48                  '0.999': 1,
 49              }
 50          }
 51          this.feeType = keys.bitcoind.feeType
 52          this.timer = null
 53  
 54          this.rpcClient = createRpcClient()
 55  
 56          waitForBitcoindRpcApi().then(() => {
 57              this.refresh()
 58          })
 59  
 60          this.initIpc()
 61      }
 62  
 63      /**
 64       * Async function to initialize inter-process communication for cases where accounts process is running in cluster mode
 65       * @returns {Promise<void>}
 66       */
 67      async initIpc() {
 68          try {
 69              const NpmRoot = execSync('npm root -g').toString('utf8')
 70              const pm2Module = await import(path.join(NpmRoot, 'pm2', 'index.js'))
 71              const pm2 = pm2Module.default
 72  
 73              pm2.launchBus((error, pm2_bus) => {
 74                  if (error) {
 75                      Logger.error(error, 'Fees : PM2 launchbus failed')
 76                      return
 77                  }
 78  
 79                  pm2_bus.on('process:msg', (packet) => {
 80                      const data = packet.data
 81                      if (data && data.topic === 'fee-estimator') {
 82                          this.estimatorData = data.value
 83                      }
 84                  })
 85              })
 86          } catch (error) { // allowed to fail
 87              Logger.error(error, 'Fees : IPC initialization failed')
 88          }
 89      }
 90  
 91      /**
 92       * Refresh and return the current fees
 93       * @returns {Promise<object>}
 94       */
 95      async getFees() {
 96          try {
 97              if (latestBlock.height > this.block)
 98                  await this.refresh()
 99  
100              return this.fees
101  
102          } catch {
103              throw errors.generic.GEN
104          }
105      }
106  
107      /**
108       * Get fee rates calculated by $1 Fee Estimator
109       * @returns {EstimatorData.fees}
110       * @throws {string} - Throw error if estimator hasn't sent any data or if bitcoind mempool isn't fully loaded
111       */
112      getEstimatorFees() {
113          if (this.estimatorData.ready === false) throw errors.estimator.NOT_AVAILABLE
114  
115          return this.estimatorData.fees
116      }
117  
118      /**
119       * Refresh the current fees
120       * @returns {Promise<void>}
121       */
122      async refresh() {
123          clearTimeout(this.timer)
124  
125          try {
126              const requests = Object.keys(this.fees).map(Number).map((target) => {
127                  return { method: 'estimatesmartfee', params: { conf_target: target, estimate_mode: this.feeType }, id: target }
128              })
129              const responses = await this.rpcClient.batch(requests)
130  
131              for (const fee of responses) {
132                  const feerate = (fee.result.errors && fee.result.errors.length > 0) ? 1 : Math.round(fee.result.feerate * 1e5)
133                  // add 1 to feerate if the feerate is larger than 1 to avoid underpaying
134                  this.fees[fee.id] = feerate === 1 ? feerate : feerate + 1
135              }
136          } catch (error) {
137              Logger.error(error, 'Bitcoind RPC : Fees.refresh()')
138          }
139          this.block = latestBlock.height
140          // Make this method refresh fees at least every minute
141          this.timer = setTimeout(() => this.refresh(), 60000) // 1 MINUTE
142      }
143  
144  }
145  
146  export default new Fees()