Skip to content

Commit a25703d

Browse files
authored
Merge pull request #4080 from stacks-network/sidecar-scripts
Add sidecar scripts for fee estimation and stacks block delay detection
2 parents 33c64f6 + 34fe70a commit a25703d

File tree

4 files changed

+424
-0
lines changed

4 files changed

+424
-0
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"toml_file_location": "Path/To/miner.toml",
3+
"polling_delay_seconds": 60
4+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"polling_delay_seconds": 60,
3+
"max_stacks_delay_seconds": 1500,
4+
"recovery_delay_seconds": 660,
5+
"shell_command": ["echo", "command not specified"]
6+
}

contrib/side-cars/fee-estimate.py

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
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\tOR")
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

Comments
 (0)