Add longTermFeeRate websocket endpoint (#1262)

* feat: add longTermFeeRate websocket endpoint

* chore: regenerate blockbook-api.ts with longTermFeeRate
This commit is contained in:
Jiří Musil
2025-06-09 14:09:15 +02:00
committed by GitHub
parent 8989ab75c2
commit ae0172dddf
10 changed files with 91 additions and 0 deletions

View File

@@ -620,3 +620,8 @@ type Eip1559Fees struct {
PriorityFeeTrend string `json:"priorityFeeTrend,omitempty" ts_type:"'up' | 'down'"`
BaseFeeTrend string `json:"baseFeeTrend,omitempty" ts_type:"'up' | 'down'"`
}
type LongTermFeeRate struct {
FeePerUnit string `json:"feePerUnit" ts_doc:"Long term fee rate (in sat/kByte)."`
Blocks uint64 `json:"blocks" ts_doc:"Amount of blocks used for the long term fee rate estimation."`
}

View File

@@ -39,6 +39,11 @@ func (b *BaseChain) GetMempoolEntry(txid string) (*MempoolEntry, error) {
return nil, errors.New("GetMempoolEntry: not supported")
}
// LongTermFeeRate returns smallest fee rate from historic blocks.
func (b *BaseChain) LongTermFeeRate() (*LongTermFeeRate, error) {
return nil, errors.New("not supported")
}
// EthereumTypeGetBalance is not supported
func (b *BaseChain) EthereumTypeGetBalance(addrDesc AddressDescriptor) (*big.Int, error) {
return nil, errors.New("not supported")

View File

@@ -297,6 +297,11 @@ func (c *blockChainWithMetrics) EstimateFee(blocks int) (v big.Int, err error) {
return c.b.EstimateFee(blocks)
}
func (c *blockChainWithMetrics) LongTermFeeRate() (v *bchain.LongTermFeeRate, err error) {
defer func(s time.Time) { c.observeRPCLatency("LongTermFeeRate", s, err) }(time.Now())
return c.b.LongTermFeeRate()
}
func (c *blockChainWithMetrics) SendRawTransaction(tx string) (v string, err error) {
defer func(s time.Time) { c.observeRPCLatency("SendRawTransaction", s, err) }(time.Now())
return c.b.SendRawTransaction(tx)

View File

@@ -873,6 +873,21 @@ func (b *BitcoinRPC) EstimateFee(blocks int) (big.Int, error) {
return r, nil
}
// LongTermFeeRate returns smallest fee rate from historic blocks.
func (b *BitcoinRPC) LongTermFeeRate() (*bchain.LongTermFeeRate, error) {
blocks := 1008 // ~7 days of blocks, highest number estimatesmartfee supports
glog.V(1).Info("rpc: estimatesmartfee (long term fee rate) - ", blocks)
// Going for the ECONOMICAL mode, to get the lowest fee rate
feePerUnit, err := b.blockchainEstimateSmartFee(blocks, false)
if err != nil {
return nil, err
}
return &bchain.LongTermFeeRate{
Blocks: uint64(blocks),
FeePerUnit: feePerUnit,
}, nil
}
// SendRawTransaction sends raw transaction
func (b *BitcoinRPC) SendRawTransaction(tx string) (string, error) {
glog.V(1).Info("rpc: sendrawtransaction")

View File

@@ -211,6 +211,12 @@ type ChainInfo struct {
Consensus interface{} `json:"consensus,omitempty" ts_doc:"Additional consensus details, structure depends on chain."`
}
// LongTermFeeRate gets information about the fee rate over longer period of time.
type LongTermFeeRate struct {
FeePerUnit big.Int `json:"feePerUnit" ts_doc:"Long term fee rate (in sat/kByte)."`
Blocks uint64 `json:"blocks" ts_doc:"Amount of blocks used for the long term fee rate estimation."`
}
// RPCError defines rpc error returned by backend
type RPCError struct {
Code int `json:"code" ts_doc:"Error code returned by the backend RPC."`
@@ -324,6 +330,7 @@ type BlockChain interface {
GetTransactionSpecific(tx *Tx) (json.RawMessage, error)
EstimateSmartFee(blocks int, conservative bool) (big.Int, error)
EstimateFee(blocks int) (big.Int, error)
LongTermFeeRate() (*LongTermFeeRate, error)
SendRawTransaction(tx string) (string, error)
GetMempoolEntry(txid string) (*MempoolEntry, error)
GetContractInfo(contractDesc AddressDescriptor) (*ContractInfo, error)

View File

@@ -745,6 +745,12 @@ export interface WsEstimateFeeRes {
feeLimit?: string;
eip1559?: Eip1559Fees;
}
export interface WsLongTermFeeRateRes {
/** Long term fee rate (in sat/kByte). */
feePerUnit: string;
/** Amount of blocks used for the long term fee rate estimation. */
blocks: number;
}
export interface WsSendTransactionReq {
/** Hex-encoded transaction data to broadcast. */
hex: string;

View File

@@ -54,6 +54,7 @@ func main() {
t.Add(server.WsTransactionSpecificReq{})
t.Add(server.WsEstimateFeeReq{})
t.Add(server.WsEstimateFeeRes{})
t.Add(server.WsLongTermFeeRateRes{})
t.Add(server.WsSendTransactionReq{})
t.Add(server.WsSubscribeAddressesReq{})
t.Add(server.WsSubscribeFiatRatesReq{})

View File

@@ -369,6 +369,9 @@ var requestHandlers = map[string]func(*WebsocketServer, *websocketChannel, *WsRe
"estimateFee": func(s *WebsocketServer, c *websocketChannel, req *WsReq) (rv interface{}, err error) {
return s.estimateFee(req.Params)
},
"longTermFeeRate": func(s *WebsocketServer, c *websocketChannel, req *WsReq) (rv interface{}, err error) {
return s.longTermFeeRate()
},
"sendTransaction": func(s *WebsocketServer, c *websocketChannel, req *WsReq) (rv interface{}, err error) {
r := WsSendTransactionReq{}
err = json.Unmarshal(req.Params, &r)
@@ -737,6 +740,17 @@ func (s *WebsocketServer) estimateFee(params []byte) (interface{}, error) {
return res, nil
}
func (s *WebsocketServer) longTermFeeRate() (res interface{}, err error) {
feeRate, err := s.chain.LongTermFeeRate()
if err != nil {
return nil, err
}
return WsLongTermFeeRateRes{
FeePerUnit: feeRate.FeePerUnit.String(),
Blocks: feeRate.Blocks,
}, nil
}
func (s *WebsocketServer) sendTransaction(tx string) (res resultSendTransaction, err error) {
txid, err := s.chain.SendRawTransaction(tx)
if err != nil {

View File

@@ -133,6 +133,12 @@ type WsEstimateFeeRes struct {
Eip1559 *api.Eip1559Fees `json:"eip1559,omitempty"`
}
// WsLongTermFeeRateRes is returned in response to a long term fee rate request.
type WsLongTermFeeRateRes struct {
FeePerUnit string `json:"feePerUnit" ts_doc:"Long term fee rate (in sat/kByte)."`
Blocks uint64 `json:"blocks" ts_doc:"Amount of blocks used for the long term fee rate estimation."`
}
// WsSendTransactionReq is used to broadcast a transaction to the network.
type WsSendTransactionReq struct {
Hex string `json:"hex" ts_doc:"Hex-encoded transaction data to broadcast."`

View File

@@ -289,6 +289,19 @@
}
}
function longTermFeeRate() {
try {
const method = 'longTermFeeRate';
send(method, {}, function (result) {
document.getElementById('longTermFeeRateResult').innerText = JSON.stringify(
result,
).replace(/,/g, ', ');
});
} catch (e) {
document.getElementById('longTermFeeRateResult').innerText = e;
}
}
function sendTransaction() {
var hex = document.getElementById('sendTransactionHex').value.trim();
const method = 'sendTransaction';
@@ -892,6 +905,20 @@
<div class="row">
<div class="col" id="estimateFeeResult"></div>
</div>
<div class="row">
<div class="col">
<input
class="btn btn-secondary"
type="button"
value="longTermFeeRate"
onclick="longTermFeeRate()"
/>
</div>
<div class="col"></div>
</div>
<div class="row">
<div class="col" id="longTermFeeRateResult"></div>
</div>
<div class="row">
<div class="col">
<input