package lnwallet import ( "encoding/json" "github.com/roasbeef/btcd/blockchain" "github.com/roasbeef/btcd/rpcclient" "github.com/roasbeef/btcutil" ) // FeeEstimator provides the ability to estimate on-chain transaction fees for // various combinations of transaction sizes and desired confirmation time // (measured by number of blocks). type FeeEstimator interface { // EstimateFeePerByte takes in a target for the number of blocks until // an initial confirmation and returns the estimated fee expressed in // satoshis/byte. EstimateFeePerByte(numBlocks uint32) (btcutil.Amount, error) // EstimateFeePerWeight takes in a target for the number of blocks // until an initial confirmation and returns the estimated fee // expressed in satoshis/weight. EstimateFeePerWeight(numBlocks uint32) (btcutil.Amount, error) // Start signals the FeeEstimator to start any processes or goroutines // it needs to perform its duty. Start() error // Stop stops any spawned goroutines and cleans up the resources used // by the fee estimator. Stop() error } // StaticFeeEstimator will return a static value for all fee calculation // requests. It is designed to be replaced by a proper fee calculation // implementation. type StaticFeeEstimator struct { // FeeRate is the static fee rate in satoshis-per-byte that will be // returned by this fee estimator. Queries for the fee rate in weight // units will be scaled accordingly. FeeRate btcutil.Amount } // EstimateFeePerByte will return a static value for fee calculations. // // NOTE: This method is part of the FeeEstimator interface. func (e StaticFeeEstimator) EstimateFeePerByte(numBlocks uint32) (btcutil.Amount, error) { return e.FeeRate, nil } // EstimateFeePerWeight will return a static value for fee calculations. // // NOTE: This method is part of the FeeEstimator interface. func (e StaticFeeEstimator) EstimateFeePerWeight(numBlocks uint32) (btcutil.Amount, error) { return e.FeeRate / blockchain.WitnessScaleFactor, nil } // Start signals the FeeEstimator to start any processes or goroutines // it needs to perform its duty. // // NOTE: This method is part of the FeeEstimator interface. func (e StaticFeeEstimator) Start() error { return nil } // Stop stops any spawned goroutines and cleans up the resources used // by the fee estimator. // // NOTE: This method is part of the FeeEstimator interface. func (e StaticFeeEstimator) Stop() error { return nil } // A compile-time assertion to ensure that StaticFeeEstimator implements the // FeeEstimator interface. var _ FeeEstimator = (*StaticFeeEstimator)(nil) // BtcdFeeEstimator is an implementation of the FeeEstimator interface backed // by the RPC interface of an active btcd node. This implementation will proxy // any fee estimation requests to btcd's RPC interface. type BtcdFeeEstimator struct { // fallBackFeeRate is the fall back fee rate in satoshis per byte that // is returned if the fee estimator does not yet have enough data to // actually produce fee estimates. fallBackFeeRate btcutil.Amount btcdConn *rpcclient.Client } // NewBtcdFeeEstimator creates a new BtcdFeeEstimator given a fully populated // rpc config that is able to successfully connect and authenticate with the // btcd node, and also a fall back fee rate. The fallback fee rate is used in // the occasion that the estimator has insufficient data, or returns zero for a // fee estimate. func NewBtcdFeeEstimator(rpcConfig rpcclient.ConnConfig, fallBackFeeRate btcutil.Amount) (*BtcdFeeEstimator, error) { rpcConfig.DisableConnectOnNew = true rpcConfig.DisableAutoReconnect = false chainConn, err := rpcclient.New(&rpcConfig, nil) if err != nil { return nil, err } return &BtcdFeeEstimator{ fallBackFeeRate: fallBackFeeRate, btcdConn: chainConn, }, nil } // Start signals the FeeEstimator to start any processes or goroutines // it needs to perform its duty. // // NOTE: This method is part of the FeeEstimator interface. func (b *BtcdFeeEstimator) Start() error { if err := b.btcdConn.Connect(20); err != nil { return err } return nil } // Stop stops any spawned goroutines and cleans up the resources used // by the fee estimator. // // NOTE: This method is part of the FeeEstimator interface. func (b *BtcdFeeEstimator) Stop() error { b.btcdConn.Shutdown() return nil } // EstimateFeePerByte takes in a target for the number of blocks until an // initial confirmation and returns the estimated fee expressed in // satoshis/byte. func (b *BtcdFeeEstimator) EstimateFeePerByte(numBlocks uint32) (btcutil.Amount, error) { feeEstimate, err := b.fetchEstimatePerByte(numBlocks) switch { // If the estimator doesn't have enough data, or returns an error, then // to return a proper value, then we'll return the default fall back // fee rate. case err != nil: walletLog.Errorf("unable to query estimator: %v", err) fallthrough case feeEstimate == 0: return b.fallBackFeeRate, nil } return feeEstimate, nil } // EstimateFeePerWeight takes in a target for the number of blocks until an // initial confirmation and returns the estimated fee expressed in // satoshis/weight. func (b *BtcdFeeEstimator) EstimateFeePerWeight(numBlocks uint32) (btcutil.Amount, error) { feePerByte, err := b.EstimateFeePerByte(numBlocks) if err != nil { return 0, err } // We'll scale down the fee per byte to fee per weight, as for each raw // byte, there's 1/4 unit of weight mapped to it. satWeight := feePerByte / blockchain.WitnessScaleFactor // If this ends up scaling down to a zero sat/weight amount, then we'll // use the default fallback fee rate. if satWeight == 0 { return b.fallBackFeeRate / blockchain.WitnessScaleFactor, nil } return satWeight, nil } // fetchEstimate returns a fee estimate for a transaction be be confirmed in // confTarget blocks. The estimate is returned in sat/byte. func (b *BtcdFeeEstimator) fetchEstimatePerByte(confTarget uint32) (btcutil.Amount, error) { // First, we'll fetch the estimate for our confirmation target. btcPerKB, err := b.btcdConn.EstimateFee(int64(confTarget)) if err != nil { return 0, err } // Next, we'll convert the returned value to satoshis, as it's // currently returned in BTC. satPerKB, err := btcutil.NewAmount(btcPerKB) if err != nil { return 0, err } // The value returned is expressed in fees per KB, while we want // fee-per-byte, so we'll divide by 1024 to map to satoshis-per-byte // before returning the estimate. satPerByte := satPerKB / 1024 walletLog.Debugf("Returning %v sat/byte for conf target of %v", int64(satPerByte), confTarget) return satPerByte, nil } // A compile-time assertion to ensure that BtcdFeeEstimator implements the // FeeEstimator interface. var _ FeeEstimator = (*BtcdFeeEstimator)(nil) // BitcoindFeeEstimator is an implementation of the FeeEstimator interface // backed by the RPC interface of an active bitcoind node. This implementation // will proxy any fee estimation requests to bitcoind's RPC interface. type BitcoindFeeEstimator struct { // fallBackFeeRate is the fall back fee rate in satoshis per byte that // is returned if the fee estimator does not yet have enough data to // actually produce fee estimates. fallBackFeeRate btcutil.Amount bitcoindConn *rpcclient.Client } // NewBitcoindFeeEstimator creates a new BitcoindFeeEstimator given a fully // populated rpc config that is able to successfully connect and authenticate // with the bitcoind node, and also a fall back fee rate. The fallback fee rate // is used in the occasion that the estimator has insufficient data, or returns // zero for a fee estimate. func NewBitcoindFeeEstimator(rpcConfig rpcclient.ConnConfig, fallBackFeeRate btcutil.Amount) (*BitcoindFeeEstimator, error) { rpcConfig.DisableConnectOnNew = true rpcConfig.DisableAutoReconnect = false rpcConfig.DisableTLS = true rpcConfig.HTTPPostMode = true chainConn, err := rpcclient.New(&rpcConfig, nil) if err != nil { return nil, err } return &BitcoindFeeEstimator{ fallBackFeeRate: fallBackFeeRate, bitcoindConn: chainConn, }, nil } // Start signals the FeeEstimator to start any processes or goroutines // it needs to perform its duty. // // NOTE: This method is part of the FeeEstimator interface. func (b *BitcoindFeeEstimator) Start() error { return nil } // Stop stops any spawned goroutines and cleans up the resources used // by the fee estimator. // // NOTE: This method is part of the FeeEstimator interface. func (b *BitcoindFeeEstimator) Stop() error { return nil } // EstimateFeePerByte takes in a target for the number of blocks until an // initial confirmation and returns the estimated fee expressed in // satoshis/byte. func (b *BitcoindFeeEstimator) EstimateFeePerByte(numBlocks uint32) (btcutil.Amount, error) { feeEstimate, err := b.fetchEstimatePerByte(numBlocks) switch { // If the estimator doesn't have enough data, or returns an error, then // to return a proper value, then we'll return the default fall back // fee rate. case err != nil: walletLog.Errorf("unable to query estimator: %v", err) fallthrough case feeEstimate == 0: return b.fallBackFeeRate, nil } return feeEstimate, nil } // EstimateFeePerWeight takes in a target for the number of blocks until an // initial confirmation and returns the estimated fee expressed in // satoshis/weight. func (b *BitcoindFeeEstimator) EstimateFeePerWeight(numBlocks uint32) (btcutil.Amount, error) { feePerByte, err := b.EstimateFeePerByte(numBlocks) if err != nil { return 0, err } // We'll scale down the fee per byte to fee per weight, as for each raw // byte, there's 1/4 unit of weight mapped to it. satWeight := feePerByte / blockchain.WitnessScaleFactor // If this ends up scaling down to a zero sat/weight amount, then we'll // use the default fallback fee rate. // TODO(aakselrod): maybe use the per-byte rate if it's non-zero? // Otherwise, we can return a higher sat/byte than sat/weight. if satWeight == 0 { return b.fallBackFeeRate / blockchain.WitnessScaleFactor, nil } return satWeight, nil } // fetchEstimate returns a fee estimate for a transaction be be confirmed in // confTarget blocks. The estimate is returned in sat/byte. func (b *BitcoindFeeEstimator) fetchEstimatePerByte(confTarget uint32) (btcutil.Amount, error) { // First, we'll send an "estimatesmartfee" command as a raw request, // since it isn't supported by btcd but is available in bitcoind. target, err := json.Marshal(uint64(confTarget)) if err != nil { return 0, err } // TODO: Allow selection of economical/conservative modifiers. resp, err := b.bitcoindConn.RawRequest("estimatesmartfee", []json.RawMessage{target}) if err != nil { return 0, err } // Next, we'll parse the response to get the BTC per KB. feeEstimate := struct { Feerate float64 `json:"feerate"` }{} err = json.Unmarshal(resp, &feeEstimate) if err != nil { return 0, err } // Next, we'll convert the returned value to satoshis, as it's // currently returned in BTC. satPerKB, err := btcutil.NewAmount(feeEstimate.Feerate) if err != nil { return 0, err } // The value returned is expressed in fees per KB, while we want // fee-per-byte, so we'll divide by 1000 to map to satoshis-per-byte // before returning the estimate. satPerByte := satPerKB / 1000 walletLog.Debugf("Returning %v sat/byte for conf target of %v", int64(satPerByte), confTarget) return satPerByte, nil } // A compile-time assertion to ensure that BitcoindFeeEstimator implements the // FeeEstimator interface. var _ FeeEstimator = (*BitcoindFeeEstimator)(nil)