Skip to content

Commit ca554d4

Browse files
committed
implemented backend architecture for fee stats API
1 parent c79c7be commit ca554d4

8 files changed

Lines changed: 524 additions & 0 deletions

File tree

backend/.gitignore

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# test files
2+
test_mock.py
3+
test_secure_connection.py
4+
test_rpc_ports.py
5+
test_getbestblockhash.py
6+
test_flask_blockchain_info.py
7+
8+
rpc_config.ini
9+
10+
11+
app_mock.py

backend/README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
The Collector (The Brain): Runs in the background, continuously pulling real data, making a prediction, and archiving the result.
2+
3+
The API (The Server): Provides instantaneous responses by querying the pre-computed data, avoiding slow, repeated calls to the Bitcoin Core node.
4+
5+
app.py
6+
7+
The API Server (Flask)
8+
9+
Exposes endpoints for both real-time data (/fees) and high-performance historical analytics (/block-stats).
10+
11+
collector.py
12+
13+
The Background Worker
14+
15+
Continuously monitors the blockchain, runs the custom prediction logic (get_custom_fee_prediction_asap), and archives the prediction-vs-actual data.
16+
17+
json_rpc_request.py
18+
19+
The RPC Client
20+
21+
Handles all communication with the Bitcoin Core node. Includes exponential backoff and a built-in cache for reliable, efficient data retrieval.
22+
23+
database.py
24+
25+
The Persistence Layer (SQLite)
26+
27+
Stores the historical performance of our model (prediction, min fee, max fee) over thousands of blocks for instant retrieval. Uses INSERT OR IGNORE for stability.

backend/app.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
from flask import Flask, jsonify
2+
from flask_cors import CORS
3+
from werkzeug.middleware.proxy_fix import ProxyFix
4+
from json_rpc_request import (
5+
estimate_smart_fee,
6+
get_mempool_info,
7+
get_blockchain_info,
8+
get_block_stats,
9+
get_block_count
10+
)
11+
12+
app = Flask(__name__)
13+
app.wsgi_app = ProxyFix(app.wsgi_app)
14+
CORS(app)
15+
16+
@app.route("/fees/<int:target>/<string:mode>/<int:level>", methods=['GET'])
17+
def fees(target, mode, level):
18+
try:
19+
result = estimate_smart_fee(conf_target=target, mode=mode, verbosity_level=level)
20+
return jsonify(result)
21+
except Exception as e:
22+
print(f"RPC Error: {e}")
23+
return jsonify({"error": f"Failed to get fee estimate: {str(e)}"}), 500
24+
25+
@app.route("/mempool/info", methods=['GET'])
26+
def mempool_info():
27+
try:
28+
result = get_mempool_info()
29+
return jsonify(result)
30+
except Exception as e:
31+
print(f"RPC Error: {e}")
32+
return jsonify({"error": f"Failed to get mempool info: {str(e)}"}), 500
33+
34+
@app.route("/blockchain/info", methods=['GET'])
35+
def blockchain_info():
36+
try:
37+
result = get_blockchain_info()
38+
return jsonify(result)
39+
except Exception as e:
40+
print(f"RPC Error: {e}")
41+
return jsonify({"error": f"Failed to get blockchain info: {str(e)}"}), 500
42+
43+
@app.route("/blockstats/<int:block_height>", methods=['GET'])
44+
def block_stats(block_height):
45+
try:
46+
result = get_block_stats(block_height)
47+
return jsonify(result)
48+
except Exception as e:
49+
print(f"RPC Error: {e}")
50+
return jsonify({"error": f"Failed to get block stats: {str(e)}"}), 500
51+
52+
@app.route("/blockcount", methods=['GET'])
53+
def block_count():
54+
try:
55+
result = get_block_count()
56+
return jsonify({"blockcount": result})
57+
except Exception as e:
58+
print(f"RPC Error: {e}")
59+
return jsonify({"error": f"Failed to get block count: {str(e)}"}), 500
60+
61+
@app.route("/health", methods=['GET'])
62+
def health():
63+
try:
64+
get_blockchain_info()
65+
return jsonify({
66+
"status": "healthy",
67+
"service": "bitcoin-core-fees-api",
68+
"rpc_connected": True
69+
})
70+
except Exception as e:
71+
return jsonify({
72+
"status": "unhealthy",
73+
"service": "bitcoin-core-fees-api",
74+
"rpc_connected": False,
75+
"error": str(e)
76+
}), 503
77+
78+
@app.errorhandler(404)
79+
def page_not_found(error):
80+
return jsonify({"error": "Endpoint not found"}), 404
81+
82+
@app.errorhandler(500)
83+
def internal_error(error):
84+
return jsonify({"error": "Internal server error"}), 500
85+
86+
if __name__ == "__main__":
87+
app.run(debug=True, host='0.0.0.0', port=5000)

