From 12ba66f4447bf6feac79830312b0933cea65438f Mon Sep 17 00:00:00 2001 From: Gosuto Inzasheru Date: Thu, 6 Nov 2025 13:40:54 +0100 Subject: [PATCH 1/2] Revert "Merge pull request #144 from BalancerMaxis/etherscan-retry-logic" This reverts commit b587dced373e7dd5d9921cb86c927a5a85972973, reversing changes made to 54ea0aa9785380edfc226eb4b438102eeebb5f7d. --- bal_tools/subgraph.py | 76 +++++++++++++++---------------------------- 1 file changed, 27 insertions(+), 49 deletions(-) diff --git a/bal_tools/subgraph.py b/bal_tools/subgraph.py index 4be60b6..f5d724a 100644 --- a/bal_tools/subgraph.py +++ b/bal_tools/subgraph.py @@ -381,61 +381,39 @@ def get_first_block_after_utc_timestamp( if timestamp > int(datetime.now().strftime("%s")): timestamp = int(datetime.now().strftime("%s")) - 2000 - max_retries = 3 - - for attempt in range(max_retries): + if use_etherscan: try: - if use_etherscan: - try: - if not self.etherscan_client: - self.etherscan_client = Etherscan() - - block_number = self.etherscan_client.get_block_by_timestamp( - chain=self.chain, timestamp=timestamp, closest="after" - ) + if not self.etherscan_client: + self.etherscan_client = Etherscan() - if block_number: - return block_number - - except Exception as e: - if attempt == max_retries - 1: - warnings.warn( - f"Etherscan V2 block fetch failed for chain {self.chain}: {str(e)}. Falling back to subgraph.", - UserWarning, - ) - else: - if "NOTOK" in str(e) or "temporarily unavailable" in str(e): - time.sleep(2**attempt) - continue - warnings.warn( - f"Etherscan V2 block fetch failed for chain {self.chain}: {str(e)}. Falling back to subgraph.", - UserWarning, - ) - - data = self.fetch_graphql_data( - "blocks", - "first_block_after_ts", - { - "timestamp_gt": int(timestamp) - 200, - "timestamp_lt": int(timestamp) + 200, - }, + block_number = self.etherscan_client.get_block_by_timestamp( + chain=self.chain, timestamp=timestamp, closest="after" ) - data["blocks"].sort(key=lambda x: x["timestamp"], reverse=True) - return int(data["blocks"][0]["number"]) - except Exception as e: - if "bad indexers" in str(e) and attempt < max_retries - 1: - time.sleep(2**attempt) - continue + if block_number: + return block_number - if attempt == max_retries - 1: - raise Exception( - f"Failed to fetch block for timestamp {timestamp} on {self.chain}: {str(e)}" - ) + except Exception as e: + warnings.warn( + f"Etherscan V2 block fetch failed for chain {self.chain}: {str(e)}. Falling back to subgraph.", + UserWarning, + ) - raise Exception( - f"Failed to fetch block for timestamp {timestamp} on {self.chain} after {max_retries} attempts" - ) + try: + data = self.fetch_graphql_data( + "blocks", + "first_block_after_ts", + { + "timestamp_gt": int(timestamp) - 200, + "timestamp_lt": int(timestamp) + 200, + }, + ) + data["blocks"].sort(key=lambda x: x["timestamp"], reverse=True) + return int(data["blocks"][0]["number"]) + except Exception as e: + raise Exception( + f"Failed to fetch block for timestamp {timestamp} on {self.chain}: {str(e)}" + ) def filter_outliers_and_average( self, prices: List[Decimal], iqr_multiplier: float = 100_000.0 From 6bb3e71598f19aedb21e28cd11067cf662a53a10 Mon Sep 17 00:00:00 2001 From: Gosuto Inzasheru Date: Thu, 6 Nov 2025 13:42:55 +0100 Subject: [PATCH 2/2] feat: use routescan as fallback for etherscan --- bal_tools/etherscan.py | 36 ++++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/bal_tools/etherscan.py b/bal_tools/etherscan.py index 98dc97a..b7c6a6c 100644 --- a/bal_tools/etherscan.py +++ b/bal_tools/etherscan.py @@ -90,6 +90,27 @@ def _get_block_by_timestamp_plasma( f"Error fetching block for timestamp {timestamp} on plasma: {str(e)}" ) + def _get_block_by_timestamp_routescan( + self, chain_id: int, timestamp: int, closest: str = "before" + ) -> Optional[int]: + routescan_url = ( + f"https://api.routescan.io/v2/network/mainnet/evm/{chain_id}/etherscan/api" + ) + params = { + "module": "block", + "action": "getblocknobytime", + "timestamp": timestamp, + "closest": closest, + } + response = self.session.get(routescan_url, params=params, timeout=30) + response.raise_for_status() + data = response.json() + + if data.get("status") == "1" and data.get("result"): + return int(data["result"]) + + return None + def get_block_by_timestamp( self, chain: str, timestamp: int, closest: str = "before" ) -> Optional[int]: @@ -119,7 +140,14 @@ def get_block_by_timestamp( return None - except Exception as e: - raise Exception( - f"Error fetching block for timestamp {timestamp} on {chain}: {str(e)}" - ) + except Exception as etherscan_error: + # if etherscan fails, try routescan as fallback + try: + return self._get_block_by_timestamp_routescan( + chain_id, timestamp, closest + ) + except Exception: + # if routescan also fails, raise the original etherscan error + raise Exception( + f"Error fetching block for timestamp {timestamp} on {chain}: {str(etherscan_error)}" + )