1
+ """
2
+ Script to continuously update the `satoshis_per_byte` value in a TOML file with the
3
+ mean fee estimate from a list of API endpoints.
4
+
5
+ Usage:
6
+ $ COMMAND /path/to/miner.toml polling_delay_seconds
7
+
8
+ Args:
9
+ toml_file_location (str): The path to the TOML file to update.
10
+ polling_delay_seconds (int): The frequency in seconds to check for fee updates.
11
+ """
12
+
13
+ import toml
14
+ import json
15
+ import requests
16
+ import time
17
+ from backoff_utils import strategies
18
+ from backoff_utils import apply_backoff
19
+ from sys import argv
20
+
21
+ # Fee estimation API URLS and their corresponding fee extraction functions.
22
+ # At least one of these needs to be working in order for the script to function.
23
+ FEE_ESTIMATIONS = [
24
+ # Bitcoiner Live API
25
+ (
26
+ 'https://bitcoiner.live/api/fees/estimates/latest' ,
27
+ lambda response_json : response_json ["estimates" ]["30" ]["sat_per_vbyte" ],
28
+ ),
29
+
30
+ # Mempool Space API
31
+ (
32
+ 'https://mempool.space/api/v1/fees/recommended' ,
33
+ lambda response_json : response_json ["halfHourFee" ],
34
+ ),
35
+
36
+ # Blockchain.info API
37
+ (
38
+ 'https://api.blockchain.info/mempool/fees' ,
39
+ lambda response_json : response_json ["regular" ],
40
+ ),
41
+ ]
42
+
43
+ def calculate_fee_estimate ():
44
+ """
45
+ Calculates the mean fee estimate from a list of API URLs
46
+ and their corresponding fee extraction functions.
47
+
48
+ Args:
49
+ FEE_ESTIMATIONS (list): A list of tuples, where each tuple
50
+ contains the URL of an API endpoint and a function that extracts
51
+ the fee estimate from the JSON response.
52
+
53
+ Returns:
54
+ int: The mean fee estimate in sat/Byte.
55
+
56
+ Raises:
57
+ None
58
+ """
59
+
60
+ # Gather all API estimated fees in sat/Byte
61
+ estimated_fees = []
62
+ for api_url , unpack_fee_estimate in FEE_ESTIMATIONS :
63
+
64
+ try :
65
+ json_response = json .loads (get_from_api (api_url ))
66
+ estimated_fee = unpack_fee_estimate (json_response )
67
+ estimated_fees .append (estimated_fee )
68
+
69
+ except Exception as e :
70
+ pass
71
+
72
+ # Calculate the mean fee estimate
73
+ mean_fee = int (sum (estimated_fees ) / len (estimated_fees ))
74
+
75
+ return mean_fee
76
+
77
+ @apply_backoff (
78
+ strategy = strategies .Exponential ,
79
+ catch_exceptions = (RuntimeError ,),
80
+ max_tries = 3 ,
81
+ max_delay = 60 ,
82
+ )
83
+ def get_from_api (api_url : str ) -> str :
84
+ """
85
+ Sends a GET request to the specified API URL and returns the string response.
86
+
87
+ Args:
88
+ api_url (str): The URL of the API endpoint to call.
89
+
90
+ Returns:
91
+ dict: The string response data.
92
+
93
+ Raises:
94
+ RuntimeError: If the API call fails.
95
+ """
96
+
97
+ try :
98
+ # Make a GET request to the API endpoint
99
+ response = requests .get (api_url )
100
+
101
+ # Check if the request was successful
102
+ if response .status_code == 200 :
103
+ # Parse the response and return the data
104
+ return response .text
105
+
106
+ except Exception as e :
107
+ # If an exception occurs, raise a RuntimeError
108
+ raise RuntimeError ("Failed to unpack JSON." )
109
+
110
+ # If the code reaches this point, it means the API call failed.
111
+ raise RuntimeError ("Failed to get response." )
112
+
113
+
114
+ def update_config_fee (toml_file_location : str , polling_delay_seconds : int ):
115
+ """
116
+ Updates the `satoshis_per_byte` value in the specified TOML file
117
+ with the mean fee estimate from a list of API endpoints.
118
+
119
+ Args:
120
+ toml_file_location (str): The path to the TOML file to update.
121
+
122
+ Raises:
123
+ IOError: If the TOML file cannot be read or written.
124
+ RuntimeError: If the fee estimation process fails.
125
+ """
126
+
127
+ while True :
128
+ # Calculate mean fee estimate from the list of APIs
129
+ fee_estimate = calculate_fee_estimate ()
130
+
131
+ # Read toml file data
132
+ with open (toml_file_location , 'r' ) as toml_file :
133
+ toml_data = toml .load (toml_file )
134
+
135
+ # Update satoshis_per_byte data
136
+ toml_data ["burnchain" ]["satoshis_per_byte" ] = fee_estimate
137
+
138
+ # Update toml file with configuration changes
139
+ with open (toml_file_location , 'w' ) as toml_file :
140
+ toml .dump (toml_data , toml_file )
141
+
142
+ time .sleep ()
143
+
144
+ def read_config (config_location : str ):
145
+ """
146
+ Reads and returns the contents of a configuration file.
147
+ """
148
+ with open (config_location , "r" ) as config_file :
149
+ return json .load (config_file )
150
+
151
+ def main ():
152
+ """
153
+ Continuously updates the `satoshis_per_byte` value in the specified
154
+ TOML file with the mean fee estimate from a list of API endpoints.
155
+
156
+ Usage:
157
+ $ {argv[0]} /path/to/miner.toml polling_delay
158
+ """
159
+
160
+ try :
161
+ configuration = {}
162
+
163
+ if len (argv ) == 1 :
164
+ configuration = read_config ("./config/fee-estimate.json" )
165
+ elif "-c" in argv :
166
+ # Load configuration from specified file
167
+ config_location = argv [argv .index ("-c" ) + 1 ]
168
+ configuration = read_config (config_location )
169
+ else :
170
+ # Load configuration from command-line arguments
171
+ configuration = {
172
+ "toml_file_location" : argv [1 ],
173
+ "polling_delay_seconds" : int (argv [2 ]),
174
+ }
175
+
176
+ update_config_fee (** configuration )
177
+
178
+ # Print usage if there are errors.
179
+ except Exception as e :
180
+ print (f"Failed to run { argv [0 ]} " )
181
+ print (f"\n \t $ COMMAND /path/to/miner.toml polling_delay_seconds" )
182
+ print ("\t \t OR" )
183
+ print (f"\t $ COMMAND -c /path/to/config_file.json\n " )
184
+ print (f"Error: { e } " )
185
+
186
+ # Execute main.
187
+ if __name__ == "__main__" :
188
+ main ()
0 commit comments