backend/collector.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import time
2+
import random
3+
import os
4+
from database import init_db, insert_analysis
5+
from json_rpc_request import (
6+
get_block_count,
7+
get_block_tx_details,
8+
get_raw_mempool,
9+
get_block_template,
10+
get_estimated_fee_rate_satvb
11+
)
12+
13+
# Configuration for the collector loop
14+
COLLECTOR_SLEEP_TIME = 30 # for a new block every 30 seconds
15+
LAST_PROCESSED_HEIGHT = 0
16+
INITIAL_HISTORY_DEPTH = 1000 # The number of blocks to fetch initially
17+
18+
# custom fee logic
19+
20+
def get_custom_fee_prediction_asap() -> float:
21+
"""
22+
CRITICAL: This is the placeholder where your custom, robust fee estimation
23+
logic must be implemented. It should use the latest RPC data to make a prediction.
24+
25+
Returns: A single ASAP feerate prediction in sat/vB.
26+
"""
27+
try:
28+
# 1. Fetch real-time data from the node
29+
# These are available for your custom logic:
30+
# mempool_data = get_raw_mempool(verbose=False)
31+
# block_template = get_block_template()
32+
33+
# A SIMPLE, TEMPORARY MODEL BASED ON Core's 1-Block Estimate
34+
# REPLACE THIS WITH YOUR CUSTOM LOGIC (Feerate Bucketing, Resting Time Analysis, etc.)
35+
36+
# We use Core's estimate as a reliable temporary proxy for demonstration
37+
core_estimate = get_estimated_fee_rate_satvb(conf_target=1, mode='conservative')
38+
predicted_fee = core_estimate.get('feerate_sat_per_vb')
39+
40+
if predicted_fee is None or predicted_fee < 1.0:
41+
# Fallback for when estimatesmartfee fails
42+
print("Warning: estimatesmartfee failed. Using a random prediction.")
43+
return round(random.uniform(5.0, 15.0), 2)
44+
45+
# Add a small random jitter to simulate a model slightly different from Core
46+
jitter = random.uniform(-0.5, 1.5)
47+
return max(1.0, predicted_fee + jitter)
48+
49+
except Exception as e:
50+
print(f"Prediction Error: {e}. Returning fallback fee.")
51+
return 10.0 # Safe fallback fee
52+
53+
# block processing
54+
55+
def process_block(height: int):
56+
"""
57+
Fetches actual block data, runs prediction, and stores the result.
58+
"""
59+
try:
60+
# 1. Run the Prediction Logic (What would we have predicted for this block?)
61+
# Since we are processing blocks sequentially, we use the current prediction
62+
# as a proxy for the prediction we would have made just before the block was found.
63+
predicted_fee = get_custom_fee_prediction_asap()
64+
65+
# 2. Get Actuals (Ground Truth) for the MINED block
66+
block_details = get_block_tx_details(height)
67+
68+
if block_details and block_details.get('min_fee') is not None:
69+
min_fee = block_details['min_fee']
70+
max_fee = block_details['max_fee']
71+
72+
# 3. Store the result
73+
insert_analysis(height, min_fee, max_fee, predicted_fee)
74+
print(f"[COLLECTOR] Processed Block {height}: Actual Min={min_fee}, Predicted={predicted_fee}")
75+
return True
76+
else:
77+
# This happens if the block is still being processed or is empty/invalid
78+
print(f"[COLLECTOR] Skipped Block {height}: No valid fee details found.")
79+
return False
80+
81+
except Exception as e:
82+
print(f"[COLLECTOR] Error processing block {height}: {e}")
83+
return False
84+
85+
def run_collector_cycle(initial_population: bool = False):
86+
"""
87+
Executes one cycle: detects and processes new blocks since the last check.
88+
"""
89+
global LAST_PROCESSED_HEIGHT
90+
91+
current_height = get_block_count()
92+
if current_height is None:
93+
return
94+
95+
if LAST_PROCESSED_HEIGHT == 0:
96+
# On first run, set the last processed height to the current height minus one
97+
LAST_PROCESSED_HEIGHT = current_height - 1
98+
99+
100+
if initial_population:
101+
# For initial run, process a large range of historical blocks
102+
start_height = max(1, current_height - INITIAL_HISTORY_DEPTH)
103+
print(f"\n[COLLECTOR] Starting initial population from Block {start_height} to {current_height}...")
104+
else:
105+
# For continuous run, process only new blocks
106+
start_height = LAST_PROCESSED_HEIGHT + 1
107+
108+
109+
blocks_to_process = range(start_height, current_height + 1)
110+
111+
for height in blocks_to_process:
112+
if height > LAST_PROCESSED_HEIGHT:
113+
if process_block(height):
114+
LAST_PROCESSED_HEIGHT = height
115+
else:
116+
# Skip blocks already processed during initial population
117+
continue
118+
119+
120+
def start_collector():
121+
"""Main function to run the collector indefinitely."""
122+
print("--- Starting Fee Estimation Collector ---")
123+
124+
# 1. Initial Historical Population (fills the database)
125+
run_collector_cycle(initial_population=True)
126+
127+
print("\nInitial historical population complete. Monitoring for new blocks...")
128+
129+
# 2. Continuous Monitoring Loop
130+
while True:
131+
try:
132+
run_collector_cycle(initial_population=False)
133+
except Exception as e:
134+
print(f"[COLLECTOR] Critical main loop error: {e}. Retrying in {COLLECTOR_SLEEP_TIME}s.")
135+
136+
time.sleep(COLLECTOR_SLEEP_TIME)
137+
138+
if __name__ == '__main__':
139+
init_db()
140+
start_collector()

backend/database.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# hey what do u think of having database??
2+
3+
4+
import sqlite3
5+
6+
DATABASE_NAME = 'fee_analysis.db'
7+
8+
def init_db():
9+
"""Initializes the SQLite database and creates the fee_analysis table."""
10+
conn = sqlite3.connect(DATABASE_NAME)
11+
cursor = conn.cursor()
12+
13+
# Create the table to store historical data:
14+
# 1. block_height: Primary key
15+
# 2. min_feerate: Actual minimum feerate of a transaction in the block (sat/vB)
16+
# 3. max_feerate: Actual maximum feerate of a transaction in the block (sat/vB)
17+
# 4. predicted_feerate: Our model's prediction for the ASAP feerate (sat/vB)
18+
# 5. forecaster_name: Allows comparison of multiple models
19+
cursor.execute("""
20+
CREATE TABLE IF NOT EXISTS fee_analysis (
21+
block_height INTEGER PRIMARY KEY,
22+
min_feerate REAL NOT NULL,
23+
max_feerate REAL NOT NULL,
24+
predicted_feerate REAL NOT NULL,
25+
forecaster_name TEXT NOT NULL
26+
)
27+
""")
28+
conn.commit()
29+
conn.close()
30+
31+
def insert_analysis(block_height, min_feerate, max_feerate, predicted_feerate, forecaster_name="OurModelV1"):
32+
"""Inserts one record of block analysis into the database."""
33+
conn = sqlite3.connect(DATABASE_NAME)
34+
cursor = conn.cursor()
35+
try:
36+
cursor.execute("""
37+
INSERT OR IGNORE INTO fee_analysis (block_height, min_feerate, max_feerate, predicted_feerate, forecaster_name)
38+
VALUES (?, ?, ?, ?, ?)
39+
""", (block_height, min_feerate, max_feerate, predicted_feerate, forecaster_name))
40+
conn.commit()
41+
except sqlite3.Error as e:
42+
print(f"Database Error on insert: {e}")
43+
finally:
44+
conn.close()
45+
46+
def fetch_analysis_range(start_height, end_height, forecaster_name="OurModelV1"):
47+
"""Fetches all necessary data for a given block range."""
48+
conn = sqlite3.connect(DATABASE_NAME)
49+
cursor = conn.cursor()
50+
51+
52+
cursor.execute("""
53+
SELECT block_height, min_feerate, max_feerate, predicted_feerate
54+
FROM fee_analysis
55+
WHERE block_height <= ? AND block_height > ? AND forecaster_name = ?
56+
ORDER BY block_height DESC
57+
""", (start_height, end_height, forecaster_name))
58+
59+
results = cursor.fetchall()
60+
conn.close()
61+
return results
62+
63+
if __name__ == '__main__':
64+
init_db()
65+
print("Database initialized.")

0 commit comments

Comments
 (0)