#!/usr/bin/env python3
# DC A1 xfer.py: ERC20 Transfer logs for the 70 SW wide-trade tokens + FW mc5 live coins.
# One address per eth_getLogs (dRPC 500s on address arrays x 100k blocks), 99,999-block chunks, browser UA, hard cap 150 calls/min
# shared by 3 threads, no retry loops: an errored chunk is split into halves down to 12.5k blocks; a failed piece is logged as a gap.
# Range per token: walk back from the gate block in chunks until a chunk is empty or holds the mint (launch), then forward to gate + 50 h.
import sqlite3, json, time, urllib.request, sys, csv, os, datetime, threading
from concurrent.futures import ThreadPoolExecutor
sys.path.insert(0, "/opt/sw"); from pp3_tfix import PAID   # never printed
T_TR = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
CH = 99999; CAP = 150
FW = ["0xd273ef9fba402603d9f583e848a0708ed0efa55b", "0x717d38587504b79e2e888c860bd1569c0d5984af",
      "0x313e89b7798d0ec8b755fd46a9da9a14a4ece708", "0x1ec25a196c49dd3445f1fcc53c37c513af92839c",
      "0x206211d456a3a9dbe45e13507aac5131ace9c716"]
calls = []; LK = threading.Lock()
def rpc(method, params):
    while True:
        with LK:
            now = time.time()
            if len(calls) < CAP or now - calls[-CAP] >= 60: calls.append(now); break
        time.sleep(0.2)
    body = json.dumps({"jsonrpc": "2.0", "id": 1, "method": method, "params": params}).encode()
    req = urllib.request.Request(PAID, body, {"Content-Type": "application/json", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/128.0 Safari/537.36"})
    r = json.loads(urllib.request.urlopen(req, timeout=90).read())
    if "error" in r: raise RuntimeError(str(r["error"])[:120])
    return r["result"]
head = int(rpc("eth_blockNumber", []), 16)
rows = list(csv.DictReader(open("/root/dca1/sw-a2-16-wide-trades.csv")))
toks = sorted(set(r["token"].lower() for r in rows))
ts = lambda s: datetime.datetime.strptime(s, "%Y-%m-%d %H:%M:%S").timestamp()
g = sorted((int(r["gate_block"]), ts(r["gate_time_bkk"])) for r in rows)
BPS = (g[-1][0] - g[0][0]) / (g[-1][1] - g[0][1]); print("head", head, "blocks/s %.3f" % BPS, flush=True)
fw = []   # keep only FW addresses that answer symbol() (tokens, not pools)
for a in FW:
    try:
        s = rpc("eth_call", [{"to": a, "data": "0x95d89b41"}, "latest"])
        if s and len(s) > 2: fw.append(a); print("FW token", a, flush=True)
        else: print("FW not token", a, flush=True)
    except Exception as e: print("FW not token", a, repr(e)[:60], flush=True)
H50 = int(50 * 3600 * BPS); Z = "0x" + "0" * 40
gate = {}
for r in rows: t = r["token"].lower(); gate[t] = min(gate.get(t, 10**12), int(r["gate_block"]))
for a in fw: gate.setdefault(a, head)
os.makedirs("/opt/tw/an1", exist_ok=True)
db = sqlite3.connect("/opt/tw/an1/xfer.db", check_same_thread=False); DL = threading.Lock()
db.execute("create table if not exists tr(token text, b int, li int, tx text, frm text, too text, val text, primary key(b, li))")
db.execute("create table if not exists meta(k text primary key, v text)")
db.execute("create table if not exists gaps(token text, a int, b int, err text)")
db.execute("create table if not exists rng(token text primary key, a int, b int, gate int, launch_seen int)")
fails = 0; t0 = time.time(); done = 0
def get(tok, a, b, depth=0):
    global fails
    try: return rpc("eth_getLogs", [{"address": tok, "topics": [T_TR], "fromBlock": hex(a), "toBlock": hex(b)}])
    except Exception as e:
        if depth < 3 and b > a:
            m = (a + b) // 2
            return get(tok, a, m, depth + 1) + get(tok, m + 1, b, depth + 1)
        with DL: fails += 1; db.execute("insert into gaps values(?,?,?,?)", (tok, a, b, repr(e)[:120]))
        return []
def conv(lg): return [(l["address"].lower(), int(l["blockNumber"], 16), int(l["logIndex"], 16), l["transactionHash"],
    "0x" + l["topics"][1][-40:], "0x" + l["topics"][2][-40:], str(int(l["data"], 16) if l["data"] not in ("0x", "") else 0)) for l in lg if len(l["topics"]) == 3]
def job(tok):
    global done
    g = gate[tok]; hi = min(head, g + H50); out = []; lo = g; seen = 0
    b = g - 1
    for _ in range(40):                      # back from the gate to launch
        a = b - CH; lg = conv(get(tok, a, b)); out += lg; lo = a
        if not lg: seen = 1; break
        if any(x[4] == Z for x in lg) and min(x[1] for x in lg if x[4] == Z) <= min(x[1] for x in lg): seen = 2; break
        b = a - 1
    for a in range(g, hi + 1, CH + 1): out += conv(get(tok, a, min(a + CH, hi)))
    with DL:
        db.executemany("insert or ignore into tr values(?,?,?,?,?,?,?)", out)
        db.execute("insert or replace into rng values(?,?,?,?,?)", (tok, lo, hi, g, seen)); db.commit(); done += 1
        print(time.strftime("%H:%M:%S"), "token", done, "of", len(gate), tok, "rows", len(out), "launch", seen, "%.0f s" % (time.time() - t0), flush=True)
with ThreadPoolExecutor(3) as ex: list(ex.map(job, sorted(gate)))
db.execute("create index if not exists tr_tok on tr(token, b)")
db.executemany("insert or replace into meta values(?,?)", [("tokens", json.dumps(sorted(gate))), ("fw_tokens", json.dumps(fw)), ("head", str(head)),
    ("built_bkk", time.strftime("%Y-%m-%d %H:%M:%S")), ("gap_pieces", str(fails)),
    ("note", "ERC20 Transfer logs, launch to gate + 50 h (SW entry is within seconds of gate); FW coins launch to head. rng.launch_seen 1 = empty chunk reached, 2 = mint chunk reached, 0 = 40-chunk cap")])
db.commit()
print("DONE rows", db.execute("select count(*) from tr").fetchone()[0], "tokens with rows", db.execute("select count(distinct token) from tr").fetchone()[0], "gaps", fails, flush=True)
