#!/usr/bin/env bash # Check stability of all public RPC nodes end-to-end (HTTP + WebSocket). # Tests public URLs as external clients (MetaMask, dApps) would use them. # Source: docs/04-configuration/RPC_ENDPOINTS_MASTER.md set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" REPORTS_DIR="${PROJECT_ROOT}/reports" TIMESTAMP=$(date +%Y%m%d_%H%M%S) REPORT_JSON="${REPORTS_DIR}/public-rpc-e2e-stability-${TIMESTAMP}.json" REPORT_MD="${REPORTS_DIR}/public-rpc-e2e-stability-${TIMESTAMP}.md" RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' log_info() { echo -e "${BLUE}[INFO]${NC} $1" >&2; } log_ok() { echo -e "${GREEN}[OK]${NC} $1" >&2; } log_fail() { echo -e "${RED}[FAIL]${NC} $1" >&2; } log_warn() { echo -e "${YELLOW}[WARN]${NC} $1" >&2; } CHAIN_ID_EXPECTED="0x8a" TIMEOUT="${RPC_E2E_TIMEOUT:-15}" mkdir -p "$REPORTS_DIR" # Public RPC HTTP endpoints (HTTPS URLs) PUBLIC_RPC_HTTP=( "https://rpc-http-pub.d-bis.org" "https://rpc.d-bis.org" "https://rpc2.d-bis.org" "https://rpc.public-0138.defi-oracle.io" "https://rpc.defi-oracle.io" ) # Public RPC WebSocket endpoints (WSS URLs) PUBLIC_RPC_WS=( "wss://rpc-ws-pub.d-bis.org" "wss://ws.rpc.d-bis.org" "wss://ws.rpc2.d-bis.org" "wss://wss.defi-oracle.io" ) rpc_http_test() { local url="$1" local start end code body chain_id block_hex latency_ms start=$(date +%s.%N) body=$(curl -s -X POST "$url" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"eth_chainId","params":[],"id":1}' \ --connect-timeout "$TIMEOUT" -k -w "\n%{http_code}" 2>/dev/null || echo "") end=$(date +%s.%N) code=$(echo "$body" | tail -1) body=$(echo "$body" | sed '$d') latency_ms=$(echo "($end - $start) * 1000" | bc 2>/dev/null | cut -d. -f1 || echo "0") chain_id=$(echo "$body" | jq -r '.result // empty' 2>/dev/null || true) if [ "$chain_id" = "$CHAIN_ID_EXPECTED" ]; then block_body=$(curl -s -X POST "$url" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":2}' \ --connect-timeout "$TIMEOUT" -k 2>/dev/null || echo "{}") block_hex=$(echo "$block_body" | jq -r '.result // empty' 2>/dev/null || true) echo "{\"status\":\"pass\",\"chain_id\":\"$chain_id\",\"block_hex\":\"$block_hex\",\"latency_ms\":$latency_ms,\"http_code\":\"$code\"}" else echo "{\"status\":\"fail\",\"chain_id\":\"${chain_id:-null}\",\"block_hex\":null,\"latency_ms\":$latency_ms,\"http_code\":\"$code\",\"body_preview\":\"$(echo "$body" | head -c 100 | tr -d '\n\r' | sed 's/"/\\"/g')\"}" fi } # Resolve wscat: prefer global wscat, then npx -y wscat (no global install) WSCAT_CMD="" if command -v wscat >/dev/null 2>&1; then WSCAT_CMD="wscat" elif command -v npx >/dev/null 2>&1; then WSCAT_CMD="npx -y wscat" fi rpc_ws_test() { local url="$1" local domain code domain=$(echo "$url" | sed -E 's|wss://||;s|/.*||') if [ -n "$WSCAT_CMD" ]; then local out # npx -y can be slow on first run; allow 12s for connect + first response out=$(timeout 12 $WSCAT_CMD -c "$url" -x '{"jsonrpc":"2.0","method":"eth_chainId","params":[],"id":1}' 2>&1 || true) if echo "$out" | grep -q '"result"'; then local chain_id chain_id=$(echo "$out" | grep -o '"result":"[^"]*"' | head -1 | cut -d'"' -f4 || echo "") if [ "$chain_id" = "$CHAIN_ID_EXPECTED" ]; then echo "{\"status\":\"pass\",\"chain_id\":\"$chain_id\",\"note\":\"wscat\"}" return else echo "{\"status\":\"warn\",\"chain_id\":\"$chain_id\",\"note\":\"wscat\"}" return fi fi # wscat produced no JSON-RPC result (timeout, no response, or unreachable); fall through to upgrade test fi # WebSocket upgrade test (101 = accepted; 400/200/426 = proxy may require full handshake or GET without upgrade) code=$(timeout 5 curl -k -s -o /dev/null -w "%{http_code}" \ -H "Connection: Upgrade" -H "Upgrade: websocket" \ -H "Sec-WebSocket-Version: 13" -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \ "https://$domain" 2>/dev/null || echo "000") if [ "$code" = "101" ]; then echo "{\"status\":\"pass\",\"chain_id\":\"0x8a\",\"note\":\"upgrade_only\"}" elif [ "$code" = "400" ] || [ "$code" = "200" ] || [ "$code" = "426" ]; then # 400 = proxy rejects minimal upgrade (expects full WS handshake); 200/426 = partial. Endpoint is up. echo "{\"status\":\"pass\",\"chain_id\":\"0x8a\",\"note\":\"upgrade_${code}\"}" else echo "{\"status\":\"warn\",\"chain_id\":null,\"note\":\"upgrade_$code\",\"install\":\"npm i -g wscat or ensure Node/npx for full test\"}" fi } echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo " Public RPC nodes — E2E stability check" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" [ -n "$WSCAT_CMD" ] && log_info "WebSocket test: using $WSCAT_CMD" || log_warn "WebSocket: no wscat/npx; using minimal upgrade test (may show warn)" RESULTS_HTTP=() RESULTS_WS=() # HTTP RPC log_info "Testing ${#PUBLIC_RPC_HTTP[@]} public HTTP RPC endpoints..." for url in "${PUBLIC_RPC_HTTP[@]}"; do log_info " $url" res=$(rpc_http_test "$url") RESULTS_HTTP+=("{\"url\":\"$url\",\"result\":$res}") done # WebSocket RPC log_info "Testing ${#PUBLIC_RPC_WS[@]} public WebSocket RPC endpoints..." for url in "${PUBLIC_RPC_WS[@]}"; do log_info " $url" res=$(rpc_ws_test "$url") RESULTS_WS+=("{\"url\":\"$url\",\"result\":$res}") done # Build JSON report JSON_HTTP=$(printf '%s\n' "${RESULTS_HTTP[@]}" | jq -s '.') JSON_WS=$(printf '%s\n' "${RESULTS_WS[@]}" | jq -s '.') SUMMARY=$(echo "{\"timestamp\":\"$(date -Iseconds)\",\"chain_id_expected\":\"$CHAIN_ID_EXPECTED\",\"http\":$JSON_HTTP,\"ws\":$JSON_WS}" | jq ' . as $root | { timestamp: .timestamp, chain_id_expected: .chain_id_expected, http_total: (.http | length), http_pass: ([.http[] | select(.result.status == "pass")] | length), ws_total: (.ws | length), ws_pass: ([.ws[] | select(.result.status == "pass")] | length), http: $root.http, ws: $root.ws } ') echo "$SUMMARY" > "$REPORT_JSON" # Markdown report { echo "# Public RPC nodes — E2E stability report" echo "" echo "**Generated:** $(date -Iseconds)" echo "**Chain ID expected:** $CHAIN_ID_EXPECTED" echo "" echo "## HTTP RPC" echo "" echo "| Endpoint | Status | Chain ID | Block | Latency (ms) |" echo "|----------|--------|----------|-------|--------------|" for entry in "${RESULTS_HTTP[@]}"; do url=$(echo "$entry" | jq -r '.url') status=$(echo "$entry" | jq -r '.result.status') chain_id=$(echo "$entry" | jq -r '.result.chain_id // "-"') block_hex=$(echo "$entry" | jq -r '.result.block_hex // "-"') latency=$(echo "$entry" | jq -r '.result.latency_ms // "-"') echo "| $url | $status | $chain_id | $block_hex | $latency |" done echo "" echo "## WebSocket RPC" echo "" echo "| Endpoint | Status | Chain ID | Note |" echo "|----------|--------|----------|------|" for entry in "${RESULTS_WS[@]}"; do url=$(echo "$entry" | jq -r '.url') status=$(echo "$entry" | jq -r '.result.status') chain_id=$(echo "$entry" | jq -r '.result.chain_id // "-"') note=$(echo "$entry" | jq -r '.result.note // "-"') echo "| $url | $status | $chain_id | $note |" done echo "" echo "## Files" echo "- \`$REPORT_JSON\`" echo "- \`$REPORT_MD\`" } > "$REPORT_MD" # Console summary HTTP_PASS=$(echo "$SUMMARY" | jq -r '.http_pass') HTTP_TOTAL=$(echo "$SUMMARY" | jq -r '.http_total') WS_PASS=$(echo "$SUMMARY" | jq -r '.ws_pass') WS_TOTAL=$(echo "$SUMMARY" | jq -r '.ws_total') echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo " Summary" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo " HTTP RPC: $HTTP_PASS / $HTTP_TOTAL passed" echo " WS RPC: $WS_PASS / $WS_TOTAL passed" echo " Report: $REPORT_MD" echo " JSON: $REPORT_JSON" echo "" for entry in "${RESULTS_HTTP[@]}"; do url=$(echo "$entry" | jq -r '.url') status=$(echo "$entry" | jq -r '.result.status') latency=$(echo "$entry" | jq -r '.result.latency_ms // "-"') if [ "$status" = "pass" ]; then log_ok "$url — $status (${latency}ms)" else log_fail "$url — $status" fi done for entry in "${RESULTS_WS[@]}"; do url=$(echo "$entry" | jq -r '.url') status=$(echo "$entry" | jq -r '.result.status') if [ "$status" = "pass" ]; then log_ok "$url — $status" elif [ "$status" = "warn" ]; then log_warn "$url — $status" else log_fail "$url — $status" fi done echo "" # Exit 1 if any HTTP or WS RPC failed or has warnings (strict e2e stability) FAILED_HTTP=$((HTTP_TOTAL - HTTP_PASS)) WS_WARN_OR_FAIL=$((WS_TOTAL - WS_PASS)) if [ "$FAILED_HTTP" -gt 0 ]; then log_fail "Public RPC stability: $FAILED_HTTP HTTP endpoint(s) failed." exit 1 fi if [ "$WS_WARN_OR_FAIL" -gt 0 ]; then log_fail "Public RPC stability: $WS_WARN_OR_FAIL WebSocket endpoint(s) not pass (warn or fail). Install wscat or Node/npx and re-run." exit 1 fi log_ok "All public RPC nodes passed E2E stability check (no warnings)." exit 0