#!/usr/bin/env python3
"""Clustly settle-rate row v1.1 — cumulative wallet-set.

Delta vs pinned v1.0.0 (sha256 acb3635f…, jill-verified 2026-09-29):
enumeration is no longer re-derived fresh from the live feed each run.
v1.1 keeps a ledger (clustly_wallets_seen.json, next to this file) of every
agentWallet ever observed, unions it with the current feed, and scans the
UNION. A wallet whose listings all vanished between snapshots stays under
observation as a "ghost" — closing the settled-but-unlisted edge jill
flagged on 2026-09-29 (delisted-wallet settles were silently unscanned).

Row semantics unchanged: a wallet "has receive evidence" iff its USDC ATA
exists with tx history or balance > 0, or the owner account shows any
confirmed signatures. Each row gains `in_current_feed` and `first_seen`.

Stdlib only. Prints the row + writes JSON next to this file.
"""
import json, time, urllib.request, sys, os

RPC = "https://api.mainnet-beta.solana.com"
USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
HERE = os.path.dirname(os.path.abspath(__file__))
LEDGER = os.path.join(HERE, "clustly_wallets_seen.json")

def rpc(method, params, retries=3):
    body = json.dumps({"jsonrpc": "2.0", "id": 1, "method": method, "params": params}).encode()
    for i in range(retries):
        try:
            req = urllib.request.Request(RPC, data=body,
                headers={"Content-Type": "application/json", "User-Agent": "ARION-brain/1.0"})
            d = json.load(urllib.request.urlopen(req, timeout=25))
            if "error" in d:
                return {"error": d["error"]}
            return d.get("result")
        except Exception as e:
            if i == retries - 1:
                return {"error": str(e)}
            time.sleep(2)

def load_ledger():
    if os.path.exists(LEDGER):
        try:
            return json.load(open(LEDGER))
        except Exception:
            pass
    return {"wallets": {}}

def main():
    now = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
    req = urllib.request.Request("https://clustly.ai/v1/listings",
                                 headers={"User-Agent": "ARION-brain/1.0"})
    d = json.load(urllib.request.urlopen(req, timeout=25))
    items = d if isinstance(d, list) else d.get("items", d.get("listings", []))
    feed_wallets = {}
    for l in items:
        feed_wallets.setdefault(l.get("agentWallet"), set()).add(l.get("agentName"))

    ledger = load_ledger()
    seen = ledger["wallets"]
    for w in feed_wallets:
        if w and w not in seen:
            seen[w] = {"first_seen": now}
    # jill (a): monotone union -> delisted-wallet diff channel. Stamp the run
    # where a known wallet first leaves the feed; that set is the ghost watchlist.
    newly_ghosted = []
    for w in seen:
        if w not in feed_wallets:
            if "first_ghost_at" not in seen[w]:
                seen[w]["first_ghost_at"] = now
                newly_ghosted.append(w)
        else:
            seen[w].pop("first_ghost_at", None)
    json.dump(ledger, open(LEDGER, "w"), indent=1)

    union = sorted(set(seen) | set(feed_wallets))
    ghosts = [w for w in union if w not in feed_wallets]
    print("listings=%d feed_wallets=%d union_wallets=%d ghosts=%d newly_ghosted=%d"
          % (len(items), len(feed_wallets), len(union), len(ghosts),
             len(newly_ghosted)))

    rows = []
    for w in union:
        names = sorted(feed_wallets.get(w) or ["<ghost>"])
        bal = rpc("getBalance", [w])
        sol = (bal or {}).get("value", 0) / 1e9 if isinstance(bal, dict) else 0
        sigs = rpc("getSignaturesForAddress", [w, {"limit": 20}])
        n_owner_sigs = len(sigs) if isinstance(sigs, list) else 0
        tok = rpc("getTokenAccountsByOwner",
                  [w, {"mint": USDC_MINT}, {"encoding": "jsonParsed"}])
        atas = tok.get("value", []) if isinstance(tok, dict) else []
        usdc_bal = 0.0
        ata_addr = None
        ata_sigs = 0
        first_sig = None
        if atas:
            a = atas[0]
            ata_addr = a.get("pubkey")
            try:
                usdc_bal = float(a["account"]["data"]["parsed"]["info"]
                                 ["tokenAmount"]["uiAmount"] or 0)
            except Exception:
                pass
            s2 = rpc("getSignaturesForAddress", [ata_addr, {"limit": 20}])
            if isinstance(s2, list):
                ata_sigs = len(s2)
                if s2:
                    first_sig = s2[-1].get("signature")
        has_recv = (usdc_bal > 0) or ata_sigs > 0 or n_owner_sigs > 0
        rows.append({
            "wallet": w, "agents": names, "sol": round(sol, 6),
            "owner_sigs_sampled": n_owner_sigs, "usdc_ata": ata_addr,
            "usdc_balance": usdc_bal, "ata_sigs_sampled": ata_sigs,
            "sample_tx": first_sig, "has_onchain_activity": has_recv,
            "in_current_feed": w in feed_wallets,
            "first_seen": seen.get(w, {}).get("first_seen"),
            "first_ghost_at": seen.get(w, {}).get("first_ghost_at"),
        })
        print("%s | %s | sol=%.6f owner_sigs=%d usdc=%s ata_sigs=%d active=%s%s"
              % (w[:12], names[0], sol, n_owner_sigs, usdc_bal, ata_sigs,
                 has_recv, "" if w in feed_wallets else " GHOST"))
        time.sleep(0.4)

    active = [r for r in rows if r["has_onchain_activity"]]
    out = {"checked_at": now, "version": "1.1.0-20260929",
           "listings": len(items), "feed_wallets": len(feed_wallets),
           "union_wallets": len(union), "ghost_wallets": len(ghosts),
           "newly_ghosted_this_run": len(newly_ghosted),
           "wallets_with_onchain_activity": len(active), "rows": rows}
    path = os.path.join(HERE, "clustly_settle_rate_v1_1_result.json")
    json.dump(out, open(path, "w"), indent=1)
    print("\nSETTLE-ROW: %d/%d union wallets show >=1 confirmed-chain signature; "
          "%d/%d hold nonzero USDC; %d ghost wallets under observation"
          % (len(active), len(union),
             sum(1 for r in rows if r["usdc_balance"] > 0), len(union),
             len(ghosts)))
    print("wrote", path)

if __name__ == "__main__":
    main()
