#!/usr/bin/env python3
"""scan_ident.py — lean $MUSEBOOK ticker-collision identity census (eip155:4663).

Identity fields only: bytecode (+EIP-1167 impl + sha256), name/symbol/decimals/
totalSupply, first-mint block + deployer (tx.from of first mint tx). No full
log enumeration — stops at the first mint, so ~8 RPC calls per contract.
"""
import hashlib, json, sys, time, urllib.request

# getLogs: publicnode gates archive calls behind a token, drpc caps free
# ranges at 10k blocks — only robinhood/ordofi serve the full index.
LOG_RPCS = ["https://rpc.mainnet.chain.robinhood.com", "https://rpc.ordofi.network"]
RPCS = LOG_RPCS + ["https://robinhood-rpc.publicnode.com", "https://robinhood.drpc.org"]
H = {"Content-Type": "application/json", "User-Agent": "ARION-scan/1.0"}
TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
CHUNK = 1_000_000
CANONICAL = "0x91A2DAe9699f0B82540B5886b0d8759C22820bA3"

CONTRACTS = [
    CANONICAL,
    "0x786d42BC32fCea61e75c87128eD4aC144133bba3",
    "0xCefbfccD83443E7aa1c493F4Df0036013542F301",
    "0x17E900C2a7A16695431469a40eA060D508eDD6D2",
    "0xFa705e3dBFeA930A3729A21665aa22C9529A5CCf",
    "0x37B70FB4269Db8B3Ae6Ff15b548277B2d9AA7777",
    "0x02626f2D3c273998823869422a6C4D5aC0dd2A07",
    "0xcf12f8aDE90f6A748aeC92Af3235b0bdfa4e27d5",
]

_i = 0
class DeterministicError(Exception):
    """Node answered with a non-transient error — retrying cannot help."""

_DET = ("exceeds limit", "not supported", "personal token", "invalid")

def rpc(method, params, budget=25):
    global _i
    pool = LOG_RPCS if method == "eth_getLogs" else RPCS
    body = json.dumps({"jsonrpc": "2.0", "id": 1, "method": method, "params": params}).encode()
    t0, delay = time.time(), 0.4
    last_err = None
    while True:
        url = pool[_i % len(pool)]
        _i += 1
        try:
            req = urllib.request.Request(url, data=body, headers=H)
            d = json.load(urllib.request.urlopen(req, timeout=25))
            if "result" in d:
                time.sleep(0.1)
                return d["result"]
            msg = str(d.get("error"))[:140]
            if any(k in msg.lower() for k in _DET):
                raise DeterministicError(msg)
            last_err = RuntimeError(msg)
        except DeterministicError:
            raise
        except Exception as e:
            last_err = e
        if time.time() - t0 > budget:
            raise last_err
        time.sleep(delay)
        delay = min(delay * 2, 8)

def call(to, data):
    return rpc("eth_call", [{"to": to, "data": data}, "latest"])

def dec_str(r):
    if not r or r == "0x":
        return ""
    raw = bytes.fromhex(r[2:])
    if len(raw) >= 96:
        try:
            ln = int.from_bytes(raw[64:96], "big")
            if 0 < ln <= 200:
                return raw[96:96 + ln].decode(errors="replace")
        except Exception:
            pass
    return raw.rstrip(b"\x00").decode(errors="replace")

def proxy_impl(code_hex):
    c = code_hex.lower()
    for marker in ("363d3d373d3d3d363d73", "363d3d37363d73", "363d3d3d363d73"):
        i = c.find(marker)
        if i >= 0:
            return "0x" + c[i + len(marker):i + len(marker) + 40]
    return None

def _logs(addr, lo, hi):
    """Returns a list of logs, or the string 'ERR' on node error (incl. the
    10k-result cap — which itself proves logs exist in range)."""
    try:
        return rpc("eth_getLogs", [{"address": addr, "fromBlock": hex(lo),
                                    "toBlock": hex(hi), "topics": [TRANSFER_TOPIC]}], budget=40) or []
    except Exception:
        return "ERR"

def first_mint(addr, head):
    """Binary-search earliest Transfer log: [0,X] non-empty or 'exceeds limit'
    error both mean a log exists at <=X. ~8 calls instead of a full walk."""
    lo, hi = 0, head
    probe = _logs(addr, 0, head)
    if isinstance(probe, list) and not probe:
        return None                      # zero Transfer events ever
    logs = probe if isinstance(probe, list) else None
    if logs:
        pass                                   # full index fit in one query
    else:
        # converge until the [0,mid] query itself fits under the 10k cap —
        # a dense token needs a window where the count is readable.
        while True:
            mid = (lo + hi) // 2
            r = _logs(addr, 0, mid)
            if r == "ERR":
                hi = mid
            elif r:
                logs = r                       # readable full-prefix set
                break
            else:
                lo = mid
            if hi - lo <= 1 and not r:
                break
    for lg in sorted(logs or [], key=lambda l: (int(l["blockNumber"], 16), int(l.get("logIndex", "0x0"), 16))):
        if int(lg["topics"][1], 16) == 0:
            return {"to": "0x" + lg["topics"][2][-40:], "tx": lg["transactionHash"],
                    "block": int(lg["blockNumber"], 16)}
    return None

def scan(c, head):
    print("scanning", c, file=sys.stderr)
    out = {"contract": c}
    code = rpc("eth_getCode", [c, "latest"]) or "0x"
    out["code_bytes"] = (len(code) - 2) // 2
    out["code_sha256_12"] = hashlib.sha256(bytes.fromhex(code[2:])).hexdigest()[:16]
    out["eip1167_impl"] = proxy_impl(code)
    out["name"] = dec_str(call(c, "0x06fdde03"))
    out["symbol"] = dec_str(call(c, "0x95d89b41"))
    d = call(c, "0x313ce567")
    out["decimals"] = int(d, 16) if d else None
    ts = call(c, "0x18160ddd")
    out["total_supply_raw"] = int(ts, 16) if ts else None
    fm = first_mint(c, head)
    out["first_mint"] = fm
    if fm:
        tx = rpc("eth_getTransactionByHash", [fm["tx"]])
        out["deployer"] = tx["from"] if tx else None
    print("done", c[:10], file=sys.stderr)
    return out

def main():
    head = int(rpc("eth_blockNumber", []), 16)
    res = []
    for c in CONTRACTS:
        try:
            res.append(scan(c, head))
        except Exception as e:
            res.append({"contract": c, "error": str(e)})
    print(json.dumps({"canonical": CANONICAL, "scan_head": head,
                      "scan_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
                      "results": res}, indent=1))

if __name__ == "__main__":
    main()
