WSOL="So11111111111111111111111111111111111111112"
STABLE={"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v":"USDC","Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB":"USDT",
 "USD1ttGY1N17NEEHLmELoaybftRBUSErhqYiQzvEmuB":"USD1","2b1kV6DkPAnxd5ixfnxCpjxmKwqjjaYmCZfHsFu24GXo":"PYUSD"}
def parse(t,mint,pools):
    if not t or t.get("meta") is None or t["meta"].get("err"):return None
    m=t["meta"];msg=t["transaction"]["message"]
    keys=[k["pubkey"] if isinstance(k,dict) else k for k in msg["accountKeys"]]
    signers=[k["pubkey"] for k in msg["accountKeys"] if isinstance(k,dict) and k.get("signer")]
    payer=keys[0]
    pre,post=m["preBalances"],m["postBalances"]
    def tb(lst):
        d={}
        for b in lst or []:
            a=b["uiTokenAmount"].get("uiAmount") or 0
            d[(b.get("owner"),b["mint"],b["accountIndex"])]=a
        return d
    P,Q=tb(m.get("preTokenBalances")),tb(m.get("postTokenBalances"))
    allk=set(P)|set(Q)
    delta={}   # (owner,mint)->delta
    preamt={}
    for k in allk:
        o,mi,_=k;delta[(o,mi)]=delta.get((o,mi),0)+Q.get(k,0)-P.get(k,0);preamt[(o,mi)]=preamt.get((o,mi),0)+P.get(k,0)
    md={o:v for (o,mi),v in delta.items() if mi==mint}
    pos={o:v for o,v in md.items() if v>0 and o not in pools}
    neg={o:v for o,v in md.items() if v<0}
    if not pos or not neg:return None           # not a swap buy (transfer, mint, sell)
    buyer=max(pos,key=pos.get);multi=len(pos)>1
    def sol_dec(a):
        if a in keys:i=keys.index(a);return (pre[i]-post[i])/1e9
        return 0
    def tok_dec(a,mi):return -delta.get((a,mi),0)
    cands=[buyer]+([payer] if payer!=buyer else [])
    spent_sol=max(sol_dec(a)+tok_dec(a,WSOL) for a in cands)
    spent_stab=max(sum(tok_dec(a,s) for s in STABLE) for a in cands)
    if spent_sol<=0.001 and spent_stab<=0.5:return None   # tokens received without paying -> not a buy by this wallet
    cash_sol=None
    if buyer in keys:cash_sol=pre[keys.index(buyer)]/1e9
    cash_wsol=preamt.get((buyer,WSOL),0)
    cash_stab={STABLE[s]:preamt.get((buyer,s),0) for s in STABLE if (buyer,s) in preamt}
    return {"sig":t["transaction"]["signatures"][0],"ts":t["blockTime"],"buyer":buyer,"payer":payer,
      "buyer_is_signer":buyer in signers,"buyer_is_payer":buyer==payer,"multi":multi,"tokens":pos[buyer],
      "spent_sol":spent_sol,"spent_stab":spent_stab,"cash_sol":cash_sol,"cash_wsol":cash_wsol,"cash_stab":cash_stab,"nsig":len(signers)}
import requests,time,random,json,sys,threading,concurrent.futures as cf
EPS=["https://api.mainnet-beta.solana.com"]
_i=[0];S=requests.Session()
def rpc(method,params,tries=10):
    for i in range(tries):
        U=EPS[(_i[0]+i)%len(EPS)];_i[0]+=1
        try:
            r=S.post(U,json={"jsonrpc":"2.0","id":1,"method":method,"params":params},timeout=40)
            if r.status_code==429:time.sleep(0.5+i*0.7);continue
            j=r.json()
            if "error" in j:time.sleep(0.5+i);continue
            return j["result"]
        except Exception:time.sleep(1+i)
    return None
def sigs(addr,stop_ts,maxpages):
    out=[];before=None
    for p in range(maxpages):
        prm={"limit":1000}
        if before:prm["before"]=before
        r=rpc("getSignaturesForAddress",[addr,prm])
        if r is None:return out,False
        out+=r
        if len(r)<1000 or (r[-1].get("blockTime") or 1e12)<stop_ts:return out,True
        before=r[-1]["signature"]
    return out,False
def gettx(s):return rpc("getTransaction",[s,{"encoding":"jsonParsed","maxSupportedTransactionVersion":0,"commitment":"confirmed"}])
def run(tok,N=120,W=48*3600):
    c=tok["created_timestamp"]/1000;mint=tok["mint"]
    pools={tok.get("bonding_curve"),tok.get("pump_swap_pool")}-{None}
    ss,ok=sigs(mint,c,400);src="mint"
    if not ok:
        ss,ok=sigs(tok["bonding_curve"],c,100);src="curve"
    w=[s for s in ss if s.get("err") is None and s.get("blockTime") and c<=s["blockTime"]<c+W]
    random.seed(mint);samp=random.sample(w,min(N,len(w)))
    with cf.ThreadPoolExecutor(8) as ex:txs=list(ex.map(lambda s:gettx(s["signature"]),samp))
    ev=[];fail=0
    for t in txs:
        if t is None:fail+=1;continue
        try:r=parse(t,mint,pools)
        except Exception as e:r=None
        if r:r["mint"]=mint;ev.append(r)
    return {"mint":mint,"src":src,"reached":ok,"n_window":len(w),"n_sample":len(samp),"fail":fail,"n_buys":len(ev)},ev
if __name__=="__main__":
    arg=sys.argv[1];out=sys.argv[2]
    if arg.endswith(".txt"):
        toks=[]
        for m in open(arg).read().split():
            try:
                c=requests.get(f"https://frontend-api-v3.pump.fun/coins-v2/{m}",headers={"User-Agent":"Mozilla/5.0 (Macintosh) Chrome/120"},timeout=30).json()
                toks.append({k:c.get(k) for k in('mint','symbol','created_timestamp','bonding_curve','pump_swap_pool')})
            except Exception as e:print("meta fail",m)
    else:toks=json.load(open(arg))
    done=set()
    try:done={json.loads(l)["meta"]["mint"] for l in open(out) if '"meta"' in l}
    except FileNotFoundError:pass
    L=threading.Lock()
    def one(tok):
        if tok["mint"] in done:return
        t0=time.time()
        try:meta,ev=run(tok)
        except Exception as e:meta,ev={"mint":tok["mint"],"error":str(e)},[]
        with L, open(out,"a") as f:
            f.write(json.dumps({"meta":meta})+"\n")
            for e in ev:f.write(json.dumps(e)+"\n")
            print(tok["symbol"],meta.get("src"),meta.get("reached"),meta.get("n_window"),meta.get("n_buys"),round(time.time()-t0),flush=True)
    with cf.ThreadPoolExecutor(3) as ex:list(ex.map(one,toks))
