#!/usr/bin/env python3
"""evm_token_security.py — EVM token security snapshot.

Input : an ERC-20 contract address (0x...) and optionally a chain name
        (base, ethereum, bsc, polygon, arbitrum, optimism, avalanche).
        If omitted, the chain is auto-detected from DexScreener pairs.
Output: markdown security snapshot — GoPlus contract flags (honeypot,
        mintable, proxy, hidden owner, selfdestruct, taxes, ownership)
        plus DexScreener market data (liquidity, price, pairs, age,
        volume). Honest scope: public API screening signals + market
        data; NOT a contract-level audit.

Usage:  python3 evm_token_security.py <0xADDR> [chain] [out.md]
Deps:   stdlib only (urllib). No API keys required.
"""
import json, sys, time, urllib.request

DEX = "https://api.dexscreener.com/latest/dex/tokens/"
GOPLUS = "https://api.gopluslabs.io/api/v1/token_security/"

# dexscreener chainId -> (goplus chain_id, display name)
CHAINS = {
    "ethereum": ("1", "Ethereum"),
    "bsc": ("56", "BNB Chain"),
    "polygon": ("137", "Polygon"),
    "arbitrum": ("42161", "Arbitrum"),
    "optimism": ("10", "Optimism"),
    "base": ("8453", "Base"),
    "avalanche": ("43114", "Avalanche"),
    "fantom": ("250", "Fantom"),
    "cronos": ("25", "Cronos"),
    "zksync": ("324", "zkSync Era"),
}


def get_json(url):
    req = urllib.request.Request(
        url, headers={"User-Agent": "automaton-safe-evm-token-scan/1.0"})
    with urllib.request.urlopen(req, timeout=20) as r:
        return json.load(r)


def truthy(v):
    return str(v) == "1"


def snapshot(addr, chain=None):
    pairs = []
    try:
        pairs = get_json(DEX + addr).get("pairs") or []
    except Exception:
        pass

    if chain:
        cid = chain.lower()
        if cid not in CHAINS:
            for k, (gid, disp) in CHAINS.items():
                if cid in (gid, disp.lower()):
                    cid = k
                    break
        if cid not in CHAINS:
            raise RuntimeError(f"unknown chain '{chain}'; supported: "
                               + ", ".join(CHAINS))
    elif pairs:
        supported = [p for p in pairs if p.get("chainId") in CHAINS]
        if not supported:
            raise RuntimeError(
                f"token only trades on unsupported chains "
                f"({sorted({p.get('chainId') for p in pairs})}); "
                "pass a supported chain explicitly")
        cid = max(supported,
                  key=lambda p: (p.get("liquidity") or {}).get("usd") or 0
                  )["chainId"]
    else:
        raise RuntimeError("no DEX pairs indexed — pass the chain "
                           "explicitly (e.g. base, ethereum, bsc)")

    goplus_id, chain_name = CHAINS[cid]
    chain_pairs = [p for p in pairs if p.get("chainId") == cid]
    base_pairs = [p for p in chain_pairs
                  if (p.get("baseToken") or {}).get("address", "").lower()
                  == addr.lower()]
    top = max(base_pairs or chain_pairs,
              key=lambda p: (p.get("liquidity") or {}).get("usd") or 0,
              default=None)

    gp = {}
    gp_err = None
    try:
        d = get_json(f"{GOPLUS}{goplus_id}?contract_addresses={addr.lower()}")
        res = d.get("result") or {}
        gp = res.get(addr.lower()) or next(iter(res.values()), {}) or {}
    except Exception as e:
        gp_err = str(e)

    price_usd = fdv_usd = None
    if top and (top.get("baseToken") or {}).get("address", "").lower() \
            == addr.lower():
        price_usd, fdv_usd = top.get("priceUsd"), top.get("fdv")
    liq = sum((p.get("liquidity") or {}).get("usd") or 0
              for p in chain_pairs)
    vol24 = sum((p.get("volume") or {}).get("h24") or 0
                for p in chain_pairs)
    txns24 = sum(((p.get("txns") or {}).get("h24") or {}).get("buys", 0)
                 + ((p.get("txns") or {}).get("h24") or {}).get("sells", 0)
                 for p in chain_pairs)
    oldest = min((p.get("pairCreatedAt") or 0 for p in chain_pairs),
                 default=0)

    flags = []
    if gp:
        checks = [
            ("is_honeypot", "HIGH",
             "Honeypot flag — buyers may be unable to sell.",
             "No honeypot flag."),
            ("is_mintable", "HIGH",
             "Mintable — new supply can be created (dilution risk).",
             "Not mintable."),
            ("hidden_owner", "HIGH",
             "Hidden owner detected — ownership obscured from view.",
             "No hidden owner."),
            ("can_take_back_ownership", "HIGH",
             "Owner can reclaim ownership after renouncing.",
             "Ownership cannot be retaken."),
            ("selfdestruct", "HIGH",
             "Selfdestruct present — contract can destroy itself.",
             "No selfdestruct."),
            ("owner_change_balance", "HIGH",
             "Owner can arbitrarily change holder balances.",
             "Owner cannot change balances."),
            ("is_proxy", "WARN",
             "Proxy contract — logic can be swapped by the deployer.",
             "Not a proxy."),
            ("is_open_source", None, None, "Contract source verified."),
            ("is_in_dex", None, None, "Listed on at least one DEX."),
        ]
        for key, sev, bad, good in checks:
            v = gp.get(key)
            if v is None:
                continue
            on = truthy(v)
            if key in ("is_open_source", "is_in_dex"):
                flags.append(("OK" if on else "WARN",
                              good if on else
                              ("Source NOT verified — bytecode only."
                               if key == "is_open_source"
                               else "No DEX listing reported by GoPlus.")))
            else:
                flags.append((sev, bad) if on else ("OK", good))
        for key, label in (("buy_tax", "Buy tax"), ("sell_tax", "Sell tax"),
                           ("transfer_tax", "Transfer tax")):
            try:
                t = float(gp.get(key) or 0)
            except (TypeError, ValueError):
                t = 0
            if t > 0.10:
                flags.append(("HIGH", f"{label} {t:.0%} — extreme."))
            elif t > 0.03:
                flags.append(("WARN", f"{label} {t:.1%}."))
            elif t > 0:
                flags.append(("OK", f"{label} {t:.1%}."))
        if truthy(gp.get("is_whitelisted")) or truthy(
                gp.get("is_blacklisted")):
            flags.append(("WARN", "Transfer whitelist/blacklist controls "
                         "present — transfers can be restricted."))
        hc = gp.get("holder_count")
        if hc is not None:
            try:
                flags.append(("OK" if int(hc) >= 100 else "WARN",
                              f"{int(hc):,} holders."))
            except (TypeError, ValueError):
                pass
    else:
        flags.append(("WARN", f"GoPlus security data unavailable"
                     + (f" ({gp_err})" if gp_err else "")
                     + " — market data only."))

    if chain_pairs:
        if liq < 5000:
            flags.append(("WARN", f"Thin liquidity (${liq:,.0f} across "
                         f"{len(chain_pairs)} pairs) — easy to manipulate."))
        else:
            flags.append(("OK", f"Liquidity ${liq:,.0f} across "
                         f"{len(chain_pairs)} {chain_name} pairs."))
        if oldest and (time.time() * 1000 - oldest) < 7 * 86400e3:
            flags.append(("WARN", "Oldest trading pair is <7 days old."))
    else:
        flags.append(("WARN", f"No {chain_name} DEX pairs indexed on "
                     "DexScreener — token may be untraded or pre-launch."))

    name = sym = ""
    if top:
        bt = top.get("baseToken") or {}
        if bt.get("address", "").lower() == addr.lower():
            name, sym = bt.get("name", ""), bt.get("symbol", "")
        else:
            qt = top.get("quoteToken") or {}
            name, sym = qt.get("name", ""), qt.get("symbol", "")
    name = name or gp.get("token_name", "")
    sym = sym or gp.get("token_symbol", "")

    return {
        "address": addr, "chain": chain_name, "chain_id": cid,
        "name": name, "symbol": sym,
        "dex_pairs": len(chain_pairs), "liquidity_usd": liq,
        "volume_24h_usd": vol24, "txns_24h": txns24,
        "price_usd": price_usd, "fdv_usd": fdv_usd,
        "top_pair": (top or {}).get("pairAddress"),
        "top_dex": (top or {}).get("dexId"),
        "oldest_pair_age_days":
            round((time.time() * 1000 - oldest) / 86400e3, 1)
            if oldest else None,
        "holder_count": gp.get("holder_count"),
        "goplus_flags": {k: gp.get(k) for k in (
            "is_honeypot", "is_mintable", "is_proxy", "hidden_owner",
            "selfdestruct", "owner_change_balance",
            "can_take_back_ownership", "is_open_source", "is_in_dex",
            "buy_tax", "sell_tax", "transfer_tax") if gp.get(k) is not None},
        "flags": flags,
    }


