Last active
January 19, 2026 14:33
-
-
Save prettyirrelevant/bf496ae490767cb1c91a6adae3d01071 to your computer and use it in GitHub Desktop.
Test Etherscan eth_call URL limits for token balance queries
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| Test max token addresses for Etherscan eth_call before HTTP 414. | |
| Usage: python test_etherscan_url_limit.py --api-key YOUR_KEY | |
| Or set ETHERSCAN_API_KEY env variable. | |
| """ | |
| import argparse | |
| import os | |
| import sys | |
| import time | |
| import requests | |
| from eth_abi import encode | |
| ETHERSCAN_API_URL = 'https://api.etherscan.io/v2/api' | |
| BALANCE_SCANNER_ADDRESS = '0x54eCF3f6f61F63fdFE7c27Ee8A86e54899600C92' | |
| TOKENS_BALANCE_SELECTOR = '0xe5da1b68' # tokensBalance(address,address[]) | |
| TEST_WALLET = '0x0000000000000000000000000000000000000001' | |
| FAKE_TOKEN = '0x0000000000000000000000000000000000000002' | |
| def encode_tokens_balance_call(wallet: str, num_tokens: int) -> str: | |
| wallet_addr = bytes.fromhex(wallet[2:]) | |
| token_addrs = [bytes.fromhex(FAKE_TOKEN[2:])] * num_tokens | |
| encoded_args = encode(['address', 'address[]'], [wallet_addr, token_addrs]) | |
| return TOKENS_BALANCE_SELECTOR + encoded_args.hex() | |
| def test_etherscan_call(api_key: str, num_tokens: int, chain_id: int = 1) -> tuple[int, int, str]: | |
| call_data = encode_tokens_balance_call(TEST_WALLET, num_tokens) | |
| params = { | |
| 'module': 'proxy', | |
| 'action': 'eth_call', | |
| 'to': BALANCE_SCANNER_ADDRESS, | |
| 'data': call_data, | |
| 'tag': 'latest', | |
| 'apikey': api_key, | |
| 'chainid': str(chain_id), | |
| } | |
| req = requests.Request('GET', ETHERSCAN_API_URL, params=params) | |
| prepared = req.prepare() | |
| url_length = len(prepared.url) if prepared.url else 0 | |
| try: | |
| response = requests.get(ETHERSCAN_API_URL, params=params, timeout=30) | |
| return response.status_code, url_length, response.text | |
| except requests.RequestException as e: | |
| return -1, url_length, str(e) | |
| def find_max_tokens(api_key: str, start: int = 10, end: int = 150, chain_id: int = 1) -> None: | |
| print(f'Testing Etherscan eth_call URL limits (chain_id={chain_id})...\n') | |
| print(f'{"Tokens":<10} {"Status":<20} {"URL Length":<12} {"Data Length":<12}') | |
| print('-' * 60) | |
| last_success = 0 | |
| first_failure = end + 1 | |
| test_values = [10, 25, 50, 75, 100, 110, 120, 130, 140, 150] | |
| for num_tokens in test_values: | |
| if num_tokens > end: | |
| break | |
| call_data = encode_tokens_balance_call(TEST_WALLET, num_tokens) | |
| data_length = len(call_data) | |
| status, url_length, text = test_etherscan_call(api_key, num_tokens, chain_id) | |
| if status == 200: | |
| if 'not supported' in text.lower() or 'upgrade' in text.lower(): | |
| status_str = '⚠ needs paid key' | |
| elif 'execution reverted' in text.lower() or 'result' in text.lower(): | |
| # execution reverted means the request got through (contract just can't find fake tokens) | |
| status_str = '✓' | |
| last_success = max(last_success, num_tokens) | |
| else: | |
| status_str = '✓' | |
| last_success = max(last_success, num_tokens) | |
| elif status == 414: | |
| status_str = '✗ 414 URI too long' | |
| first_failure = min(first_failure, num_tokens) | |
| elif status == 404: | |
| status_str = '✗ 404' | |
| first_failure = min(first_failure, num_tokens) | |
| elif status == 400: | |
| status_str = '✗ 400' | |
| first_failure = min(first_failure, num_tokens) | |
| elif status == 429: | |
| status_str = '⚠ 429 rate limit' | |
| else: | |
| status_str = f'? {status}' | |
| print(f'{num_tokens:<10} {status_str:<20} {url_length:<12} {data_length:<12}') | |
| time.sleep(0.2) | |
| if last_success > 0 and last_success < first_failure - 1: | |
| print(f'\nBinary search between {last_success} and {first_failure}...\n') | |
| low, high = last_success, first_failure | |
| while low < high - 1: | |
| mid = (low + high) // 2 | |
| call_data = encode_tokens_balance_call(TEST_WALLET, mid) | |
| data_length = len(call_data) | |
| status, url_length, text = test_etherscan_call(api_key, mid, chain_id) | |
| is_success = status == 200 and 'not supported' not in text.lower() and 'upgrade' not in text.lower() | |
| if is_success: | |
| status_str = f'{status} ✓' | |
| low = mid | |
| else: | |
| status_str = f'{status} ✗' | |
| high = mid | |
| print(f'{mid:<10} {status_str:<20} {url_length:<12} {data_length:<12}') | |
| time.sleep(0.2) | |
| last_success = low | |
| print('\n' + '=' * 60) | |
| if last_success > 0: | |
| print(f'Maximum safe number of tokens: {last_success}') | |
| print(f'Recommended value (with margin): {max(1, last_success - 10)}') | |
| call_data = encode_tokens_balance_call(TEST_WALLET, last_success) | |
| _, url_length, _ = test_etherscan_call(api_key, last_success, chain_id) | |
| print(f'\nAt {last_success} tokens:') | |
| print(f' - Total URL length: {url_length} chars') | |
| print(f' - Data param length: {len(call_data)} chars') | |
| else: | |
| print('Could not determine max tokens (API may require paid key)') | |
| def main() -> int: | |
| parser = argparse.ArgumentParser(description='Test Etherscan eth_call URL limits') | |
| parser.add_argument('--api-key', help='Etherscan API key') | |
| parser.add_argument('--chain-id', type=int, default=1, help='Chain ID (default: 1)') | |
| parser.add_argument('--start', type=int, default=10, help='Start number of tokens') | |
| parser.add_argument('--end', type=int, default=150, help='End number of tokens') | |
| args = parser.parse_args() | |
| api_key = args.api_key or os.environ.get('ETHERSCAN_API_KEY') | |
| if not api_key: | |
| print('Error: No API key. Use --api-key or set ETHERSCAN_API_KEY env variable.') | |
| return 1 | |
| find_max_tokens(api_key, args.start, args.end, args.chain_id) | |
| return 0 | |
| if __name__ == '__main__': | |
| sys.exit(main()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment