"""sellcross.py (ppfeeds #661, 10 Oct 2026): sell simulation AT the cap cross for hooked v4 pools (cls H3000, quote WETH or native).
For each pool, the first swap whose cap (USD, from sqrtP and supply) is >= a level (100k, 300k) triggers: UniversalRouter V4_SWAP eth_call selling
0.02 ETH worth of the token from a fresh address with state overrides (token balance, token->Permit2 allowance, Permit2->router allowance).
Output: sellcross.db table cross(pool, token, quote, level, t_cross, block, cap_usd, age_s, amount_in, ok, out_eth, revert, t_sim, mode, ...).
--backfill N: also the last N pools (simulated at the cross block when the node serves that state, else at latest, mode says which).
No money, no keys. Free providers only (rpcfall without the paid URL)."""
import sys, json, os, time, sqlite3, subprocess
sys.path.insert(0, "/opt/sw"); sys.path.insert(0, "/opt/sw/sellsim")
import rpcfall, collections
from sellprobe import k, pad, h, W, P2, UR, BIG, probe_balance, probe_allow
from sellsim import build, pool_key
D = "/opt/sw/sellsim"; DB = os.environ.get("SC_DB", D + "/sellcross.db"); HB = D + "/sellcross.hb"
SW = "/opt/sw/v4tape/swaps.jsonl"; PL = "/opt/sw/v4tape/pools.jsonl"
LEVELS = (100000, 300000); ETH_IN = 0.02
PUB = "main:/opt/launchwatch/pub/hx-c9ois/fw/sellcross.db"
try: ETHUSD = json.load(open("/opt/sw/ethusd_hourly.json"))[-1][1]
except Exception: ETHUSD = 2500.0
def rpc(m, p): return rpcfall.post({"jsonrpc": "2.0", "id": 1, "method": m, "params": p}, timeout=20, tries=3)
def db():
    c = sqlite3.connect(DB, timeout=30)
    c.execute("""CREATE TABLE IF NOT EXISTS cross(pool TEXT, token TEXT, hooks TEXT, quote TEXT, level INT, t_cross INT, block INT, cap_usd REAL, age_s INT,
      amount_in TEXT, ok INT, out_eth REAL, revert TEXT, t_sim REAL, mode TEXT, err TEXT, PRIMARY KEY(pool, level))""")
    return c
def cap_usd(sqrtp, tok, quote, supply):
    r = (sqrtp / 2 ** 96) ** 2          # token1 per token0
    tok_is_c0 = quote == "weth" and tok.lower() < "0x0bd7d308f8e1639fab988df18a8011f41eacad73"
    q_per_tok = r if tok_is_c0 else (1 / r if r else 0)   # quote units per token
    return supply * q_per_tok * ETHUSD, q_per_tok
def simulate(pid, p, supply, q_per_tok, block, hist):
    tok = p["token"]; tag = hex(block) if hist else "latest"
    key = pool_key(pid, p["block"])
    amt = int(ETH_IN / q_per_tok * 1e18) if q_per_tok > 0 else 0
    b = probe_balance(tok); a = probe_allow(tok, W, P2)
    if not b or not a: return dict(amt=amt, err="slot_probe_fail bal=%s allow=%s" % (bool(b), bool(a)))
    slot2 = h(k(pad(UR) + k(pad(tok) + k(pad(W) + pad(1)))))
    ov = {tok: {"stateDiff": {b[1]: h(pad(amt)), a[1]: h(pad(BIG))}},
          P2: {"stateDiff": {slot2: h(pad(((1 << 48) - 1) << 160 | (1 << 160) - 1))}}, W: {"balance": "0x" + "f" * 20}}
    def call(minout):
        return rpc("eth_call", [{"from": W, "to": UR, "data": build(key, tok, amt, minout)}, tag, ov])
    out = dict(amt=amt); r = call(0)
    if "result" in r: out["ok"] = 1
    elif "error" in r:
        d = str(r["error"].get("data", r["error"].get("message")))
        if "429" in d or "rate" in d.lower(): out["err"] = "rpc:" + d[:100]; return out
        out["ok"] = 0; out["revert"] = d[:200]
    if out.get("ok") == 1:
        r2 = call(1 << 120); d = r2.get("error", {}).get("data", "")
        if isinstance(d, str) and len(d) >= 10 + 128: out["out_eth"] = int(d[-64:], 16) / 1e18
    return out
WETH_ADDR = "0x0bd7d308f8e1639fab988df18a8011f41eacad73"; UR_SENDER = "0x8876789976decbfcbbbe364623c63652db8c0904"
ROUTERS = collections.defaultdict(collections.Counter)   # pool -> sender counts (non-UR); the hook accepts only the pool's own router (#2357)
def router_data(tok, hooks, amt, minout, router):
    ws = [WETH_ADDR, tok, 3000, 60, hooks, 0, amt, minout, int(router, 16), 0xffffffff]
    return "0xb3938887" + "".join(pad(x).hex() for x in ws)
def simulate_router(pid, p, router, amt, block, hist):
    tok = p["token"]; tag = hex(block) if hist else "latest"
    b = probe_balance(tok); a = probe_allow(tok, W, router)
    if not b or not a: return dict(amt=amt, err="slot_probe_fail bal=%s allow=%s" % (bool(b), bool(a)))
    ov = {tok: {"stateDiff": {b[1]: h(pad(amt)), a[1]: h(pad(1 << 200))}}, W: {"balance": "0x" + "f" * 20},
          router: {"stateDiff": {h(k(pad(W) + pad(4))): h(pad(1))}}}   # caller whitelist slot 4 (router buyers are whitelisted)
    def call(minout): return rpc("eth_call", [{"from": W, "to": router, "data": router_data(tok, p["hooks"], amt, minout, router)}, tag, ov])
    out = dict(amt=amt); r = call(0)
    if "result" in r: out["ok"] = 1
    elif "error" in r:
        d = str(r["error"].get("data", r["error"].get("message")))
        if "429" in d or "rate" in d.lower(): out["err"] = "rpc:" + d[:100]; return out
        out["ok"] = 0; out["revert"] = d[:200]
    if out.get("ok") == 1:
        d = call(1 << 120).get("error", {}).get("data", "")
        if isinstance(d, str) and len(d) >= 138: out["out_eth"] = int(d[-64:], 16) / 1e18
    return out
