From c3a1e44ba1eb28a481acae7d57b6303ff73521f0 Mon Sep 17 00:00:00 2001 From: Anton <43762166+smypmsa@users.noreply.github.com> Date: Wed, 12 Mar 2025 07:07:53 +0100 Subject: [PATCH 1/3] chore: adjust RPC request frequency in vercel.json --- vercel.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/vercel.json b/vercel.json index 2fa1a4f..d7d6c77 100644 --- a/vercel.json +++ b/vercel.json @@ -28,19 +28,19 @@ "crons": [ { "path": "/api/read/ethereum", - "schedule": "*/2 * * * *" + "schedule": "* * * * *" }, { "path": "/api/read/base", - "schedule": "*/2 * * * *" + "schedule": "* * * * *" }, { "path": "/api/read/solana", - "schedule": "*/2 * * * *" + "schedule": "* * * * *" }, { "path": "/api/read/ton", - "schedule": "*/2 * * * *" + "schedule": "* * * * *" }, { "path": "/api/write/solana", From e5a5f1ae388f47453a09b471208048abfa3bba3a Mon Sep 17 00:00:00 2001 From: Anton <43762166+smypmsa@users.noreply.github.com> Date: Wed, 12 Mar 2025 07:12:10 +0100 Subject: [PATCH 2/3] chore: updated .gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index c427b6e..386a36b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ __pycache__ +.ruff_cache .vercel *.vercel @@ -11,4 +12,4 @@ venv api/test endpoints*.json -endpoints \ No newline at end of file +endpoints From 32e73933afdf909013955edb576a2b9fc4833404 Mon Sep 17 00:00:00 2001 From: smypmsa Date: Mon, 7 Apr 2025 06:52:46 +0000 Subject: [PATCH 3/3] feat: add Arbitrum and BSC chains --- api/read/arbitrum.py | 29 ++++++++ api/read/bnbsc.py | 29 ++++++++ api/support/update_state.py | 9 ++- common/metrics_handler.py | 2 +- common/state/blockchain_fetcher.py | 15 +++-- config/defaults.py | 2 + metrics/arbitrum.py | 102 +++++++++++++++++++++++++++++ metrics/bnbsc.py | 102 +++++++++++++++++++++++++++++ tests/test_api_read.py | 2 +- vercel.json | 18 ++++- 10 files changed, 302 insertions(+), 8 deletions(-) create mode 100644 api/read/arbitrum.py create mode 100644 api/read/bnbsc.py create mode 100644 metrics/arbitrum.py create mode 100644 metrics/bnbsc.py diff --git a/api/read/arbitrum.py b/api/read/arbitrum.py new file mode 100644 index 0000000..378e40b --- /dev/null +++ b/api/read/arbitrum.py @@ -0,0 +1,29 @@ +from common.metrics_handler import BaseVercelHandler, MetricsHandler +from config.defaults import MetricsServiceConfig +from metrics.arbitrum import ( + HTTPAccBalanceLatencyMetric, + HTTPBlockNumberLatencyMetric, + HTTPDebugTraceBlockByNumberLatencyMetric, + HTTPDebugTraceTxLatencyMetric, + HTTPEthCallLatencyMetric, + HTTPTxReceiptLatencyMetric, +) +from metrics.ethereum import ( + WSBlockLatencyMetric, +) + +metric_name = f"{MetricsServiceConfig.METRIC_PREFIX}response_latency_seconds" + +METRICS = [ + (WSBlockLatencyMetric, metric_name), + (HTTPBlockNumberLatencyMetric, metric_name), + (HTTPEthCallLatencyMetric, metric_name), + (HTTPAccBalanceLatencyMetric, metric_name), + (HTTPDebugTraceBlockByNumberLatencyMetric, metric_name), + (HTTPDebugTraceTxLatencyMetric, metric_name), + (HTTPTxReceiptLatencyMetric, metric_name), +] + + +class handler(BaseVercelHandler): + metrics_handler = MetricsHandler("Arbitrum", METRICS) diff --git a/api/read/bnbsc.py b/api/read/bnbsc.py new file mode 100644 index 0000000..7826659 --- /dev/null +++ b/api/read/bnbsc.py @@ -0,0 +1,29 @@ +from common.metrics_handler import BaseVercelHandler, MetricsHandler +from config.defaults import MetricsServiceConfig +from metrics.bnbsc import ( + HTTPAccBalanceLatencyMetric, + HTTPBlockNumberLatencyMetric, + HTTPDebugTraceBlockByNumberLatencyMetric, + HTTPDebugTraceTxLatencyMetric, + HTTPEthCallLatencyMetric, + HTTPTxReceiptLatencyMetric, +) +from metrics.ethereum import ( + WSBlockLatencyMetric, +) + +metric_name = f"{MetricsServiceConfig.METRIC_PREFIX}response_latency_seconds" + +METRICS = [ + (WSBlockLatencyMetric, metric_name), + (HTTPBlockNumberLatencyMetric, metric_name), + (HTTPEthCallLatencyMetric, metric_name), + (HTTPAccBalanceLatencyMetric, metric_name), + (HTTPDebugTraceBlockByNumberLatencyMetric, metric_name), + (HTTPDebugTraceTxLatencyMetric, metric_name), + (HTTPTxReceiptLatencyMetric, metric_name), +] + + +class handler(BaseVercelHandler): + metrics_handler = MetricsHandler("BNB Smart Chain", METRICS) diff --git a/api/support/update_state.py b/api/support/update_state.py index e5f2a7d..240b875 100644 --- a/api/support/update_state.py +++ b/api/support/update_state.py @@ -12,7 +12,14 @@ from common.state.blockchain_fetcher import BlockchainData, BlockchainDataFetcher from common.state.blockchain_state import BlockchainState -SUPPORTED_BLOCKCHAINS: list[str] = ["ethereum", "solana", "ton", "base"] +SUPPORTED_BLOCKCHAINS: list[str] = [ + "ethereum", + "solana", + "ton", + "base", + "arbitrum", + "bnb smart chain", +] ALLOWED_PROVIDERS: set[str] = { "Chainstack" } # To reduce number of RPC calls, use only one provider here diff --git a/common/metrics_handler.py b/common/metrics_handler.py index 696d81e..1c8378e 100644 --- a/common/metrics_handler.py +++ b/common/metrics_handler.py @@ -128,7 +128,7 @@ async def handle(self) -> tuple[str, str]: if metrics_text: await self.push_to_grafana(metrics_text) else: - logging.warning("Noting to push to Grafana.") + logging.warning("Nothing to push to Grafana.") return "done", metrics_text diff --git a/common/state/blockchain_fetcher.py b/common/state/blockchain_fetcher.py index 9c1b01b..b72f458 100644 --- a/common/state/blockchain_fetcher.py +++ b/common/state/blockchain_fetcher.py @@ -118,7 +118,7 @@ async def _fetch_evm_data(self, blockchain: str) -> BlockchainData: latest_number = int(latest_block["number"], 16) offset_range = MetricsServiceConfig.BLOCK_OFFSET_RANGES.get( - blockchain.lower(), (20, 100) + blockchain.lower(), (7200, 14400) ) offset = random.randint(offset_range[0], offset_range[1]) old_number = max(0, latest_number - offset) @@ -162,7 +162,7 @@ async def _fetch_solana_data(self) -> BlockchainData: tx_sig = latest_block.get("signatures", [""])[0] offset_range = MetricsServiceConfig.BLOCK_OFFSET_RANGES.get( - "solana", (100, 1000) + "solana", (432000, 648000) ) offset = random.randint(offset_range[0], offset_range[1]) target_slot = max(0, actual_latest_slot - offset) @@ -189,7 +189,9 @@ async def _fetch_ton_data(self) -> BlockchainData: if not isinstance(last_block, dict): return BlockchainData.empty() - offset_range = MetricsServiceConfig.BLOCK_OFFSET_RANGES.get("ton", (10, 50)) + offset_range = MetricsServiceConfig.BLOCK_OFFSET_RANGES.get( + "ton", (1555200, 1572480) + ) offset = random.randint(offset_range[0], offset_range[1]) old_seqno = max(0, last_block["seqno"] - offset) @@ -229,7 +231,12 @@ async def _fetch_ton_data(self) -> BlockchainData: async def fetch_latest_data(self, blockchain: str) -> BlockchainData: """Fetches latest block and transaction data for specified blockchain.""" try: - if blockchain.lower() in ("ethereum", "base"): + if blockchain.lower() in ( + "ethereum", + "base", + "arbitrum", + "bnb smart chain", + ): return await self._fetch_evm_data(blockchain) elif blockchain.lower() == "solana": return await self._fetch_solana_data() diff --git a/config/defaults.py b/config/defaults.py index d0b9df7..a0b541e 100644 --- a/config/defaults.py +++ b/config/defaults.py @@ -30,6 +30,8 @@ class MetricsServiceConfig: "base": (7200, 14400), "solana": (432000, 648000), "ton": (1555200, 1572480), + "arbitrum": (7200, 14400), + "bnb smart chain": (7200, 14400), } diff --git a/metrics/arbitrum.py b/metrics/arbitrum.py new file mode 100644 index 0000000..b608fbb --- /dev/null +++ b/metrics/arbitrum.py @@ -0,0 +1,102 @@ +"""Base EVM metrics implementation for HTTP endpoints.""" + +from common.metric_types import HttpCallLatencyMetricBase + + +class HTTPEthCallLatencyMetric(HttpCallLatencyMetricBase): + """Collects response time for eth_call simulation.""" + + @property + def method(self) -> str: + return "eth_call" + + @staticmethod + def get_params_from_state(state_data: dict) -> list: + """Get eth_call parameters for Aave Pool Addresses Provider query.""" + return [ + { + "to": "0xa97684ead0e402dC232d5A977953DF7ECBaB3CDb", + "data": "0x026b1d5f0000000000000000000000000000000000000000000000000000000000000000", + }, + "latest", + ] + + +class HTTPTxReceiptLatencyMetric(HttpCallLatencyMetricBase): + """Collects latency for transaction receipt retrieval.""" + + @property + def method(self) -> str: + return "eth_getTransactionReceipt" + + @staticmethod + def validate_state(state_data: dict) -> bool: + """Validate blockchain state contains transaction hash.""" + return bool(state_data and state_data.get("tx")) + + @staticmethod + def get_params_from_state(state_data: dict) -> list: + """Get parameters using transaction hash from state.""" + return [state_data["tx"]] + + +class HTTPAccBalanceLatencyMetric(HttpCallLatencyMetricBase): + """Collects latency for account balance queries.""" + + @property + def method(self) -> str: + return "eth_getBalance" + + @staticmethod + def validate_state(state_data: dict) -> bool: + """Validates that required block number (hex) exists in state data.""" + return bool(state_data and state_data.get("old_block")) + + @staticmethod + def get_params_from_state(state_data: dict) -> list: + """Get parameters with fixed monitoring address.""" + return ["0x794a61358D6845594F94dc1DB02A252b5b4814aD", state_data["old_block"]] + + +class HTTPDebugTraceTxLatencyMetric(HttpCallLatencyMetricBase): + """Collects latency for transaction tracing.""" + + @property + def method(self) -> str: + return "debug_traceTransaction" + + @staticmethod + def validate_state(state_data: dict) -> bool: + """Validate blockchain state contains transaction hash.""" + return bool(state_data and state_data.get("tx")) + + @staticmethod + def get_params_from_state(state_data: dict) -> list: + """Get parameters using transaction hash from state.""" + return [state_data["tx"], {"tracer": "callTracer"}] + + +class HTTPDebugTraceBlockByNumberLatencyMetric(HttpCallLatencyMetricBase): + """Collects call latency for the `debug_traceBlockByNumber` method.""" + + @property + def method(self) -> str: + return "debug_traceBlockByNumber" + + @staticmethod + def get_params_from_state(state_data: dict) -> list: + """Get fixed parameters for latest block tracing.""" + return ["latest", {"tracer": "callTracer"}] + + +class HTTPBlockNumberLatencyMetric(HttpCallLatencyMetricBase): + """Collects call latency for the `eth_blockNumber` method.""" + + @property + def method(self) -> str: + return "eth_blockNumber" + + @staticmethod + def get_params_from_state(state_data: dict) -> list: + """Get empty parameter list for block number query.""" + return [] diff --git a/metrics/bnbsc.py b/metrics/bnbsc.py new file mode 100644 index 0000000..b496138 --- /dev/null +++ b/metrics/bnbsc.py @@ -0,0 +1,102 @@ +"""Base EVM metrics implementation for HTTP endpoints.""" + +from common.metric_types import HttpCallLatencyMetricBase + + +class HTTPEthCallLatencyMetric(HttpCallLatencyMetricBase): + """Collects response time for eth_call simulation.""" + + @property + def method(self) -> str: + return "eth_call" + + @staticmethod + def get_params_from_state(state_data: dict) -> list: + """Get eth_call parameters for Aave Pool Addresses Provider query.""" + return [ + { + "to": "0xff75B6da14FfbbfD355Daf7a2731456b3562Ba6D", + "data": "0x026b1d5f0000000000000000000000000000000000000000000000000000000000000000", + }, + "latest", + ] + + +class HTTPTxReceiptLatencyMetric(HttpCallLatencyMetricBase): + """Collects latency for transaction receipt retrieval.""" + + @property + def method(self) -> str: + return "eth_getTransactionReceipt" + + @staticmethod + def validate_state(state_data: dict) -> bool: + """Validate blockchain state contains transaction hash.""" + return bool(state_data and state_data.get("tx")) + + @staticmethod + def get_params_from_state(state_data: dict) -> list: + """Get parameters using transaction hash from state.""" + return [state_data["tx"]] + + +class HTTPAccBalanceLatencyMetric(HttpCallLatencyMetricBase): + """Collects latency for account balance queries.""" + + @property + def method(self) -> str: + return "eth_getBalance" + + @staticmethod + def validate_state(state_data: dict) -> bool: + """Validates that required block number (hex) exists in state data.""" + return bool(state_data and state_data.get("old_block")) + + @staticmethod + def get_params_from_state(state_data: dict) -> list: + """Get parameters with fixed monitoring address.""" + return ["0x6807dc923806fE8Fd134338EABCA509979a7e0cB", state_data["old_block"]] + + +class HTTPDebugTraceTxLatencyMetric(HttpCallLatencyMetricBase): + """Collects latency for transaction tracing.""" + + @property + def method(self) -> str: + return "debug_traceTransaction" + + @staticmethod + def validate_state(state_data: dict) -> bool: + """Validate blockchain state contains transaction hash.""" + return bool(state_data and state_data.get("tx")) + + @staticmethod + def get_params_from_state(state_data: dict) -> list: + """Get parameters using transaction hash from state.""" + return [state_data["tx"], {"tracer": "callTracer"}] + + +class HTTPDebugTraceBlockByNumberLatencyMetric(HttpCallLatencyMetricBase): + """Collects call latency for the `debug_traceBlockByNumber` method.""" + + @property + def method(self) -> str: + return "debug_traceBlockByNumber" + + @staticmethod + def get_params_from_state(state_data: dict) -> list: + """Get fixed parameters for latest block tracing.""" + return ["latest", {"tracer": "callTracer"}] + + +class HTTPBlockNumberLatencyMetric(HttpCallLatencyMetricBase): + """Collects call latency for the `eth_blockNumber` method.""" + + @property + def method(self) -> str: + return "eth_blockNumber" + + @staticmethod + def get_params_from_state(state_data: dict) -> list: + """Get empty parameter list for block number query.""" + return [] diff --git a/tests/test_api_read.py b/tests/test_api_read.py index 18014ad..a374061 100644 --- a/tests/test_api_read.py +++ b/tests/test_api_read.py @@ -29,7 +29,7 @@ def main() -> None: setup_environment() # Import handler after environment setup - from api.read.ton import handler + from api.read.bnbsc import handler server = HTTPServer(("localhost", 8000), handler) print("Server started at http://localhost:8000") diff --git a/vercel.json b/vercel.json index d7d6c77..16ea967 100644 --- a/vercel.json +++ b/vercel.json @@ -16,6 +16,14 @@ "memory": 200, "maxDuration": 59 }, + "api/read/arbitrum.py": { + "memory": 400, + "maxDuration": 59 + }, + "api/read/bnbsc.py": { + "memory": 400, + "maxDuration": 59 + }, "api/write/solana.py": { "memory": 200, "maxDuration": 70 @@ -42,6 +50,14 @@ "path": "/api/read/ton", "schedule": "* * * * *" }, + { + "path": "/api/read/arbitrum", + "schedule": "* * * * *" + }, + { + "path": "/api/read/bnbsc", + "schedule": "* * * * *" + }, { "path": "/api/write/solana", "schedule": "*/15 * * * *" @@ -51,4 +67,4 @@ "schedule": "*/30 * * * *" } ] -} +} \ No newline at end of file