#!/usr/bin/env python3
"""
Retry pass for the 18 addresses where all public RPCs returned 403.
Uses slow serial requests with backoff, and probes for non-standard ownership
selectors (Ownable2Step pendingOwner, AccessControl DEFAULT_ADMIN_ROLE holder).
"""
import json, time, hashlib
from pathlib import Path
from urllib.request import Request, urlopen
from urllib.error import HTTPError

BASELINE = Path(__file__).parent
DEP = json.load(open(BASELINE / "deployments.json"))
FR  = json.load(open(BASELINE / "onchain_fetch_report.json"))

# Different public RPC set — try nodes with less aggressive rate limiting
RPCS = {
    "base": [
        "https://base-rpc.publicnode.com",
        "https://base.drpc.org",
        "https://mainnet.base.org",
    ],
    "arbitrum": [
        "https://arbitrum-one-rpc.publicnode.com",
        "https://arbitrum.drpc.org",
        "https://arb1.arbitrum.io/rpc",
    ],
}

# Additional selectors to probe
SELECTORS = {
    "owner()":          "0x8da5cb5b",
    "getOwner()":       "0x893d20e8",
    "admin()":          "0xf851a440",
    "pendingOwner()":   "0xe30c3978",
    "governance()":     "0x5aa6e675",
    "treasury()":       "0x61d027b3",
    "paused()":         "0x5c975abb",
    # AccessControl role checks require role hash; DEFAULT_ADMIN_ROLE = 0x00
    "hasRole_DEFAULT_ADMIN(0xfa15...)":
        "0x91d14854"
        + ("00" * 32)  # role = bytes32(0)
        + "000000000000000000000000fa15ee28939b222b0448261a22156070f0a7813c",
    "hasRole_DEFAULT_ADMIN(0xa054...)":
        "0x91d14854"
        + ("00" * 32)
        + "000000000000000000000000a054f831b562e729f8d268291ebde1b2edcfb84f",
}

def call(rpc_url, method, params, timeout=30):
    body = json.dumps({"jsonrpc":"2.0","id":1,"method":method,"params":params}).encode()
    req = Request(rpc_url, data=body,
                  headers={"content-type":"application/json",
                           "user-agent":"Mozilla/5.0 vaultfire-baseline/1.0"})
    with urlopen(req, timeout=timeout) as r:
        return json.loads(r.read())

def rpc_fallback(chain, method, params):
    for i, url in enumerate(RPCS[chain]):
        try:
            r = call(url, method, params)
            if "result" in r:
                return r["result"], url, None
            return None, url, r.get("error")
        except HTTPError as e:
            if e.code in (403, 429):
                time.sleep(2.0 * (i + 1))
                continue
            return None, url, str(e)
        except Exception as e:
            return None, url, str(e)
    return None, None, "all RPCs failed"

# Find failures
failures = []
for r in FR["results"]:
    if not r["has_code"] and r["chain"] in RPCS:
        failures.append(r)
print(f"Retrying {len(failures)} 403-blocked addresses serially with backoff...\n")

recovered = []
for i, r in enumerate(failures, 1):
    addr, chain = r["address"], r["chain"]
    time.sleep(1.5)  # gentle
    code, url, err = rpc_fallback(chain, "eth_getCode", [addr, "latest"])
    if code and code != "0x":
        raw = bytes.fromhex(code[2:])
        sha = hashlib.sha256(raw).hexdigest()
        bal, _, _ = rpc_fallback(chain, "eth_getBalance", [addr, "latest"])
        bal_int = int(bal, 16) if bal else 0
        # try selectors
        found = {}
        for name, sel in SELECTORS.items():
            time.sleep(0.4)
            call_res, _, _ = rpc_fallback(chain, "eth_call", [{"to":addr,"data":sel},"latest"])
            if call_res and call_res != "0x" and len(call_res) >= 66:
                found[name] = call_res
        recovered.append({
            "chain": chain, "name": r["name"], "address": addr,
            "bytes": len(raw), "sha256": sha, "balance": bal_int,
            "selector_results": found,
        })
        print(f"[{i:2d}/{len(failures)}] OK  {chain:10s} {r['name']:36s} {len(raw):5d}B  bal={bal_int/1e18:.4f}")
        for sel_name, val in found.items():
            addr_out = "0x" + val[-40:] if len(val) >= 66 else val[:20]
            bool_val = "true" if int(val, 16) == 1 else "false" if int(val, 16) == 0 else None
            print(f"             {sel_name:32s} -> {val[:34]}...  ({addr_out} / bool={bool_val})")
    else:
        print(f"[{i:2d}/{len(failures)}] FAIL {chain:10s} {r['name']:36s}  err={err}")

# Update deployments.json
by_key = {(r["chain"], r["address"]): r for r in recovered}
for c in DEP["contracts"]:
    key = (c["chain"], c["address"])
    if key in by_key:
        r = by_key[key]
        c["onchain"] = c.get("onchain") or {}
        c["onchain"].update({
            "bytecodeBytes": r["bytes"],
            "bytecodeSha256": r["sha256"],
            "hasCode": True,
            "nativeBalanceWei": r["balance"],
            "selectorResults": r["selector_results"],
        })
        c["reconciled"]["bytecodeHashed"] = True
        c["reconciled"]["balanceRead"] = True

(BASELINE / "deployments.json").write_text(json.dumps(DEP, indent=2))
(BASELINE / "onchain_retry_report.json").write_text(json.dumps({
    "recovered": len(recovered), "attempted": len(failures), "results": recovered
}, indent=2))
print(f"\nRecovered {len(recovered)}/{len(failures)}. Updated deployments.json.")
