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()