Some checks failed
Deploy Explorer Live / deploy (push) Failing after 15s
The health check stopped after two non-empty lines and missed the data line that follows event: ping on mission-control streams. Co-authored-by: Cursor <cursoragent@cursor.com>
240 lines
8.2 KiB
Bash
Executable File
240 lines
8.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
set -euo pipefail
|
|
|
|
BASE_URL="${1:-https://explorer.d-bis.org}"
|
|
|
|
python3 - "$BASE_URL" <<'PY'
|
|
import re
|
|
import sys
|
|
import time
|
|
|
|
import requests
|
|
|
|
|
|
base = sys.argv[1].rstrip("/")
|
|
session = requests.Session()
|
|
session.headers.update({"User-Agent": "ExplorerHealthCheck/2.0"})
|
|
|
|
failed = False
|
|
|
|
html_checks = [
|
|
"/",
|
|
"/home",
|
|
"/blocks",
|
|
"/transactions",
|
|
"/addresses",
|
|
"/bridge",
|
|
"/routes",
|
|
"/weth",
|
|
"/tokens",
|
|
"/pools",
|
|
"/watchlist",
|
|
"/more",
|
|
"/analytics",
|
|
"/operator",
|
|
"/system",
|
|
"/liquidity",
|
|
"/wallet",
|
|
"/snap/",
|
|
"/docs.html",
|
|
"/privacy.html",
|
|
"/terms.html",
|
|
"/acknowledgments.html",
|
|
]
|
|
|
|
json_checks = [
|
|
"/api/v2/stats",
|
|
"/api/config/token-list",
|
|
"/api/config/networks",
|
|
"/api/config/capabilities",
|
|
"/config/CHAIN138_RPC_CAPABILITIES.json",
|
|
"/config/topology-graph.json",
|
|
"/config/mission-control-verify.example.json",
|
|
"/explorer-api/v1/features",
|
|
"/explorer-api/v1/ai/context?q=cUSDT",
|
|
"/explorer-api/v1/track1/bridge/status",
|
|
"/explorer-api/v1/mission-control/liquidity/token/0x93E66202A11B1772E55407B32B44e5Cd8eda7f22/pools",
|
|
"/token-aggregation/api/v1/routes/tree?chainId=138&tokenIn=0x93E66202A11B1772E55407B32B44e5Cd8eda7f22&tokenOut=0x004b63A7B5b0E06f6bB6adb4a5F9f590BF3182D1&amountIn=1000000",
|
|
"/token-aggregation/api/v1/routes/matrix",
|
|
"/token-aggregation/api/v1/routes/ingestion?fromChainId=138&routeType=swap",
|
|
"/token-aggregation/api/v1/routes/partner-payloads?partner=0x&amount=1000000&includeUnsupported=true",
|
|
]
|
|
|
|
asset_checks = [
|
|
"/token-icons/cUSDC.png",
|
|
"/token-icons/cUSDT.png",
|
|
"/token-icons/cXAUC.png",
|
|
"/token-icons/cXAUT.png",
|
|
]
|
|
|
|
|
|
def mark_failure(message):
|
|
global failed
|
|
failed = True
|
|
print(message)
|
|
|
|
|
|
print("== Public routes ==")
|
|
for path in html_checks:
|
|
url = base + path
|
|
try:
|
|
resp = session.get(url, timeout=20, allow_redirects=True)
|
|
ctype = resp.headers.get("content-type", "")
|
|
print(f"{resp.status_code:>3} {path} [{ctype[:60]}]")
|
|
if resp.status_code >= 400:
|
|
failed = True
|
|
except Exception as exc:
|
|
mark_failure(f"ERR {path} [{exc}]")
|
|
|
|
print("\n== JSON and API surfaces ==")
|
|
for path in json_checks:
|
|
url = base + path
|
|
try:
|
|
resp = session.get(url, timeout=20, allow_redirects=True)
|
|
ctype = resp.headers.get("content-type", "")
|
|
print(f"{resp.status_code:>3} {path} [{ctype[:60]}]")
|
|
if resp.status_code >= 400:
|
|
failed = True
|
|
continue
|
|
if "json" not in ctype:
|
|
failed = True
|
|
print(f" expected JSON content-type, got: {ctype}")
|
|
continue
|
|
payload = resp.json()
|
|
if path == "/api/config/capabilities":
|
|
if payload.get("chainId") != 138:
|
|
mark_failure(" capabilities JSON does not report chainId 138")
|
|
wallet_support = payload.get("walletSupport", {})
|
|
if wallet_support.get("walletWatchAsset") is not True:
|
|
mark_failure(" capabilities JSON does not advertise walletWatchAsset")
|
|
elif path == "/api/config/token-list":
|
|
tokens = payload.get("tokens", [])
|
|
if not tokens:
|
|
mark_failure(" token list is empty")
|
|
elif path == "/api/config/networks":
|
|
chains = payload.get("chains", [])
|
|
if not chains:
|
|
mark_failure(" networks payload does not include any chains")
|
|
elif path == "/explorer-api/v1/features":
|
|
if "features" not in payload:
|
|
mark_failure(" features payload is missing the features key")
|
|
elif path == "/explorer-api/v1/track1/bridge/status":
|
|
data = payload.get("data", {})
|
|
relays = data.get("ccip_relays", {})
|
|
expected_relays = {
|
|
"avax",
|
|
"avax_cw",
|
|
"avax_to_138",
|
|
"bsc",
|
|
"mainnet_cw",
|
|
"mainnet_weth",
|
|
}
|
|
missing = sorted(expected_relays - set(relays))
|
|
if missing:
|
|
mark_failure(f" bridge status is missing relay keys: {', '.join(missing)}")
|
|
if data.get("status") not in {"operational", "paused", "degraded"}:
|
|
mark_failure(" bridge status payload does not include a recognized overall status")
|
|
except Exception as exc:
|
|
mark_failure(f"ERR {path} [{exc}]")
|
|
|
|
print("\n== Static assets ==")
|
|
for path in asset_checks:
|
|
url = base + path
|
|
try:
|
|
resp = session.get(url, timeout=20, allow_redirects=True)
|
|
ctype = resp.headers.get("content-type", "")
|
|
print(f"{resp.status_code:>3} {path} [{ctype[:60]}]")
|
|
if resp.status_code >= 400:
|
|
failed = True
|
|
except Exception as exc:
|
|
mark_failure(f"ERR {path} [{exc}]")
|
|
|
|
print("\n== Mission-control SSE ==")
|
|
stream_url = base + "/explorer-api/v1/mission-control/stream"
|
|
try:
|
|
with session.get(stream_url, timeout=(20, 20), stream=True) as resp:
|
|
ctype = resp.headers.get("content-type", "")
|
|
print(f"{resp.status_code:>3} /explorer-api/v1/mission-control/stream [{ctype[:60]}]")
|
|
if resp.status_code >= 400:
|
|
failed = True
|
|
else:
|
|
saw_event = False
|
|
saw_data = False
|
|
deadline = time.time() + 25
|
|
for raw in resp.iter_lines(decode_unicode=True):
|
|
if raw:
|
|
if raw.startswith("event:"):
|
|
saw_event = True
|
|
if raw.startswith("data:"):
|
|
saw_data = True
|
|
if saw_event and saw_data:
|
|
break
|
|
if time.time() > deadline:
|
|
break
|
|
if not saw_event:
|
|
mark_failure(" mission-control stream did not emit an event line")
|
|
if not saw_data:
|
|
mark_failure(" mission-control stream did not emit a data line")
|
|
except Exception as exc:
|
|
mark_failure(f"ERR /explorer-api/v1/mission-control/stream [{exc}]")
|
|
|
|
print("\n== Internal href targets from homepage ==")
|
|
try:
|
|
home = session.get(base + "/", timeout=20).text
|
|
hrefs = sorted(set(re.findall(r'href=\"([^\"]+)\"', home)))
|
|
for href in hrefs:
|
|
if href.startswith("/") and not href.startswith("//"):
|
|
resp = session.get(base + href, timeout=20, allow_redirects=True)
|
|
print(f"{resp.status_code:>3} {href}")
|
|
if resp.status_code >= 400:
|
|
failed = True
|
|
except Exception as exc:
|
|
mark_failure(f"ERR homepage href sweep failed: {exc}")
|
|
|
|
print("\n== External explorer roots referenced by bridge surfaces ==")
|
|
external_roots = [
|
|
"https://etherscan.io/",
|
|
"https://bscscan.com/",
|
|
"https://polygonscan.com/",
|
|
"https://subnets.avax.network/c-chain",
|
|
"https://basescan.org/",
|
|
"https://arbiscan.io/",
|
|
"https://optimistic.etherscan.io/",
|
|
]
|
|
for url in external_roots:
|
|
try:
|
|
resp = session.get(url, timeout=20, allow_redirects=True)
|
|
print(f"{resp.status_code:>3} {url}")
|
|
except Exception as exc:
|
|
print(f"ERR {url} [{exc}]")
|
|
|
|
print("\n== Native ETH USD (token-aggregation WETH proxy) ==")
|
|
weth = "0xc02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
|
|
try:
|
|
cg = session.get(
|
|
"https://api.coingecko.com/api/v3/simple/price",
|
|
params={"ids": "ethereum", "vs_currencies": "usd"},
|
|
timeout=15,
|
|
).json()
|
|
cg_usd = float(cg["ethereum"]["usd"])
|
|
ta = session.get(
|
|
f"{base}/token-aggregation/api/v1/tokens/{weth}",
|
|
params={"chainId": 138},
|
|
timeout=20,
|
|
).json()
|
|
ta_usd = float(ta["token"]["market"]["priceUsd"])
|
|
delta_pct = abs(ta_usd - cg_usd) / cg_usd * 100.0
|
|
layer = ta.get("token", {}).get("pricing", {}).get("sourceLayer", "")
|
|
print(f" coingecko=${cg_usd:.2f} explorer=${ta_usd:.2f} delta={delta_pct:.2f}% layer={layer}")
|
|
if abs(ta_usd - 2490.0) < 0.01:
|
|
mark_failure(" ETH price still at stale repo snapshot $2490")
|
|
if delta_pct > 5.0:
|
|
mark_failure(f" ETH price drift {delta_pct:.2f}% exceeds 5% vs CoinGecko")
|
|
except Exception as exc:
|
|
mark_failure(f" native ETH price check failed: {exc}")
|
|
|
|
if failed:
|
|
sys.exit(1)
|
|
PY
|