def publish(last=[0]):
    if os.environ.get("NOPUB") or time.time() - last[0] < 60: return
    last[0] = time.time()
    try:
        c = sqlite3.connect(DB); s = sqlite3.connect("/tmp/sellcross.snap"); c.backup(s); s.close(); c.close()
        subprocess.run(["scp", "-q", "/tmp/sellcross.snap", PUB + ".tmp"], timeout=40)
        subprocess.run(["ssh", "main", "mv /opt/launchwatch/pub/hx-c9ois/fw/sellcross.db.tmp /opt/launchwatch/pub/hx-c9ois/fw/sellcross.db"], timeout=30)
    except Exception as e: print("publish", repr(e)[:100], flush=True)
def main():
    nb = int(sys.argv[2]) if len(sys.argv) > 2 and sys.argv[1] == "--backfill" else 0
    pools = {}
    for l in open(PL):
        p = json.loads(l)
        if p.get("cls") == "H3000" and p.get("quote") in ("weth", "native"):
            try: p["supply_i"] = int(p["token_supply"])
            except Exception: p["supply_i"] = 10 ** 27
            pools[p["pool_id"]] = p
    c = db(); done = {(r[0], r[1]) for r in c.execute("select pool, level from cross")}
    first = {}   # (pool, level) -> swap row
    stop_before = None
    if nb:   # historic scan: last nb pools by init_time
        keep = {pid for pid, _ in sorted(((i, p["init_time"]) for i, p in pools.items()), key=lambda x: -x[1])[:nb]}
    else: keep = set()
    pos = 0
    f = open(SW)
    if not nb: f.seek(0, 2)    # live: start at the end
    n = 0; hist_ok = None
    while True:
        try:
            new_pools = False
            while True:
                ln = f.readline()
                if not ln or not ln.endswith("\n"):
                    if ln: f.seek(f.tell() - len(ln.encode()))
                    break
                try: s = json.loads(ln)
                except Exception: continue
                pid = s["pool_id"]
                if s.get("cls") != "H3000" or not s.get("sqrtP"): continue
                if s.get("sender") and s["sender"] != UR_SENDER: ROUTERS[pid][s["sender"]] += 1
                p = pools.get(pid)
                if not p:
                    for l in open(PL):    # pool added since start
                        q = json.loads(l)
                        if q["pool_id"] == pid and q.get("quote") in ("weth", "native"):
                            try: q["supply_i"] = int(q["token_supply"])
                            except Exception: q["supply_i"] = 10 ** 27
                            pools[pid] = p = q
                    if not p: continue
                if nb and pid not in keep: continue
                cap, qpt = cap_usd(s["sqrtP"], p["token"], p["quote"], p["supply_i"] / 1e18)
                for lv in LEVELS:
                    if cap >= lv and (pid, lv) not in done:
                        done.add((pid, lv)); first[(pid, lv)] = (s, cap, qpt)
            for (pid, lv), (s, cap, qpt) in list(first.items()):
                del first[(pid, lv)]; p = pools[pid]
                hist = bool(nb) and time.time() - s["t"] > 120
                try:
                    rt = ROUTERS[pid].most_common(1)[0][0] if p["quote"] == "weth" and ROUTERS[pid] else None
                    if rt: sim = lambda hh: simulate_router(pid, p, rt, int(ETH_IN / qpt * 1e18) if qpt > 0 else 0, s["block"], hh)
                    else: sim = lambda hh: simulate(pid, p, p["supply_i"], qpt, s["block"], hh)
                    r = sim(hist)
                    mode = ("router:" if rt else "") + ("at_cross" if not hist else "hist_at_block")
                    if hist and "rpc:" not in str(r.get("err")) and r.get("err") is None and r.get("ok") is None: pass
                    if hist and ("missing trie" in str(r.get("revert", "")) or "state" in str(r.get("revert", "")).lower()):
                        r = sim(False); mode = ("router:" if rt else "") + "hist_at_latest"
                except Exception as e: r = dict(err=repr(e)[:150]); mode = "err"
                if str(r.get("err", "")).startswith("rpc:") or "429" in str(r.get("err", "")):
                    done.discard((pid, lv)); first[(pid, lv)] = (s, cap, qpt); time.sleep(10); continue
                c.execute("insert or replace into cross values(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", (pid, p["token"], p["hooks"], p["quote"], lv, s["t"], s["block"], min(round(cap), 10**15),
                          s["t"] - p["init_time"], str(r.get("amt")), r.get("ok"), r.get("out_eth"), r.get("revert"), round(time.time(), 1), mode, r.get("err")))
                c.commit(); n += 1; publish()
            open(HB + ".tmp", "w").write(json.dumps(dict(t=time.time(), n=n))); os.replace(HB + ".tmp", HB)
            publish()
            if nb: nb = 0   # backfill pass done, keep following
        except Exception as e: print(time.strftime("%H:%M:%S"), "loop", repr(e)[:200], flush=True)
        time.sleep(3)
if __name__ == "__main__": main()
