diff --git a/optional-skills/blockchain/base/SKILL.md b/optional-skills/blockchain/base/SKILL.md deleted file mode 100644 index b5c041a9714..00000000000 --- a/optional-skills/blockchain/base/SKILL.md +++ /dev/null @@ -1,232 +0,0 @@ ---- -name: base -description: Query Base (Ethereum L2) blockchain data with USD pricing — wallet balances, token info, transaction details, gas analysis, contract inspection, whale detection, and live network stats. Uses Base RPC + CoinGecko. No API key required. -version: 0.1.0 -author: youssefea -license: MIT -platforms: [linux, macos, windows] -metadata: - hermes: - tags: [Base, Blockchain, Crypto, Web3, RPC, DeFi, EVM, L2, Ethereum] - related_skills: [] ---- - -# Base Blockchain Skill - -Query Base (Ethereum L2) on-chain data enriched with USD pricing via CoinGecko. -8 commands: wallet portfolio, token info, transactions, gas analysis, -contract inspection, whale detection, network stats, and price lookup. - -No API key needed. Uses only Python standard library (urllib, json, argparse). - ---- - -## When to Use - -- User asks for a Base wallet balance, token holdings, or portfolio value -- User wants to inspect a specific transaction by hash -- User wants ERC-20 token metadata, price, supply, or market cap -- User wants to understand Base gas costs and L1 data fees -- User wants to inspect a contract (ERC type detection, proxy resolution) -- User wants to find large ETH transfers (whale detection) -- User wants Base network health, gas price, or ETH price -- User asks "what's the price of USDC/AERO/DEGEN/ETH?" - ---- - -## Prerequisites - -The helper script uses only Python standard library (urllib, json, argparse). -No external packages required. - -Pricing data comes from CoinGecko's free API (no key needed, rate-limited -to ~10-30 requests/minute). For faster lookups, use `--no-prices` flag. - ---- - -## Quick Reference - -RPC endpoint (default): https://mainnet.base.org -Override: export BASE_RPC_URL=https://your-private-rpc.com - -Helper script path: ~/.hermes/skills/blockchain/base/scripts/base_client.py - -``` -python3 base_client.py wallet
[--limit N] [--all] [--no-prices] -python3 base_client.py tx -python3 base_client.py token -python3 base_client.py gas -python3 base_client.py contract
-python3 base_client.py whales [--min-eth N] -python3 base_client.py stats -python3 base_client.py price -``` - ---- - -## Procedure - -### 0. Setup Check - -```bash -python3 --version - -# Optional: set a private RPC for better rate limits -export BASE_RPC_URL="https://mainnet.base.org" - -# Confirm connectivity -python3 ~/.hermes/skills/blockchain/base/scripts/base_client.py stats -``` - -### 1. Wallet Portfolio - -Get ETH balance and ERC-20 token holdings with USD values. -Checks ~15 well-known Base tokens (USDC, WETH, AERO, DEGEN, etc.) -via on-chain `balanceOf` calls. Tokens sorted by value, dust filtered. - -```bash -python3 ~/.hermes/skills/blockchain/base/scripts/base_client.py \ - wallet 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 -``` - -Flags: -- `--limit N` — show top N tokens (default: 20) -- `--all` — show all tokens, no dust filter, no limit -- `--no-prices` — skip CoinGecko price lookups (faster, RPC-only) - -Output includes: ETH balance + USD value, token list with prices sorted -by value, dust count, total portfolio value in USD. - -Note: Only checks known tokens. Unknown ERC-20s are not discovered. -Use the `token` command with a specific contract address for any token. - -### 2. Transaction Details - -Inspect a full transaction by its hash. Shows ETH value transferred, -gas used, fee in ETH/USD, status, and decoded ERC-20/ERC-721 transfers. - -```bash -python3 ~/.hermes/skills/blockchain/base/scripts/base_client.py \ - tx 0xabc123...your_tx_hash_here -``` - -Output: hash, block, from, to, value (ETH + USD), gas price, gas used, -fee, status, contract creation address (if any), token transfers. - -### 3. Token Info - -Get ERC-20 token metadata: name, symbol, decimals, total supply, price, -market cap, and contract code size. - -```bash -python3 ~/.hermes/skills/blockchain/base/scripts/base_client.py \ - token 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 -``` - -Output: name, symbol, decimals, total supply, price, market cap. -Reads name/symbol/decimals directly from the contract via eth_call. - -### 4. Gas Analysis - -Detailed gas analysis with cost estimates for common operations. -Shows current gas price, base fee trends over 10 blocks, block -utilization, and estimated costs for ETH transfers, ERC-20 transfers, -and swaps. - -```bash -python3 ~/.hermes/skills/blockchain/base/scripts/base_client.py gas -``` - -Output: current gas price, base fee, block utilization, 10-block trend, -cost estimates in ETH and USD. - -Note: Base is an L2 — actual transaction costs include an L1 data -posting fee that depends on calldata size and L1 gas prices. The -estimates shown are for L2 execution only. - -### 5. Contract Inspection - -Inspect an address: determine if it's an EOA or contract, detect -ERC-20/ERC-721/ERC-1155 interfaces, resolve EIP-1967 proxy -implementation addresses. - -```bash -python3 ~/.hermes/skills/blockchain/base/scripts/base_client.py \ - contract 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 -``` - -Output: is_contract, code size, ETH balance, detected interfaces -(ERC-20, ERC-721, ERC-1155), ERC-20 metadata, proxy implementation -address. - -### 6. Whale Detector - -Scan the most recent block for large ETH transfers with USD values. - -```bash -python3 ~/.hermes/skills/blockchain/base/scripts/base_client.py \ - whales --min-eth 1.0 -``` - -Note: scans the latest block only — point-in-time snapshot, not historical. -Default threshold is 1.0 ETH (lower than Solana's default since ETH -values are higher). - -### 7. Network Stats - -Live Base network health: latest block, chain ID, gas price, base fee, -block utilization, transaction count, and ETH price. - -```bash -python3 ~/.hermes/skills/blockchain/base/scripts/base_client.py stats -``` - -### 8. Price Lookup - -Quick price check for any token by contract address or known symbol. - -```bash -python3 ~/.hermes/skills/blockchain/base/scripts/base_client.py price ETH -python3 ~/.hermes/skills/blockchain/base/scripts/base_client.py price USDC -python3 ~/.hermes/skills/blockchain/base/scripts/base_client.py price AERO -python3 ~/.hermes/skills/blockchain/base/scripts/base_client.py price DEGEN -python3 ~/.hermes/skills/blockchain/base/scripts/base_client.py price 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 -``` - -Known symbols: ETH, WETH, USDC, cbETH, AERO, DEGEN, TOSHI, BRETT, -WELL, wstETH, rETH, cbBTC. - ---- - -## Pitfalls - -- **CoinGecko rate-limits** — free tier allows ~10-30 requests/minute. - Price lookups use 1 request per token. Use `--no-prices` for speed. -- **Public RPC rate-limits** — Base's public RPC limits requests. - For production use, set BASE_RPC_URL to a private endpoint - (Alchemy, QuickNode, Infura). -- **Wallet shows known tokens only** — unlike Solana, EVM chains have no - built-in "get all tokens" RPC. The wallet command checks ~15 popular - Base tokens via `balanceOf`. Unknown ERC-20s won't appear. Use the - `token` command for any specific contract. -- **Token names read from contract** — if a contract doesn't implement - `name()` or `symbol()`, these fields may be empty. Known tokens have - hardcoded labels as fallback. -- **Gas estimates are L2 only** — Base transaction costs include an L1 - data posting fee (depends on calldata size and L1 gas prices). The gas - command estimates L2 execution cost only. -- **Whale detector scans latest block only** — not historical. Results - vary by the moment you query. Default threshold is 1.0 ETH. -- **Proxy detection** — only EIP-1967 proxies are detected. Other proxy - patterns (EIP-1167 minimal proxy, custom storage slots) are not checked. -- **Retry on 429** — both RPC and CoinGecko calls retry up to 2 times - with exponential backoff on rate-limit errors. - ---- - -## Verification - -```bash -# Should print Base chain ID (8453), latest block, gas price, and ETH price -python3 ~/.hermes/skills/blockchain/base/scripts/base_client.py stats -``` diff --git a/optional-skills/blockchain/base/scripts/base_client.py b/optional-skills/blockchain/base/scripts/base_client.py deleted file mode 100644 index cafffb49f2e..00000000000 --- a/optional-skills/blockchain/base/scripts/base_client.py +++ /dev/null @@ -1,1008 +0,0 @@ -#!/usr/bin/env python3 -""" -Base Blockchain CLI Tool for Hermes Agent ------------------------------------------- -Queries the Base (Ethereum L2) JSON-RPC API and CoinGecko for enriched on-chain data. -Uses only Python standard library — no external packages required. - -Usage: - python3 base_client.py stats - python3 base_client.py wallet
[--limit N] [--all] [--no-prices] - python3 base_client.py tx - python3 base_client.py token - python3 base_client.py gas - python3 base_client.py contract
- python3 base_client.py whales [--min-eth N] - python3 base_client.py price - -Environment: - BASE_RPC_URL Override the default RPC endpoint (default: https://mainnet.base.org) -""" - -import argparse -import json -import os -import sys -import time -import urllib.request -import urllib.error -from typing import Any, Dict, List, Optional, Tuple - -RPC_URL = os.environ.get( - "BASE_RPC_URL", - "https://mainnet.base.org", -) - -WEI_PER_ETH = 10**18 -GWEI = 10**9 - -# ERC-20 function selectors (first 4 bytes of keccak256 hash) -SEL_BALANCE_OF = "70a08231" -SEL_NAME = "06fdde03" -SEL_SYMBOL = "95d89b41" -SEL_DECIMALS = "313ce567" -SEL_TOTAL_SUPPLY = "18160ddd" - -# ERC-165 supportsInterface(bytes4) selector -SEL_SUPPORTS_INTERFACE = "01ffc9a7" - -# Interface IDs for ERC-165 detection -IFACE_ERC721 = "80ac58cd" -IFACE_ERC1155 = "d9b67a26" - -# Transfer(address,address,uint256) event topic -TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" - -# Well-known Base tokens — maps lowercase address -> (symbol, name, decimals). -KNOWN_TOKENS: Dict[str, Tuple[str, str, int]] = { - "0x4200000000000000000000000000000000000006": ("WETH", "Wrapped Ether", 18), - "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913": ("USDC", "USD Coin", 6), - "0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22": ("cbETH", "Coinbase Wrapped Staked ETH", 18), - "0x940181a94a35a4569e4529a3cdfb74e38fd98631": ("AERO", "Aerodrome Finance", 18), - "0x4ed4e862860bed51a9570b96d89af5e1b0efefed": ("DEGEN", "Degen", 18), - "0xac1bd2486aaf3b5c0fc3fd868558b082a531b2b4": ("TOSHI", "Toshi", 18), - "0x532f27101965dd16442e59d40670faf5ebb142e4": ("BRETT", "Brett", 18), - "0xa88594d404727625a9437c3f886c7643872296ae": ("WELL", "Moonwell", 18), - "0xc1cba3fcea344f92d9239c08c0568f6f2f0ee452": ("wstETH", "Wrapped Lido Staked ETH", 18), - "0xb6fe221fe9eef5aba221c348ba20a1bf5e73624c": ("rETH", "Rocket Pool ETH", 18), - "0xcbb7c0000ab88b473b1f5afd9ef808440eed33bf": ("cbBTC", "Coinbase Wrapped BTC", 8), -} - -# Reverse lookup: symbol -> contract address (for the `price` command). -_SYMBOL_TO_ADDRESS = {v[0].upper(): k for k, v in KNOWN_TOKENS.items()} -_SYMBOL_TO_ADDRESS["ETH"] = "ETH" - - -# --------------------------------------------------------------------------- -# HTTP / RPC helpers -# --------------------------------------------------------------------------- - -def _http_get_json(url: str, timeout: int = 10, retries: int = 2) -> Any: - """GET JSON from a URL with retry on 429 rate-limit. Returns parsed JSON or None.""" - for attempt in range(retries + 1): - req = urllib.request.Request( - url, headers={"Accept": "application/json", "User-Agent": "HermesAgent/1.0"}, - ) - try: - with urllib.request.urlopen(req, timeout=timeout) as resp: - return json.load(resp) - except urllib.error.HTTPError as exc: - if exc.code == 429 and attempt < retries: - time.sleep(2.0 * (attempt + 1)) - continue - return None - except Exception: - return None - return None - - -def _rpc_call(method: str, params: list = None, retries: int = 2) -> Any: - """Send a JSON-RPC request with retry on 429 rate-limit.""" - payload = json.dumps({ - "jsonrpc": "2.0", "id": 1, - "method": method, "params": params or [], - }).encode() - - _headers = {"Content-Type": "application/json", "User-Agent": "HermesAgent/1.0"} - - for attempt in range(retries + 1): - req = urllib.request.Request( - RPC_URL, data=payload, headers=_headers, method="POST", - ) - try: - with urllib.request.urlopen(req, timeout=20) as resp: - body = json.load(resp) - if "error" in body: - err = body["error"] - if isinstance(err, dict) and err.get("code") == 429: - if attempt < retries: - time.sleep(1.5 * (attempt + 1)) - continue - sys.exit(f"RPC error: {err}") - return body.get("result") - except urllib.error.HTTPError as exc: - if exc.code == 429 and attempt < retries: - time.sleep(1.5 * (attempt + 1)) - continue - sys.exit(f"RPC HTTP error: {exc}") - except urllib.error.URLError as exc: - sys.exit(f"RPC connection error: {exc}") - return None - - -# Keep backward compat alias. -rpc = _rpc_call - - -_BATCH_LIMIT = 10 # Base public RPC limits to 10 calls per batch - - -def _rpc_batch_chunk(items: list) -> list: - """Send a single batch of JSON-RPC requests (max _BATCH_LIMIT).""" - payload = json.dumps(items).encode() - _headers = {"Content-Type": "application/json", "User-Agent": "HermesAgent/1.0"} - - for attempt in range(3): - req = urllib.request.Request( - RPC_URL, data=payload, headers=_headers, method="POST", - ) - try: - with urllib.request.urlopen(req, timeout=30) as resp: - data = json.load(resp) - # If the RPC returns an error dict instead of a list, treat as failure - if isinstance(data, dict) and "error" in data: - sys.exit(f"RPC batch error: {data['error']}") - return data if isinstance(data, list) else [] - except urllib.error.HTTPError as exc: - if exc.code == 429 and attempt < 2: - time.sleep(1.5 * (attempt + 1)) - continue - sys.exit(f"RPC batch HTTP error: {exc}") - except urllib.error.URLError as exc: - sys.exit(f"RPC batch error: {exc}") - return [] - - -def rpc_batch(calls: list) -> list: - """Send a batch of JSON-RPC requests, auto-chunking to respect limits.""" - items = [ - {"jsonrpc": "2.0", "id": i, "method": c["method"], "params": c.get("params", [])} - for i, c in enumerate(calls) - ] - - if len(items) <= _BATCH_LIMIT: - return _rpc_batch_chunk(items) - - # Split into chunks of _BATCH_LIMIT - all_results = [] - for start in range(0, len(items), _BATCH_LIMIT): - chunk = items[start:start + _BATCH_LIMIT] - all_results.extend(_rpc_batch_chunk(chunk)) - return all_results - - -def wei_to_eth(wei: int) -> float: - return wei / WEI_PER_ETH - - -def wei_to_gwei(wei: int) -> float: - return wei / GWEI - - -def hex_to_int(hex_str: Optional[str]) -> int: - """Convert hex string (0x...) to int. Returns 0 for None/empty.""" - if not hex_str or hex_str == "0x": - return 0 - return int(hex_str, 16) - - -def print_json(obj: Any) -> None: - print(json.dumps(obj, indent=2)) - - -def _short_addr(addr: str) -> str: - """Abbreviate an address for display: first 6 + last 4.""" - if len(addr) <= 14: - return addr - return f"{addr[:6]}...{addr[-4:]}" - - -# --------------------------------------------------------------------------- -# ABI encoding / decoding helpers -# --------------------------------------------------------------------------- - -def _encode_address(addr: str) -> str: - """ABI-encode an address as a 32-byte hex string (no 0x prefix).""" - clean = addr.lower().replace("0x", "") - return clean.zfill(64) - - -def _decode_uint(hex_data: Optional[str]) -> int: - """Decode a hex-encoded uint256 return value.""" - if not hex_data or hex_data == "0x": - return 0 - return int(hex_data.replace("0x", ""), 16) - - -def _decode_string(hex_data: Optional[str]) -> str: - """Decode an ABI-encoded string return value.""" - if not hex_data or hex_data == "0x" or len(hex_data) < 130: - return "" - data = hex_data[2:] if hex_data.startswith("0x") else hex_data - try: - length = int(data[64:128], 16) - if length == 0 or length > 256: - return "" - str_hex = data[128:128 + length * 2] - return bytes.fromhex(str_hex).decode("utf-8").strip("\x00") - except (ValueError, UnicodeDecodeError): - return "" - - -def _eth_call(to: str, selector: str, args: str = "", block: str = "latest") -> Optional[str]: - """Execute eth_call with a function selector. Returns None on revert/error.""" - data = "0x" + selector + args - try: - payload = json.dumps({ - "jsonrpc": "2.0", "id": 1, - "method": "eth_call", "params": [{"to": to, "data": data}, block], - }).encode() - req = urllib.request.Request( - RPC_URL, data=payload, - headers={"Content-Type": "application/json", "User-Agent": "HermesAgent/1.0"}, - method="POST", - ) - with urllib.request.urlopen(req, timeout=20) as resp: - body = json.load(resp) - if "error" in body: - return None - return body.get("result") - except Exception: - return None - - -# --------------------------------------------------------------------------- -# Price & token name helpers (CoinGecko — free, no API key) -# --------------------------------------------------------------------------- - -def fetch_prices(addresses: List[str], max_lookups: int = 20) -> Dict[str, float]: - """Fetch USD prices for Base token addresses via CoinGecko (one per request). - - CoinGecko free tier doesn't support batch Base token lookups, - so we do individual calls — capped at *max_lookups* to stay within - rate limits. Returns {lowercase_address: usd_price}. - """ - prices: Dict[str, float] = {} - for i, addr in enumerate(addresses[:max_lookups]): - url = ( - f"https://api.coingecko.com/api/v3/simple/token_price/base" - f"?contract_addresses={addr}&vs_currencies=usd" - ) - data = _http_get_json(url, timeout=10) - if data and isinstance(data, dict): - for key, info in data.items(): - if isinstance(info, dict) and "usd" in info: - prices[addr.lower()] = info["usd"] - break - # Pause between calls to respect CoinGecko free-tier rate-limits - if i < len(addresses[:max_lookups]) - 1: - time.sleep(1.0) - return prices - - -def fetch_eth_price() -> Optional[float]: - """Fetch current ETH price in USD via CoinGecko.""" - data = _http_get_json( - "https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd" - ) - if data and "ethereum" in data: - return data["ethereum"].get("usd") - return None - - -def resolve_token_name(addr: str) -> Optional[Dict[str, str]]: - """Look up token name and symbol. Checks known tokens first, then on-chain. - - Returns {"name": ..., "symbol": ...} or None. - """ - addr_lower = addr.lower() - if addr_lower in KNOWN_TOKENS: - sym, name, _ = KNOWN_TOKENS[addr_lower] - return {"symbol": sym, "name": name} - # Try reading name() and symbol() from the contract - name_hex = _eth_call(addr, SEL_NAME) - symbol_hex = _eth_call(addr, SEL_SYMBOL) - name = _decode_string(name_hex) if name_hex else "" - symbol = _decode_string(symbol_hex) if symbol_hex else "" - if symbol: - return {"symbol": symbol.upper(), "name": name} - return None - - -def _token_label(addr: str) -> str: - """Return a human-readable label: symbol if known, else abbreviated address.""" - addr_lower = addr.lower() - if addr_lower in KNOWN_TOKENS: - return KNOWN_TOKENS[addr_lower][0] - return _short_addr(addr) - - -# --------------------------------------------------------------------------- -# 1. Network Stats -# --------------------------------------------------------------------------- - -def cmd_stats(_args): - """Base network health: block, gas, chain ID, ETH price.""" - results = rpc_batch([ - {"method": "eth_blockNumber"}, - {"method": "eth_gasPrice"}, - {"method": "eth_chainId"}, - {"method": "eth_getBlockByNumber", "params": ["latest", False]}, - ]) - - by_id = {r["id"]: r.get("result") for r in results} - - block_num = hex_to_int(by_id.get(0)) - gas_price = hex_to_int(by_id.get(1)) - chain_id = hex_to_int(by_id.get(2)) - block = by_id.get(3) or {} - - base_fee = hex_to_int(block.get("baseFeePerGas")) if block.get("baseFeePerGas") else None - timestamp = hex_to_int(block.get("timestamp")) if block.get("timestamp") else None - gas_used = hex_to_int(block.get("gasUsed")) if block.get("gasUsed") else None - gas_limit = hex_to_int(block.get("gasLimit")) if block.get("gasLimit") else None - tx_count = len(block.get("transactions", [])) - - eth_price = fetch_eth_price() - - out = { - "chain": "Base" if chain_id == 8453 else f"Chain {chain_id}", - "chain_id": chain_id, - "latest_block": block_num, - "gas_price_gwei": round(wei_to_gwei(gas_price), 4), - } - if base_fee is not None: - out["base_fee_gwei"] = round(wei_to_gwei(base_fee), 4) - if timestamp: - out["block_timestamp"] = timestamp - if gas_used is not None and gas_limit: - out["block_gas_used"] = gas_used - out["block_gas_limit"] = gas_limit - out["block_utilization_pct"] = round(gas_used / gas_limit * 100, 2) - out["block_tx_count"] = tx_count - if eth_price is not None: - out["eth_price_usd"] = eth_price - print_json(out) - - -# --------------------------------------------------------------------------- -# 2. Wallet Info (ETH + ERC-20 balances with prices) -# --------------------------------------------------------------------------- - -def cmd_wallet(args): - """ETH balance + ERC-20 token holdings with USD values.""" - address = args.address.lower() - show_all = getattr(args, "all", False) - limit = getattr(args, "limit", 20) or 20 - skip_prices = getattr(args, "no_prices", False) - - # Batch: ETH balance + balanceOf for all known tokens - calls = [{"method": "eth_getBalance", "params": [address, "latest"]}] - token_addrs = list(KNOWN_TOKENS.keys()) - for token_addr in token_addrs: - calls.append({ - "method": "eth_call", - "params": [ - {"to": token_addr, "data": "0x" + SEL_BALANCE_OF + _encode_address(address)}, - "latest", - ], - }) - - results = rpc_batch(calls) - by_id = {r["id"]: r.get("result") for r in results} - - eth_balance = wei_to_eth(hex_to_int(by_id.get(0))) - - # Parse token balances - tokens = [] - for i, token_addr in enumerate(token_addrs): - raw = hex_to_int(by_id.get(i + 1)) - if raw == 0: - continue - sym, name, decimals = KNOWN_TOKENS[token_addr] - amount = raw / (10 ** decimals) - tokens.append({ - "address": token_addr, - "symbol": sym, - "name": name, - "amount": amount, - "decimals": decimals, - }) - - # Fetch prices - eth_price = None - prices: Dict[str, float] = {} - if not skip_prices: - eth_price = fetch_eth_price() - if tokens: - mints_to_price = [t["address"] for t in tokens] - prices = fetch_prices(mints_to_price, max_lookups=20) - - # Enrich with USD values, filter dust, sort - enriched = [] - dust_count = 0 - dust_value = 0.0 - for t in tokens: - usd_price = prices.get(t["address"]) - usd_value = round(usd_price * t["amount"], 2) if usd_price else None - - if not show_all and usd_value is not None and usd_value < 0.01: - dust_count += 1 - dust_value += usd_value - continue - - entry = {"token": t["symbol"], "address": t["address"], "amount": t["amount"]} - if usd_price is not None: - entry["price_usd"] = usd_price - entry["value_usd"] = usd_value - enriched.append(entry) - - # Sort: tokens with known USD value first (highest->lowest), then unknowns - enriched.sort( - key=lambda x: (x.get("value_usd") is not None, x.get("value_usd") or 0), - reverse=True, - ) - - # Apply limit unless --all - total_tokens = len(enriched) - if not show_all and len(enriched) > limit: - enriched = enriched[:limit] - hidden_tokens = total_tokens - len(enriched) - - # Compute portfolio total - total_usd = sum(t.get("value_usd", 0) for t in enriched) - eth_value_usd = round(eth_price * eth_balance, 2) if eth_price else None - if eth_value_usd: - total_usd += eth_value_usd - total_usd += dust_value - - output = { - "address": args.address, - "eth_balance": round(eth_balance, 18), - } - if eth_price: - output["eth_price_usd"] = eth_price - output["eth_value_usd"] = eth_value_usd - output["tokens_shown"] = len(enriched) - if hidden_tokens > 0: - output["tokens_hidden"] = hidden_tokens - output["erc20_tokens"] = enriched - if dust_count > 0: - output["dust_filtered"] = {"count": dust_count, "total_value_usd": round(dust_value, 4)} - if total_usd > 0: - output["portfolio_total_usd"] = round(total_usd, 2) - if hidden_tokens > 0 and not show_all: - output["warning"] = ( - "portfolio_total_usd may be partial because hidden tokens are not " - "included when --limit is applied." - ) - output["note"] = f"Checked {len(KNOWN_TOKENS)} known Base tokens. Unknown ERC-20s not shown." - - print_json(output) - - -# --------------------------------------------------------------------------- -# 3. Transaction Details -# --------------------------------------------------------------------------- - -def cmd_tx(args): - """Full transaction details by hash.""" - tx_hash = args.hash - - results = rpc_batch([ - {"method": "eth_getTransactionByHash", "params": [tx_hash]}, - {"method": "eth_getTransactionReceipt", "params": [tx_hash]}, - ]) - - by_id = {r["id"]: r.get("result") for r in results} - tx = by_id.get(0) - receipt = by_id.get(1) - - if tx is None: - sys.exit("Transaction not found.") - - value_wei = hex_to_int(tx.get("value")) - tx_gas_price = hex_to_int(tx.get("gasPrice")) - gas_used = hex_to_int(receipt.get("gasUsed")) if receipt else None - effective_gas_price = ( - hex_to_int(receipt.get("effectiveGasPrice")) if receipt and receipt.get("effectiveGasPrice") - else tx_gas_price - ) - l2_fee_wei = effective_gas_price * gas_used if gas_used is not None else None - l1_fee_wei = hex_to_int(receipt.get("l1Fee")) if receipt and receipt.get("l1Fee") else 0 - fee_wei = (l2_fee_wei + l1_fee_wei) if l2_fee_wei is not None else None - - eth_price = fetch_eth_price() - - out = { - "hash": tx_hash, - "block": hex_to_int(tx.get("blockNumber")), - "from": tx.get("from"), - "to": tx.get("to"), - "value_ETH": round(wei_to_eth(value_wei), 18) if value_wei else 0, - "gas_price_gwei": round(wei_to_gwei(effective_gas_price), 4), - } - if gas_used is not None: - out["gas_used"] = gas_used - if l2_fee_wei is not None: - out["l2_fee_ETH"] = round(wei_to_eth(l2_fee_wei), 12) - if l1_fee_wei: - out["l1_fee_ETH"] = round(wei_to_eth(l1_fee_wei), 12) - if fee_wei is not None: - out["fee_ETH"] = round(wei_to_eth(fee_wei), 12) - if receipt: - out["status"] = "success" if receipt.get("status") == "0x1" else "failed" - out["contract_created"] = receipt.get("contractAddress") - out["log_count"] = len(receipt.get("logs", [])) - - # Decode ERC-20 transfers from logs - transfers = [] - if receipt: - for log in receipt.get("logs", []): - topics = log.get("topics", []) - if len(topics) >= 3 and topics[0] == TRANSFER_TOPIC: - from_addr = "0x" + topics[1][-40:] - to_addr = "0x" + topics[2][-40:] - token_contract = log.get("address", "") - label = _token_label(token_contract) - - entry = { - "token": label, - "contract": token_contract, - "from": from_addr, - "to": to_addr, - } - # ERC-20: 3 topics, amount in data - if len(topics) == 3: - amount_hex = log.get("data", "0x") - if amount_hex and amount_hex != "0x": - raw_amount = hex_to_int(amount_hex) - addr_lower = token_contract.lower() - if addr_lower in KNOWN_TOKENS: - decimals = KNOWN_TOKENS[addr_lower][2] - entry["amount"] = raw_amount / (10 ** decimals) - else: - entry["raw_amount"] = raw_amount - # ERC-721: 4 topics, tokenId in topics[3] - elif len(topics) == 4: - entry["token_id"] = hex_to_int(topics[3]) - entry["type"] = "ERC-721" - - transfers.append(entry) - - if transfers: - out["token_transfers"] = transfers - - if eth_price is not None: - if value_wei: - out["value_USD"] = round(wei_to_eth(value_wei) * eth_price, 2) - if l2_fee_wei is not None: - out["l2_fee_USD"] = round(wei_to_eth(l2_fee_wei) * eth_price, 4) - if l1_fee_wei: - out["l1_fee_USD"] = round(wei_to_eth(l1_fee_wei) * eth_price, 4) - if fee_wei is not None: - out["fee_USD"] = round(wei_to_eth(fee_wei) * eth_price, 4) - - print_json(out) - - -# --------------------------------------------------------------------------- -# 4. Token Info -# --------------------------------------------------------------------------- - -def cmd_token(args): - """ERC-20 token metadata, supply, price, market cap.""" - addr = args.address.lower() - - # Batch: name, symbol, decimals, totalSupply, code check - calls = [ - {"method": "eth_call", "params": [{"to": addr, "data": "0x" + SEL_NAME}, "latest"]}, - {"method": "eth_call", "params": [{"to": addr, "data": "0x" + SEL_SYMBOL}, "latest"]}, - {"method": "eth_call", "params": [{"to": addr, "data": "0x" + SEL_DECIMALS}, "latest"]}, - {"method": "eth_call", "params": [{"to": addr, "data": "0x" + SEL_TOTAL_SUPPLY}, "latest"]}, - {"method": "eth_getCode", "params": [addr, "latest"]}, - ] - results = rpc_batch(calls) - by_id = {r["id"]: r.get("result") for r in results} - - code = by_id.get(4) - if not code or code == "0x": - sys.exit("Address is not a contract.") - - name = _decode_string(by_id.get(0)) - symbol = _decode_string(by_id.get(1)) - decimals_raw = by_id.get(2) - decimals = _decode_uint(decimals_raw) - total_supply_raw = _decode_uint(by_id.get(3)) - - # Fall back to known tokens if on-chain read failed - if not symbol and addr in KNOWN_TOKENS: - symbol = KNOWN_TOKENS[addr][0] - name = KNOWN_TOKENS[addr][1] - decimals = KNOWN_TOKENS[addr][2] - - is_known_token = addr in KNOWN_TOKENS - is_erc20 = bool((symbol or is_known_token) and decimals_raw and decimals_raw != "0x") - if not is_erc20: - sys.exit("Contract does not appear to be an ERC-20 token.") - - total_supply = total_supply_raw / (10 ** decimals) if decimals else total_supply_raw - - # Fetch price - price_data = fetch_prices([addr]) - - out = {"address": args.address} - if name: - out["name"] = name - if symbol: - out["symbol"] = symbol - out["decimals"] = decimals - out["total_supply"] = round(total_supply, min(decimals, 6)) - out["code_size_bytes"] = (len(code) - 2) // 2 - if addr in price_data: - out["price_usd"] = price_data[addr] - out["market_cap_usd"] = round(price_data[addr] * total_supply, 0) - - print_json(out) - - -# --------------------------------------------------------------------------- -# 5. Gas Analysis (Base-specific: L2 execution + L1 data costs) -# --------------------------------------------------------------------------- - -def cmd_gas(_args): - """Detailed gas analysis with L1 data fee context and cost estimates.""" - latest_hex = _rpc_call("eth_blockNumber") - latest = hex_to_int(latest_hex) - - # Get last 10 blocks for trend analysis + current gas price - block_calls = [] - for i in range(10): - block_calls.append({ - "method": "eth_getBlockByNumber", - "params": [hex(latest - i), False], - }) - block_calls.append({"method": "eth_gasPrice"}) - - results = rpc_batch(block_calls) - by_id = {r["id"]: r.get("result") for r in results} - - current_gas_price = hex_to_int(by_id.get(10)) - - base_fees = [] - gas_utilizations = [] - tx_counts = [] - latest_block_info = None - - for i in range(10): - b = by_id.get(i) - if not b: - continue - bf = hex_to_int(b.get("baseFeePerGas", "0x0")) - gu = hex_to_int(b.get("gasUsed", "0x0")) - gl = hex_to_int(b.get("gasLimit", "0x0")) - txc = len(b.get("transactions", [])) - base_fees.append(bf) - if gl > 0: - gas_utilizations.append(gu / gl * 100) - tx_counts.append(txc) - - if i == 0: - latest_block_info = { - "block": hex_to_int(b.get("number")), - "base_fee_gwei": round(wei_to_gwei(bf), 6), - "gas_used": gu, - "gas_limit": gl, - "utilization_pct": round(gu / gl * 100, 2) if gl > 0 else 0, - "tx_count": txc, - } - - avg_base_fee = sum(base_fees) / len(base_fees) if base_fees else 0 - avg_utilization = sum(gas_utilizations) / len(gas_utilizations) if gas_utilizations else 0 - avg_tx_count = sum(tx_counts) / len(tx_counts) if tx_counts else 0 - - # Estimate costs for common operations - eth_price = fetch_eth_price() - - simple_transfer_gas = 21_000 - erc20_transfer_gas = 65_000 - swap_gas = 200_000 - - def _estimate_cost(gas: int) -> Dict[str, Any]: - cost_wei = gas * current_gas_price - cost_eth = wei_to_eth(cost_wei) - entry: Dict[str, Any] = {"gas_units": gas, "cost_ETH": round(cost_eth, 10)} - if eth_price: - entry["cost_USD"] = round(cost_eth * eth_price, 6) - return entry - - out: Dict[str, Any] = { - "current_gas_price_gwei": round(wei_to_gwei(current_gas_price), 6), - "latest_block": latest_block_info, - "trend_10_blocks": { - "avg_base_fee_gwei": round(wei_to_gwei(avg_base_fee), 6), - "avg_utilization_pct": round(avg_utilization, 2), - "avg_tx_count": round(avg_tx_count, 1), - "min_base_fee_gwei": round(wei_to_gwei(min(base_fees)), 6) if base_fees else None, - "max_base_fee_gwei": round(wei_to_gwei(max(base_fees)), 6) if base_fees else None, - }, - "cost_estimates": { - "eth_transfer": _estimate_cost(simple_transfer_gas), - "erc20_transfer": _estimate_cost(erc20_transfer_gas), - "swap": _estimate_cost(swap_gas), - }, - "note": "Base is an L2. Total tx cost = L2 execution fee + L1 data posting fee. " - "L1 data fee depends on calldata size and L1 gas prices (not shown here). " - "Actual costs may be slightly higher than estimates.", - } - if eth_price: - out["eth_price_usd"] = eth_price - print_json(out) - - -# --------------------------------------------------------------------------- -# 6. Contract Inspection -# --------------------------------------------------------------------------- - -def cmd_contract(args): - """Inspect an address: EOA vs contract, ERC type detection, proxy resolution.""" - addr = args.address.lower() - - # Batch: getCode, getBalance, name, symbol, decimals, totalSupply, ERC-721, ERC-1155 - calls = [ - {"method": "eth_getCode", "params": [addr, "latest"]}, - {"method": "eth_getBalance", "params": [addr, "latest"]}, - {"method": "eth_call", "params": [{"to": addr, "data": "0x" + SEL_NAME}, "latest"]}, - {"method": "eth_call", "params": [{"to": addr, "data": "0x" + SEL_SYMBOL}, "latest"]}, - {"method": "eth_call", "params": [{"to": addr, "data": "0x" + SEL_DECIMALS}, "latest"]}, - {"method": "eth_call", "params": [{"to": addr, "data": "0x" + SEL_TOTAL_SUPPLY}, "latest"]}, - {"method": "eth_call", "params": [ - {"to": addr, "data": "0x" + SEL_SUPPORTS_INTERFACE + IFACE_ERC721.zfill(64)}, - "latest", - ]}, - {"method": "eth_call", "params": [ - {"to": addr, "data": "0x" + SEL_SUPPORTS_INTERFACE + IFACE_ERC1155.zfill(64)}, - "latest", - ]}, - ] - results = rpc_batch(calls) - - # Handle per-item errors gracefully - by_id: Dict[int, Any] = {} - for r in results: - if "error" not in r: - by_id[r["id"]] = r.get("result") - else: - by_id[r["id"]] = None - - code = by_id.get(0, "0x") - eth_balance = hex_to_int(by_id.get(1)) - - if not code or code == "0x": - out = { - "address": args.address, - "is_contract": False, - "eth_balance": round(wei_to_eth(eth_balance), 18), - "note": "This is an externally owned account (EOA), not a contract.", - } - print_json(out) - return - - code_size = (len(code) - 2) // 2 - - # Check ERC-20 - name = _decode_string(by_id.get(2)) - symbol = _decode_string(by_id.get(3)) - decimals_raw = by_id.get(4) - supply_raw = by_id.get(5) - is_erc20 = bool(symbol and decimals_raw and decimals_raw != "0x") - - # Check ERC-721 / ERC-1155 via ERC-165 - erc721_result = by_id.get(6) - erc1155_result = by_id.get(7) - is_erc721 = erc721_result is not None and _decode_uint(erc721_result) == 1 - is_erc1155 = erc1155_result is not None and _decode_uint(erc1155_result) == 1 - - # Detect proxy pattern (EIP-1967 implementation slot) - impl_slot = "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc" - impl_result = _rpc_call("eth_getStorageAt", [addr, impl_slot, "latest"]) - is_proxy = False - impl_address = None - if impl_result and impl_result != "0x" + "0" * 64: - impl_address = "0x" + impl_result[-40:] - if impl_address != "0x" + "0" * 40: - is_proxy = True - - out: Dict[str, Any] = { - "address": args.address, - "is_contract": True, - "code_size_bytes": code_size, - "eth_balance": round(wei_to_eth(eth_balance), 18), - } - - interfaces = [] - if is_erc20: - interfaces.append("ERC-20") - if is_erc721: - interfaces.append("ERC-721") - if is_erc1155: - interfaces.append("ERC-1155") - if interfaces: - out["detected_interfaces"] = interfaces - - if is_erc20: - decimals = _decode_uint(decimals_raw) - supply = _decode_uint(supply_raw) - out["erc20"] = { - "name": name, - "symbol": symbol, - "decimals": decimals, - "total_supply": supply / (10 ** decimals) if decimals else supply, - } - - if is_proxy: - out["proxy"] = { - "is_proxy": True, - "implementation": impl_address, - "standard": "EIP-1967", - } - - # Check known tokens - if addr in KNOWN_TOKENS: - sym, tname, _ = KNOWN_TOKENS[addr] - out["known_token"] = {"symbol": sym, "name": tname} - - print_json(out) - - -# --------------------------------------------------------------------------- -# 7. Whale Detector -# --------------------------------------------------------------------------- - -def cmd_whales(args): - """Scan the latest block for large ETH transfers with USD values.""" - min_wei = int(args.min_eth * WEI_PER_ETH) - - block = rpc("eth_getBlockByNumber", ["latest", True]) - if block is None: - sys.exit("Could not retrieve latest block.") - - eth_price = fetch_eth_price() - - whales = [] - for tx in (block.get("transactions") or []): - value = hex_to_int(tx.get("value")) - if value >= min_wei: - entry: Dict[str, Any] = { - "hash": tx.get("hash"), - "from": tx.get("from"), - "to": tx.get("to"), - "value_ETH": round(wei_to_eth(value), 6), - } - if eth_price: - entry["value_USD"] = round(wei_to_eth(value) * eth_price, 2) - whales.append(entry) - - # Sort by value descending - whales.sort(key=lambda x: x["value_ETH"], reverse=True) - - out: Dict[str, Any] = { - "block": hex_to_int(block.get("number")), - "block_time": hex_to_int(block.get("timestamp")), - "min_threshold_ETH": args.min_eth, - "large_transfers": whales, - "note": "Scans latest block only — point-in-time snapshot.", - } - if eth_price: - out["eth_price_usd"] = eth_price - print_json(out) - - -# --------------------------------------------------------------------------- -# 8. Price Lookup -# --------------------------------------------------------------------------- - -def cmd_price(args): - """Quick price lookup for a token by contract address or known symbol.""" - query = args.token - - # Check if it's a known symbol - addr = _SYMBOL_TO_ADDRESS.get(query.upper(), query).lower() - - # Special case: ETH itself - if addr == "eth": - eth_price = fetch_eth_price() - out: Dict[str, Any] = {"query": query, "token": "ETH", "name": "Ethereum"} - if eth_price: - out["price_usd"] = eth_price - else: - out["price_usd"] = None - out["note"] = "Price not available." - print_json(out) - return - - # Resolve name - token_meta = resolve_token_name(addr) - - # Fetch price - prices = fetch_prices([addr]) - - out = {"query": query, "address": addr} - if token_meta: - out["name"] = token_meta["name"] - out["symbol"] = token_meta["symbol"] - if addr in prices: - out["price_usd"] = prices[addr] - else: - out["price_usd"] = None - out["note"] = "Price not available — token may not be listed on CoinGecko." - print_json(out) - - -# --------------------------------------------------------------------------- -# CLI -# --------------------------------------------------------------------------- - -def main(): - parser = argparse.ArgumentParser( - prog="base_client.py", - description="Base blockchain query tool for Hermes Agent", - ) - sub = parser.add_subparsers(dest="command", required=True) - - sub.add_parser("stats", help="Network stats: block, gas, chain ID, ETH price") - - p_wallet = sub.add_parser("wallet", help="ETH balance + ERC-20 tokens with USD values") - p_wallet.add_argument("address") - p_wallet.add_argument("--limit", type=int, default=20, - help="Max tokens to display (default: 20)") - p_wallet.add_argument("--all", action="store_true", - help="Show all tokens (no limit, no dust filter)") - p_wallet.add_argument("--no-prices", action="store_true", - help="Skip price lookups (faster, RPC-only)") - - p_tx = sub.add_parser("tx", help="Transaction details by hash") - p_tx.add_argument("hash") - - p_token = sub.add_parser("token", help="ERC-20 token metadata, price, and market cap") - p_token.add_argument("address") - - sub.add_parser("gas", help="Gas analysis with cost estimates and L1 data fee context") - - p_contract = sub.add_parser("contract", help="Contract inspection: type detection, proxy check") - p_contract.add_argument("address") - - p_whales = sub.add_parser("whales", help="Large ETH transfers in the latest block") - p_whales.add_argument("--min-eth", type=float, default=1.0, - help="Minimum ETH transfer size (default: 1.0)") - - p_price = sub.add_parser("price", help="Quick price lookup by address or symbol") - p_price.add_argument("token", help="Contract address or known symbol (ETH, USDC, AERO, ...)") - - args = parser.parse_args() - - dispatch = { - "stats": cmd_stats, - "wallet": cmd_wallet, - "tx": cmd_tx, - "token": cmd_token, - "gas": cmd_gas, - "contract": cmd_contract, - "whales": cmd_whales, - "price": cmd_price, - } - dispatch[args.command](args) - - -if __name__ == "__main__": - main() diff --git a/optional-skills/blockchain/evm/SKILL.md b/optional-skills/blockchain/evm/SKILL.md index 5990326c1ca..64f90e580d0 100644 --- a/optional-skills/blockchain/evm/SKILL.md +++ b/optional-skills/blockchain/evm/SKILL.md @@ -25,6 +25,11 @@ Optimism, Avalanche (C-Chain), zkSync Era. No API key needed. Zero external dependencies — Python standard library only (urllib, json, argparse, threading). +> **Supersedes the standalone `base` skill.** Base-specific tokens (AERO, DEGEN, +> TOSHI, BRETT, WELL, cbETH, cbBTC, wstETH, rETH) and all Base RPC functionality +> previously living under `optional-skills/blockchain/base/` have been folded +> into this skill. Pass `--chain base` to any command for Base coverage. + --- ## When to Use @@ -188,8 +193,10 @@ Shows gwei price + USD cost for: transfer, ERC-20 transfer, approve, swap, NFT m - `wallet` and `allowance` only check known token list (~30 tokens per chain). Use a block explorer for complete token discovery. - `activity` scans recent blocks only (max 200). For full history, use Etherscan API. - `multichain` runs 8 parallel threads — can trigger rate limits on public RPCs. -- ENS requires internet access to ensideas.com. -- Tx decode requires internet access to 4byte.directory. +- ENS resolution depends on a single public endpoint (ensideas.com / ens.vitalik.ca) with no fallback. If that endpoint is down, `ens` will fail — re-run later or use a block explorer. +- Tx decoding depends on a single public endpoint (4byte.directory) with no fallback. Selectors not in their database show up as `unknown`. +- **L2 gas estimates are L2-execution only.** On rollups like Base, Arbitrum, Optimism, and zkSync, the actual transaction cost also includes an L1 data-posting fee that depends on calldata size and current L1 gas prices. The `gas` command does not estimate that L1 component. For Base specifically, see the network's L1 fee oracle (contract `0x420000000000000000000000000000000000000F`). +- Address / tx-hash inputs are validated for 0x-prefix + correct length + hex, but EIP-55 checksum casing is **not** enforced (RPC endpoints accept any-case hex). --- diff --git a/optional-skills/blockchain/evm/scripts/evm_client.py b/optional-skills/blockchain/evm/scripts/evm_client.py index fc2dd2142c9..31da48fd192 100644 --- a/optional-skills/blockchain/evm/scripts/evm_client.py +++ b/optional-skills/blockchain/evm/scripts/evm_client.py @@ -137,9 +137,21 @@ KNOWN_TOKENS: Dict[str, Dict[str, str]] = { "DOGE": "0xbA2aE424d960c26247Dd6c32edC70B295c744C43", }, "base": { - "USDC": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", - "DAI": "0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb", - "WETH": "0x4200000000000000000000000000000000000006", + # Stables + wrapped + "USDC": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "DAI": "0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb", + "WETH": "0x4200000000000000000000000000000000000006", + # Liquid-staked ETH variants + "cbETH": "0x2Ae3F1Ec7F1F5012CFEab0185bfc7aa3cF0DEc22", + "wstETH": "0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452", + "rETH": "0xB6fe221Fe9EeF5aBa221c348bA20A1Bf5e73624c", + "cbBTC": "0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf", + # Base-native DeFi + meme tokens (carried over from the standalone base/ skill) + "AERO": "0x940181a94A35A4569E4529A3CDfB74e38FD98631", + "DEGEN": "0x4ed4E862860beD51a9570b96d89aF5E1B0Efefed", + "TOSHI": "0xAC1Bd2486aAf3B5C0fc3Fd868558b082a531B2B4", + "BRETT": "0x532f27101965dd16442E59d40670FaF5eBB142E4", + "WELL": "0xA88594D404727625A9437C3f886C7643872296AE", }, "arbitrum": { "USDC": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", @@ -226,9 +238,73 @@ def hex_to_int(h: str) -> int: return 0 return int(h, 16) + +# --------------------------------------------------------------------------- +# Input validation +# --------------------------------------------------------------------------- + +def is_valid_address(s: str) -> bool: + """Return True if `s` looks like a 20-byte hex Ethereum address. + + Does NOT validate EIP-55 checksum — RPC endpoints accept any-case hex. + Just guards against typos / wrong-length input before we burn an RPC call. + """ + if not isinstance(s, str): + return False + if not s.startswith("0x") and not s.startswith("0X"): + return False + if len(s) != 42: + return False + try: + int(s, 16) + except ValueError: + return False + return True + + +def is_valid_txhash(s: str) -> bool: + """Return True if `s` looks like a 32-byte hex transaction hash.""" + if not isinstance(s, str): + return False + if not s.startswith("0x") and not s.startswith("0X"): + return False + if len(s) != 66: + return False + try: + int(s, 16) + except ValueError: + return False + return True + + +def require_address(s: str, *, field: str = "address") -> str: + """Return `s` lowercased if valid, else exit with an error message. + + Centralizing validation here means every subcommand fails fast on bad input + instead of bubbling up an opaque RPC error 30 seconds later. + """ + if not is_valid_address(s): + sys.stderr.write( + f"error: invalid {field} {s!r}: expected 0x-prefixed 40-hex-char address\n" + ) + sys.exit(2) + return s.lower() + + +def require_txhash(s: str, *, field: str = "tx hash") -> str: + """Return `s` lowercased if valid, else exit with an error message.""" + if not is_valid_txhash(s): + sys.stderr.write( + f"error: invalid {field} {s!r}: expected 0x-prefixed 64-hex-char tx hash\n" + ) + sys.exit(2) + return s.lower() + + def wei_to_native(wei: int, decimals: int = 18) -> float: return wei / (10 ** decimals) + def gwei_from_wei(wei: int) -> float: return wei / 1e9 @@ -326,25 +402,36 @@ def rpc_call(chain: str, method: str, params: List[Any], req_id: int = 1) -> Any raise RuntimeError(f"RPC error: {resp['error']}") return resp.get("result") -def rpc_batch(chain: str, calls: List[Tuple[str, List[Any]]]) -> List[Any]: - """Send a batch of JSON-RPC calls; returns list of results in same order.""" +def rpc_batch(chain: str, calls: List[Tuple[str, List[Any]]], batch_limit: int = 10) -> List[Any]: + """Send a batch of JSON-RPC calls; returns list of results in same order. + + Auto-chunks at `batch_limit` (default 10) so we stay under per-RPC limits. + Base's public RPC caps batches at 10 — exceeding that returns a single error + dict instead of a results list, which would mask all our calls. + """ url = get_rpc_url(chain) - payload = [ + + # Build the full payload, preserving order via JSON-RPC `id` + items = [ {"jsonrpc": "2.0", "id": i, "method": m, "params": p} for i, (m, p) in enumerate(calls) ] - resp = _http_post(url, payload) - if isinstance(resp, list): - # Sort by id to preserve order - resp_sorted = sorted(resp, key=lambda x: x.get("id", 0)) - results = [] - for r in resp_sorted: - if "error" in r: - results.append(None) - else: - results.append(r.get("result")) - return results - return [resp.get("result")] + + out: List[Any] = [None] * len(items) + for start in range(0, len(items), batch_limit): + chunk = items[start:start + batch_limit] + resp = _http_post(url, chunk) + if not isinstance(resp, list): + # Single error response (e.g. batch-too-large) — leave this chunk as None + continue + for r in resp: + rid = r.get("id") + if isinstance(rid, int) and 0 <= rid < len(out): + if "error" in r: + out[rid] = None + else: + out[rid] = r.get("result") + return out # --------------------------------------------------------------------------- # ABI encoding helpers (minimal, for ERC-20 calls) @@ -556,7 +643,7 @@ def cmd_stats(args: argparse.Namespace) -> None: def cmd_wallet(args: argparse.Namespace) -> None: - address = args.address + address = require_address(args.address) chain = args.chain limit = args.limit no_prices = args.no_prices @@ -633,7 +720,7 @@ def cmd_wallet(args: argparse.Namespace) -> None: def cmd_tx(args: argparse.Namespace) -> None: - tx_hash = args.hash + tx_hash = require_txhash(args.hash) chain = args.chain cfg = CHAINS[chain] @@ -702,7 +789,7 @@ def cmd_tx(args: argparse.Namespace) -> None: def cmd_token(args: argparse.Namespace) -> None: - contract = args.contract + contract = require_address(args.contract, field="contract address") chain = args.chain # Batch all ERC-20 metadata calls @@ -744,7 +831,7 @@ def cmd_token(args: argparse.Namespace) -> None: def cmd_activity(args: argparse.Namespace) -> None: - address = args.address + address = require_address(args.address) chain = args.chain limit = args.limit cfg = CHAINS[chain] @@ -1000,7 +1087,7 @@ def cmd_multichain(args: argparse.Namespace) -> None: """Scan same wallet across all 8 chains simultaneously.""" import threading - address = args.address + address = require_address(args.address) results: Dict[str, Any] = {} lock = threading.Lock() @@ -1019,9 +1106,10 @@ def cmd_multichain(args: argparse.Namespace) -> None: "tokens": [], "total_usd": native_usd or 0.0, } - # Check known tokens for this chain + # Check known tokens for this chain. + # KNOWN_TOKENS[chain] maps {symbol: contract_address}, not {addr: (sym, name)}. known = KNOWN_TOKENS.get(chain, {}) - for contract, (symbol, _name) in known.items(): + for symbol, contract in known.items(): raw = eth_call_erc20(chain, contract, "balanceOf(address)", address) if not raw or raw == "0x": continue @@ -1067,7 +1155,7 @@ def cmd_multichain(args: argparse.Namespace) -> None: def cmd_allowance(args: argparse.Namespace) -> None: """Check dangerous ERC-20 approvals for a wallet (known spenders).""" - address = args.address + address = require_address(args.address) chain = args.chain # Well-known spender contracts (DEXes, bridges, etc.) @@ -1085,7 +1173,8 @@ def cmd_allowance(args: argparse.Namespace) -> None: known = KNOWN_TOKENS.get(chain, {}) approvals = [] - for contract, (symbol, _name) in known.items(): + # KNOWN_TOKENS[chain] is {symbol: contract_address}, not {addr: (sym, name)}. + for symbol, contract in known.items(): for spender_addr, spender_name in KNOWN_SPENDERS.items(): # allowance(owner, spender) = 0xdd62ed3e owner_pad = address.lower().replace("0x", "").zfill(64) @@ -1127,7 +1216,7 @@ def cmd_allowance(args: argparse.Namespace) -> None: def cmd_decode(args: argparse.Namespace) -> None: """Decode transaction input data using 4byte.directory.""" chain = args.chain - tx_hash = args.hash + tx_hash = require_txhash(args.hash) tx = rpc_call(chain, "eth_getTransactionByHash", [tx_hash]) if not tx: @@ -1212,7 +1301,7 @@ def cmd_ens(args: argparse.Namespace) -> None: def cmd_contract(args: argparse.Namespace) -> None: """Inspect a smart contract: bytecode size, proxy detection, creation info.""" chain = args.chain - address = args.address + address = require_address(args.address) # Get bytecode code_hex = rpc_call(chain, "eth_getCode", [address, "latest"]) diff --git a/website/docs/reference/optional-skills-catalog.md b/website/docs/reference/optional-skills-catalog.md index 1cedabe4ff2..840aebc8488 100644 --- a/website/docs/reference/optional-skills-catalog.md +++ b/website/docs/reference/optional-skills-catalog.md @@ -38,7 +38,7 @@ hermes skills uninstall | Skill | Description | |-------|-------------| -| [**base**](/docs/user-guide/skills/optional/blockchain/blockchain-base) | Query Base (Ethereum L2) blockchain data with USD pricing — wallet balances, token info, transaction details, gas analysis, contract inspection, whale detection, and live network stats. Uses Base RPC + CoinGecko. No API key required. | +| [**evm**](/docs/user-guide/skills/optional/blockchain/blockchain-evm) | Query EVM blockchain data across 8 chains — wallet portfolios, ERC-20 tokens, transactions, gas tracker, whale detection, multi-chain scan, ENS resolution, allowance checker, contract inspection, and tx decoder. Supports Ethereum, BNB Chain, Base, Arbitrum, Polygon, Optimism, Avalanche, zkSync. Uses public RPCs + CoinGecko. No API key required. | | [**solana**](/docs/user-guide/skills/optional/blockchain/blockchain-solana) | Query Solana blockchain data with USD pricing — wallet balances, token portfolios with values, transaction details, NFTs, whale detection, and live network stats. Uses Solana RPC + CoinGecko. No API key required. | ## communication diff --git a/website/docs/user-guide/skills/optional/blockchain/blockchain-base.md b/website/docs/user-guide/skills/optional/blockchain/blockchain-base.md deleted file mode 100644 index a9d9cb8c6c1..00000000000 --- a/website/docs/user-guide/skills/optional/blockchain/blockchain-base.md +++ /dev/null @@ -1,249 +0,0 @@ ---- -title: "Base" -sidebar_label: "Base" -description: "Query Base (Ethereum L2) blockchain data with USD pricing — wallet balances, token info, transaction details, gas analysis, contract inspection, whale detect..." ---- - -{/* This page is auto-generated from the skill's SKILL.md by website/scripts/generate-skill-docs.py. Edit the source SKILL.md, not this page. */} - -# Base - -Query Base (Ethereum L2) blockchain data with USD pricing — wallet balances, token info, transaction details, gas analysis, contract inspection, whale detection, and live network stats. Uses Base RPC + CoinGecko. No API key required. - -## Skill metadata - -| | | -|---|---| -| Source | Optional — install with `hermes skills install official/blockchain/base` | -| Path | `optional-skills/blockchain/base` | -| Version | `0.1.0` | -| Author | youssefea | -| License | MIT | -| Platforms | linux, macos, windows | -| Tags | `Base`, `Blockchain`, `Crypto`, `Web3`, `RPC`, `DeFi`, `EVM`, `L2`, `Ethereum` | - -## Reference: full SKILL.md - -:::info -The following is the complete skill definition that Hermes loads when this skill is triggered. This is what the agent sees as instructions when the skill is active. -::: - -# Base Blockchain Skill - -Query Base (Ethereum L2) on-chain data enriched with USD pricing via CoinGecko. -8 commands: wallet portfolio, token info, transactions, gas analysis, -contract inspection, whale detection, network stats, and price lookup. - -No API key needed. Uses only Python standard library (urllib, json, argparse). - ---- - -## When to Use - -- User asks for a Base wallet balance, token holdings, or portfolio value -- User wants to inspect a specific transaction by hash -- User wants ERC-20 token metadata, price, supply, or market cap -- User wants to understand Base gas costs and L1 data fees -- User wants to inspect a contract (ERC type detection, proxy resolution) -- User wants to find large ETH transfers (whale detection) -- User wants Base network health, gas price, or ETH price -- User asks "what's the price of USDC/AERO/DEGEN/ETH?" - ---- - -## Prerequisites - -The helper script uses only Python standard library (urllib, json, argparse). -No external packages required. - -Pricing data comes from CoinGecko's free API (no key needed, rate-limited -to ~10-30 requests/minute). For faster lookups, use `--no-prices` flag. - ---- - -## Quick Reference - -RPC endpoint (default): https://mainnet.base.org -Override: export BASE_RPC_URL=https://your-private-rpc.com - -Helper script path: ~/.hermes/skills/blockchain/base/scripts/base_client.py - -``` -python3 base_client.py wallet
[--limit N] [--all] [--no-prices] -python3 base_client.py tx -python3 base_client.py token -python3 base_client.py gas -python3 base_client.py contract
-python3 base_client.py whales [--min-eth N] -python3 base_client.py stats -python3 base_client.py price -``` - ---- - -## Procedure - -### 0. Setup Check - -```bash -python3 --version - -# Optional: set a private RPC for better rate limits -export BASE_RPC_URL="https://mainnet.base.org" - -# Confirm connectivity -python3 ~/.hermes/skills/blockchain/base/scripts/base_client.py stats -``` - -### 1. Wallet Portfolio - -Get ETH balance and ERC-20 token holdings with USD values. -Checks ~15 well-known Base tokens (USDC, WETH, AERO, DEGEN, etc.) -via on-chain `balanceOf` calls. Tokens sorted by value, dust filtered. - -```bash -python3 ~/.hermes/skills/blockchain/base/scripts/base_client.py \ - wallet 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 -``` - -Flags: -- `--limit N` — show top N tokens (default: 20) -- `--all` — show all tokens, no dust filter, no limit -- `--no-prices` — skip CoinGecko price lookups (faster, RPC-only) - -Output includes: ETH balance + USD value, token list with prices sorted -by value, dust count, total portfolio value in USD. - -Note: Only checks known tokens. Unknown ERC-20s are not discovered. -Use the `token` command with a specific contract address for any token. - -### 2. Transaction Details - -Inspect a full transaction by its hash. Shows ETH value transferred, -gas used, fee in ETH/USD, status, and decoded ERC-20/ERC-721 transfers. - -```bash -python3 ~/.hermes/skills/blockchain/base/scripts/base_client.py \ - tx 0xabc123...your_tx_hash_here -``` - -Output: hash, block, from, to, value (ETH + USD), gas price, gas used, -fee, status, contract creation address (if any), token transfers. - -### 3. Token Info - -Get ERC-20 token metadata: name, symbol, decimals, total supply, price, -market cap, and contract code size. - -```bash -python3 ~/.hermes/skills/blockchain/base/scripts/base_client.py \ - token 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 -``` - -Output: name, symbol, decimals, total supply, price, market cap. -Reads name/symbol/decimals directly from the contract via eth_call. - -### 4. Gas Analysis - -Detailed gas analysis with cost estimates for common operations. -Shows current gas price, base fee trends over 10 blocks, block -utilization, and estimated costs for ETH transfers, ERC-20 transfers, -and swaps. - -```bash -python3 ~/.hermes/skills/blockchain/base/scripts/base_client.py gas -``` - -Output: current gas price, base fee, block utilization, 10-block trend, -cost estimates in ETH and USD. - -Note: Base is an L2 — actual transaction costs include an L1 data -posting fee that depends on calldata size and L1 gas prices. The -estimates shown are for L2 execution only. - -### 5. Contract Inspection - -Inspect an address: determine if it's an EOA or contract, detect -ERC-20/ERC-721/ERC-1155 interfaces, resolve EIP-1967 proxy -implementation addresses. - -```bash -python3 ~/.hermes/skills/blockchain/base/scripts/base_client.py \ - contract 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 -``` - -Output: is_contract, code size, ETH balance, detected interfaces -(ERC-20, ERC-721, ERC-1155), ERC-20 metadata, proxy implementation -address. - -### 6. Whale Detector - -Scan the most recent block for large ETH transfers with USD values. - -```bash -python3 ~/.hermes/skills/blockchain/base/scripts/base_client.py \ - whales --min-eth 1.0 -``` - -Note: scans the latest block only — point-in-time snapshot, not historical. -Default threshold is 1.0 ETH (lower than Solana's default since ETH -values are higher). - -### 7. Network Stats - -Live Base network health: latest block, chain ID, gas price, base fee, -block utilization, transaction count, and ETH price. - -```bash -python3 ~/.hermes/skills/blockchain/base/scripts/base_client.py stats -``` - -### 8. Price Lookup - -Quick price check for any token by contract address or known symbol. - -```bash -python3 ~/.hermes/skills/blockchain/base/scripts/base_client.py price ETH -python3 ~/.hermes/skills/blockchain/base/scripts/base_client.py price USDC -python3 ~/.hermes/skills/blockchain/base/scripts/base_client.py price AERO -python3 ~/.hermes/skills/blockchain/base/scripts/base_client.py price DEGEN -python3 ~/.hermes/skills/blockchain/base/scripts/base_client.py price 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 -``` - -Known symbols: ETH, WETH, USDC, cbETH, AERO, DEGEN, TOSHI, BRETT, -WELL, wstETH, rETH, cbBTC. - ---- - -## Pitfalls - -- **CoinGecko rate-limits** — free tier allows ~10-30 requests/minute. - Price lookups use 1 request per token. Use `--no-prices` for speed. -- **Public RPC rate-limits** — Base's public RPC limits requests. - For production use, set BASE_RPC_URL to a private endpoint - (Alchemy, QuickNode, Infura). -- **Wallet shows known tokens only** — unlike Solana, EVM chains have no - built-in "get all tokens" RPC. The wallet command checks ~15 popular - Base tokens via `balanceOf`. Unknown ERC-20s won't appear. Use the - `token` command for any specific contract. -- **Token names read from contract** — if a contract doesn't implement - `name()` or `symbol()`, these fields may be empty. Known tokens have - hardcoded labels as fallback. -- **Gas estimates are L2 only** — Base transaction costs include an L1 - data posting fee (depends on calldata size and L1 gas prices). The gas - command estimates L2 execution cost only. -- **Whale detector scans latest block only** — not historical. Results - vary by the moment you query. Default threshold is 1.0 ETH. -- **Proxy detection** — only EIP-1967 proxies are detected. Other proxy - patterns (EIP-1167 minimal proxy, custom storage slots) are not checked. -- **Retry on 429** — both RPC and CoinGecko calls retry up to 2 times - with exponential backoff on rate-limit errors. - ---- - -## Verification - -```bash -# Should print Base chain ID (8453), latest block, gas price, and ETH price -python3 ~/.hermes/skills/blockchain/base/scripts/base_client.py stats -``` diff --git a/website/docs/user-guide/skills/optional/blockchain/blockchain-evm.md b/website/docs/user-guide/skills/optional/blockchain/blockchain-evm.md new file mode 100644 index 00000000000..1b481b3d9b3 --- /dev/null +++ b/website/docs/user-guide/skills/optional/blockchain/blockchain-evm.md @@ -0,0 +1,226 @@ +--- +title: "Evm" +sidebar_label: "Evm" +description: "Query EVM blockchain data across 8 chains — wallet portfolios, ERC-20 tokens, transactions, gas tracker, whale detection, multi-chain scan, ENS resolution, a..." +--- + +{/* This page is auto-generated from the skill's SKILL.md by website/scripts/generate-skill-docs.py. Edit the source SKILL.md, not this page. */} + +# Evm + +Query EVM blockchain data across 8 chains — wallet portfolios, ERC-20 tokens, transactions, gas tracker, whale detection, multi-chain scan, ENS resolution, allowance checker, contract inspection, and tx decoder. Supports Ethereum, BNB Chain, Base, Arbitrum, Polygon, Optimism, Avalanche, zkSync. Uses public RPCs + CoinGecko. No API key required. + +## Skill metadata + +| | | +|---|---| +| Source | Optional — install with `hermes skills install official/blockchain/evm` | +| Path | `optional-skills/blockchain/evm` | +| Version | `1.0.0` | +| Author | Mibayy | +| License | MIT | +| Tags | `EVM`, `Ethereum`, `BNB`, `BSC`, `Base`, `Arbitrum`, `Polygon`, `Optimism`, `Avalanche`, `zkSync`, `Blockchain`, `Crypto`, `Web3`, `DeFi`, `NFT`, `ENS`, `Whale`, `Security` | +| Related skills | [`solana`](/docs/user-guide/skills/optional/blockchain/blockchain-solana) | + +## Reference: full SKILL.md + +:::info +The following is the complete skill definition that Hermes loads when this skill is triggered. This is what the agent sees as instructions when the skill is active. +::: + +# EVM Blockchain Skill + +Query EVM-compatible blockchain data across 8 chains with USD pricing. +14 commands: wallet portfolio, token info, transactions, activity, gas tracker, +network stats, price lookup, multi-chain scan, whale detection, ENS resolution, +allowance checker, contract inspector, and transaction decoder. + +Supports 8 chains: Ethereum, BNB Chain (BSC), Base, Arbitrum One, Polygon, +Optimism, Avalanche (C-Chain), zkSync Era. + +No API key needed. Zero external dependencies — Python standard library only +(urllib, json, argparse, threading). + +> **Supersedes the standalone `base` skill.** Base-specific tokens (AERO, DEGEN, +> TOSHI, BRETT, WELL, cbETH, cbBTC, wstETH, rETH) and all Base RPC functionality +> previously living under `optional-skills/blockchain/base/` have been folded +> into this skill. Pass `--chain base` to any command for Base coverage. + +--- + +## When to Use +- User asks for a wallet balance or portfolio on any EVM chain +- User wants to check the same wallet across ALL chains at once +- User wants to inspect a transaction by hash (or decode what it did) +- User wants ERC-20 token metadata, price, supply, or market cap +- User wants recent transaction history for an address +- User wants current gas prices or to compare fees across chains +- User wants to find large whale transfers in recent blocks +- User asks to resolve an ENS name (vitalik.eth) or reverse-lookup an address +- User wants to check if a contract has dangerous token approvals +- User wants to inspect a smart contract (proxy? ERC-20? ERC-721? bytecode size?) +- User wants to compare gas costs across chains before a transaction + +--- + +## Prerequisites +Python 3.8+ standard library only. No pip installs required. +Pricing: CoinGecko free API (rate-limited, ~10-30 req/min). +ENS: ensideas.com public API. +Tx decoding: 4byte.directory public API. + +Override RPC endpoint: `export EVM_RPC_URL=https://your-rpc.com` + +Helper script path: `~/.hermes/skills/blockchain/evm/scripts/evm_client.py` + +--- + +## Quick Reference + +``` +SCRIPT=~/.hermes/skills/blockchain/evm/scripts/evm_client.py + +# Network & prices +python3 $SCRIPT stats # Ethereum stats +python3 $SCRIPT stats --chain arbitrum # Arbitrum stats +python3 $SCRIPT compare # Gas + prices ALL 8 chains + +# Wallet +python3 $SCRIPT wallet 0xd8dA...96045 # Portfolio (ETH + ERC-20) +python3 $SCRIPT wallet 0xd8dA...96045 --chain bsc +python3 $SCRIPT multichain 0xd8dA...96045 # Same wallet on ALL chains + +# Tokens & prices +python3 $SCRIPT price ETH +python3 $SCRIPT price 0xdAC1...1ec7 # By contract address +python3 $SCRIPT token 0xdAC1...1ec7 # ERC-20 metadata + market cap + +# Transactions +python3 $SCRIPT tx 0x5c50...f060 # Transaction details +python3 $SCRIPT decode 0x5c50...f060 # Decode input data (4byte.directory) +python3 $SCRIPT activity 0xd8dA...96045 # Recent transactions + +# Gas +python3 $SCRIPT gas # Gas prices + cost estimates +python3 $SCRIPT gas --chain optimism + +# Security +python3 $SCRIPT allowance 0xd8dA...96045 # Dangerous ERC-20 approvals +python3 $SCRIPT contract 0xdAC1...1ec7 # Contract inspection (proxy? standards?) + +# ENS +python3 $SCRIPT ens vitalik.eth # Name -> address + profile +python3 $SCRIPT ens 0xd8dA...96045 # Address -> ENS name + +# Whale detection +python3 $SCRIPT whale # Large transfers (last 20 blocks, >$10k) +python3 $SCRIPT whale --blocks 50 --min-usd 100000 --chain arbitrum +``` + +--- + +## Procedure + +### 0. Setup Check +```bash +python3 --version # 3.8+ required +python3 ~/.hermes/skills/blockchain/evm/scripts/evm_client.py stats +``` + +### 1. Wallet Portfolio +Native balance + known ERC-20 tokens, sorted by USD value. +```bash +python3 $SCRIPT wallet 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 +python3 $SCRIPT wallet 0xd8dA... --chain bsc --no-prices # faster +``` + +### 2. Multi-Chain Scan +Scans all 8 chains simultaneously for the same address using threads. +```bash +python3 $SCRIPT multichain 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 +``` +Output: per-chain native balance + token holdings + grand total USD. + +### 3. Compare (Gas + Prices) +All 8 chains queried in parallel. Shows cheapest/most expensive chain. +```bash +python3 $SCRIPT compare +``` + +### 4. Transaction Details & Decode +```bash +python3 $SCRIPT tx 0x5c504ed432cb51138bcf09aa5e8a410dd4a1e204ef84bfed1be16dfba1b22060 +python3 $SCRIPT decode 0x5c504ed... # Shows human-readable function signature +``` +Decode uses 4byte.directory to translate 0xa9059cbb -> transfer(address,uint256). + +### 5. ENS Resolution +```bash +python3 $SCRIPT ens vitalik.eth # -> 0xd8dA... + avatar + social links +python3 $SCRIPT ens 0xd8dA...96045 # -> vitalik.eth +``` + +### 6. Allowance Checker (Security) +Checks ERC-20 approvals granted to known DEX/bridge contracts. +```bash +python3 $SCRIPT allowance 0xYourWallet +``` +Flags UNLIMITED approvals as HIGH risk. + +### 7. Contract Inspector +```bash +python3 $SCRIPT contract 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 # USDC (proxy) +python3 $SCRIPT contract 0xdAC17F958D2ee523a2206206994597C13D831ec7 # USDT (ERC-20) +``` +Detects: proxy (EIP-1967/EIP-1167), ERC-20, ERC-721, ERC-165. Shows bytecode size and implementation address for proxies. + +### 8. Whale Detection +```bash +python3 $SCRIPT whale # ETH, last 20 blocks, >$10k +python3 $SCRIPT whale --blocks 50 --min-usd 50000 --chain bsc +``` + +### 9. Gas Tracker +```bash +python3 $SCRIPT gas +python3 $SCRIPT gas --chain polygon +``` +Shows gwei price + USD cost for: transfer, ERC-20 transfer, approve, swap, NFT mint, NFT transfer. + +--- + +## Supported Chains +| Key | Name | Native | Chain ID | +|-----------|----------------|--------|----------| +| ethereum | Ethereum | ETH | 1 | +| bsc | BNB Chain | BNB | 56 | +| base | Base | ETH | 8453 | +| arbitrum | Arbitrum One | ETH | 42161 | +| polygon | Polygon | POL | 137 | +| optimism | Optimism | ETH | 10 | +| avalanche | Avalanche C | AVAX | 43114 | +| zksync | zkSync Era | ETH | 324 | + +--- + +## Pitfalls +- CoinGecko free tier: ~10-30 req/min. Use `--no-prices` for faster wallet scans. +- Public RPCs may throttle. Set EVM_RPC_URL to a private endpoint for production. +- `wallet` and `allowance` only check known token list (~30 tokens per chain). Use a block explorer for complete token discovery. +- `activity` scans recent blocks only (max 200). For full history, use Etherscan API. +- `multichain` runs 8 parallel threads — can trigger rate limits on public RPCs. +- ENS resolution depends on a single public endpoint (ensideas.com / ens.vitalik.ca) with no fallback. If that endpoint is down, `ens` will fail — re-run later or use a block explorer. +- Tx decoding depends on a single public endpoint (4byte.directory) with no fallback. Selectors not in their database show up as `unknown`. +- **L2 gas estimates are L2-execution only.** On rollups like Base, Arbitrum, Optimism, and zkSync, the actual transaction cost also includes an L1 data-posting fee that depends on calldata size and current L1 gas prices. The `gas` command does not estimate that L1 component. For Base specifically, see the network's L1 fee oracle (contract `0x420000000000000000000000000000000000000F`). +- Address / tx-hash inputs are validated for 0x-prefix + correct length + hex, but EIP-55 checksum casing is **not** enforced (RPC endpoints accept any-case hex). + +--- + +## Verification +```bash +# Should print current block, gas price, ETH price +python3 ~/.hermes/skills/blockchain/evm/scripts/evm_client.py stats + +# Should resolve vitalik.eth to 0xd8dA... +python3 ~/.hermes/skills/blockchain/evm/scripts/evm_client.py ens vitalik.eth +```