Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.env
__pycache__/
uv.lock
uv.lock.venv./
.venv/
1 change: 1 addition & 0 deletions config/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .config import PROVIDERS, PRICING_CONFIG, ALCHEMY_COMPUTE_UNITS, QUICKNODE_CREDITS
55 changes: 55 additions & 0 deletions config/compute_units.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
ALCHEMY_COMPUTE_UNITS = {
'net_version': 0,
'eth_chainId': 0,
'eth_syncing': 0,
'eth_protocolVersion': 0,
'net_listening': 0,
'eth_uninstallFilter': 10,
'eth_accounts': 10,
'eth_blockNumber': 10,
'eth_subscribe': 10,
'eth_unsubscribe': 10,
'eth_feeHistory': 10,
'eth_maxPriorityFeePerGas': 10,
'eth_createAccessList': 10,
'eth_getTransactionReceipt': 20,
'eth_getUncleByBlockHashAndIndex': 20,
'eth_getUncleByBlockNumberAndIndex': 20,
'eth_getTransactionByBlockHashAndIndex': 20,
'eth_getTransactionByBlockNumberAndIndex': 20,
'eth_getUncleCountByBlockHash': 20,
'eth_getUncleCountByBlockNumber': 20,
'web3_clientVersion': 20,
'web3_sha3': 20,
'eth_getBlockByNumber': 20,
'eth_getStorageAt': 20,
'eth_getTransactionByHash': 20,
'eth_gasPrice': 20,
'eth_getBalance': 20,
'eth_getCode': 20,
'eth_getFilterChanges': 20,
'eth_newBlockFilter': 20,
'eth_newFilter': 20,
'eth_simulateV1': 40,
'eth_newPendingTransactionFilter': 20,
'eth_getBlockTransactionCountByHash': 20,
'eth_getBlockTransactionCountByNumber': 20,
'eth_getProof': 20,
'eth_getBlockByHash': 20,
'erigon_forks': 20,
'erigon_getHeaderByHash': 20,
'erigon_getHeaderByNumber': 20,
'erigon_getLogsByHash': 20,
'erigon_issuance': 20,
'eth_getTransactionCount': 20,
'eth_call': 26,
'eth_getFilterLogs': 60,
'eth_getLogs': 60,
'eth_estimateGas': 20,
'eth_sendRawTransaction': 40,
}

QUICKNODE_CREDITS = {
'trace_call': 40,
'default': 20
}
53 changes: 53 additions & 0 deletions config/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import os
from dotenv import load_dotenv
from config.compute_units import ALCHEMY_COMPUTE_UNITS, QUICKNODE_CREDITS

load_dotenv(os.path.join(os.path.dirname(os.path.dirname(__file__)), ".env"))

PRICING_CONFIG = {
"chainstack": {
"threshold": 20_000_000,
"high_volume_price": 0.000015,
"low_volume_price": 0.00000245
},
"alchemy": {
"threshold": 300_000_000,
"high_volume_price": 0.00000040,
"low_volume_price": 0.00000045
},
"quicknode": {
"threshold": 80_000_000,
"high_volume_price": 0.00000062,
"low_volume_price": 0.000000525
}
}

PROVIDERS = [
{
"name": "Chainstack",
"base_url": os.getenv("CHAINSTACK_URL"),
"type": "flat",
"price_per_million": 0.25,
"variable_price": True
},
{
"name": "Alchemy",
"base_url": os.getenv("ALCHEMY_URL"),
"type": "cu",
"cu_tiers": [
{"limit": 300_000_000, "price_per_million": 0.45},
{"limit": float("inf"), "price_per_million": 0.40}
],
"method_cus": ALCHEMY_COMPUTE_UNITS
},
{
"name": "QuickNode",
"base_url": os.getenv("QUICKNODE_URL"),
"type": "credit",
"credit_tiers": [
{"limit": 80_000_000, "price_per_million": 0.62},
{"limit": float("inf"), "price_per_million": 0.525}
],
"method_credits": QUICKNODE_CREDITS
}
]
1 change: 1 addition & 0 deletions metrics/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .store import MetricsStore, get_latest_provider_snapshot, get_all_historical_data
56 changes: 56 additions & 0 deletions metrics/store.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import pandas as pd


class MetricsStore:
def __init__(self):
self.df = pd.DataFrame(
columns=["Provider", "Method", "Latency", "Price", "Eligible", "RequestCount"]
)
self.request_counts = {}

def add_record(self, provider: str, method: str, latency_ms: float, price: float):
key = (provider, method)
self.request_counts[key] = self.request_counts.get(key, 0) + 1

row = {
"Provider": provider,
"Method": method,
"Latency": latency_ms,
"Price": price,
"Eligible": True,
"RequestCount": self.request_counts[key],
}
self.df = pd.concat([self.df, pd.DataFrame([row])], ignore_index=True)

def get_df(self) -> pd.DataFrame:
return self.df.copy()