def report(s):
    L = []
    L.append(f"# EVM Token Security Snapshot — {s['chain']}")
    L.append(f"generated {time.strftime('%Y-%m-%d %H:%M:%SZ', time.gmtime())}"
             " · automaton-safe")
    L.append("")
    L.append(f"**Contract:** `{s['address']}`")
    if s["name"] or s["symbol"]:
        L.append(f"**Token:** {s['name']} ({s['symbol']})")
    if s["holder_count"] is not None:
        L.append(f"**Holders:** {s['holder_count']}")
    L.append("")
    L.append("## Contract flags (GoPlus)")
    for sev, msg in s["flags"]:
        L.append(f"- **[{sev}]** {msg}")
    L.append("")
    L.append("## Market data (DexScreener)")
    L.append(f"- {s['chain']} DEX pairs: {s['dex_pairs']}"
             + (f" (top: {s['top_dex']} `{s['top_pair']}`)"
                if s["top_pair"] else ""))
    if s["price_usd"]:
        L.append(f"- Price: ${s['price_usd']}  ·  FDV: "
                 f"${(s['fdv_usd'] or 0):,.0f}")
    L.append(f"- Liquidity: ${s['liquidity_usd']:,.0f}  ·  "
             f"24h volume: ${s['volume_24h_usd']:,.0f}  ·  "
             f"24h txns: {s['txns_24h']}")
    if s["oldest_pair_age_days"] is not None:
        L.append(f"- Oldest pair age: {s['oldest_pair_age_days']} days")
    L.append("")
    L.append("---")
    L.append("*Scope: public API screening signals (GoPlus) + market data. "
             "This is a snapshot, not a contract audit — byte-level review "
             "needs source analysis.*")
    return "\n".join(L)


if __name__ == "__main__":
    if len(sys.argv) < 2:
        sys.exit("usage: evm_token_security.py <0xADDR> [chain] [out.md]")
    addr = sys.argv[1]
    if not (addr.startswith("0x") and len(addr) == 42):
        sys.exit("error: expected a 0x-prefixed 20-byte EVM address")
    rest = sys.argv[2:]
    chain = rest[0] if rest and not rest[0].endswith(".md") else None
    out_path = next((a for a in rest if a.endswith(".md")), None)
    out = report(snapshot(addr, chain))
    if out_path:
        open(out_path, "w").write(out + "\n")
    else:
        print(out)