def get_latest(self, method: str) -> pd.DataFrame:
df_method = self.df[self.df["Method"] == method]
latest_records = []
for provider in df_method["Provider"].unique():
df_provider = df_method[df_method["Provider"] == provider]
latest_records.append(df_provider.iloc[-1])
return pd.DataFrame(latest_records)

def get_all_records(self, method: str = None) -> pd.DataFrame:
if method:
return self.df[self.df["Method"] == method].copy()
return self.df.copy()

def get_request_count(self, provider: str, method: str) -> int:
return self.request_counts.get((provider, method), 0)

def get_all_request_counts(self) -> dict:
return self.request_counts.copy()



def get_latest_provider_snapshot(providers: list, method: str) -> pd.DataFrame:
latest = pd.concat([p.metrics.get_latest(method) for p in providers], ignore_index=True)
return latest


def get_all_historical_data(providers: list, method: str) -> pd.DataFrame:
all_records = pd.concat([p.metrics.get_all_records(method) for p in providers], ignore_index=True)
return all_records if not all_records.empty else pd.DataFrame()
2 changes: 1 addition & 1 deletion providers/base.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import aiohttp
import time
from typing import Dict, Any, List, Optional
from data.metrics import MetricsStore
from metrics import MetricsStore


class RPCProvider:
Expand Down
6 changes: 5 additions & 1 deletion providers/registry.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from typing import Dict, Any, List, Optional
from core.config import (
from config import (
PROVIDERS,
PRICING_CONFIG,
ALCHEMY_COMPUTE_UNITS,
Expand Down Expand Up @@ -28,6 +28,8 @@ def price_per_call(self, method: str = None) -> float:
if provider == self.name
)

total_cu += compute_units

if total_cu > PRICING_CONFIG["alchemy"]["threshold"]:
return PRICING_CONFIG["alchemy"]["high_volume_price"] * compute_units
return PRICING_CONFIG["alchemy"]["low_volume_price"] * compute_units
Expand All @@ -43,6 +45,8 @@ def price_per_call(self, method: str = None) -> float:
if provider == self.name
)

total_credits += credits

if total_credits > PRICING_CONFIG["quicknode"]["threshold"]:
return PRICING_CONFIG["quicknode"]["high_volume_price"] * credits

Expand Down
2 changes: 1 addition & 1 deletion services/metric_collector.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from typing import List, Dict, Any
import pandas as pd
from providers.base import RPCProvider
from data.metrics import get_latest_provider_snapshot
from metrics import get_latest_provider_snapshot

class MetricCollector:
def __init__(self):
Expand Down
2 changes: 1 addition & 1 deletion strategy/scoring_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import pandas as pd
from .normalizer import normalize
from .critic_weights import compute_critic_weights
from data.metrics import get_latest_provider_snapshot, get_all_historical_data
from metrics import get_latest_provider_snapshot, get_all_historical_data

def calculate_dynamic_scores(providers: list, method: str = None):
# Filter out "Best" provider from scoring
Expand Down
17 changes: 10 additions & 7 deletions tests/integration/test_comprehensive.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ class TestResult:
class ComprehensiveTestRunner:
def __init__(self, base_url="http://localhost:6969"):
self.base_url = base_url
self.test_data_dir = "/Users/sambhavjain/Desktop/bliply/bliply/test_data"
self.project_root = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
self.test_data_dir = os.path.join(self.project_root, "test_data")
self.results = []
self.server_process = None

Expand Down Expand Up @@ -261,8 +262,8 @@ def generate_comparison_report(self):

# Save detailed results to JSON and Excel
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
results_file = f"/Users/sambhavjain/Desktop/bliply/test_results_{timestamp}.json"
excel_file = f"/Users/sambhavjain/Desktop/bliply/test_results_{timestamp}.xlsx"
results_file = os.path.join(self.project_root, f"test_results_{timestamp}.json")
excel_file = os.path.join(self.project_root, f"test_results_{timestamp}.xlsx")

# Convert DataFrames to serializable format
provider_stats_dict = {}
Expand Down Expand Up @@ -482,14 +483,16 @@ def main():
print("🧪 Bliply Comprehensive Test Suite")
print("=" * 50)

project_root = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))

# Check if we're in the right directory
if not os.path.exists("/Users/sambhavjain/Desktop/bliply/bliply/main.py"):
print("❌ Error: main.py not found. Please run from the correct directory.")
if not os.path.exists(os.path.join(project_root, "main.py")):
print(f"❌ Error: main.py not found in {project_root}. Please run from the correct directory.")
return

# Check if test_data exists
if not os.path.exists("/Users/sambhavjain/Desktop/bliply/bliply/test_data"):
print("❌ Error: test_data directory not found.")
if not os.path.exists(os.path.join(project_root, "test_data")):
print(f"❌ Error: test_data directory not found in {project_root}.")
return

# Run the comprehensive test
Expand Down