#!/usr/bin/env python3
"""receipt_verify.py — ARION verification desk: single-tx receipt recompute.

Recomputes a payment/swap/escrow claim from raw chain reads and emits the
replayable inputs for a signed receipt post (musebook rubric v1.2).

EVM:   python3 receipt_verify.py evm <tx_hash> [--rpc URL] [--watch ADDR:TOKEN]
           [--min-conf N] [--undo-scan TOKEN]
Solana: python3 receipt_verify.py solana <signature> [--rpc URL] [--min-conf N]

--watch prints balanceOf(ADDR) on TOKEN at block-1, block, and latest,
which is how the CRT order was proven (8.25 -> 9.1681 USDC, to the wei).

v1.1 (buyer-requested): every result now carries a `finality` block —
chain head {number, hash, timestamp} read at verification time plus the
confirmation count (head - tx block). Embedding the head block hash binds
the recompute to a moment that could not be known earlier (timing proof);
`--min-conf N` downgrades the verdict to VALID-UNCONFIRMED below N
confirmations instead of silently passing a reorgable read.

v1.2 (colony peer spec): EVM results now also grade the finality tier the
tx has actually reached, using the RPC's `safe`/`finalized` block tags.
On OP-stack L2s (Base) `safe` = state derived from batch data posted to
L1 and `finalized` = that derivation L1-finalized, so the tier tells the
buyer which guarantee was *proven* instead of implying L1 settlement from
an L2 confirmation count. Tiers: L1_FINALIZED (tx block <= finalized
tag), L1_DATA_POSTED (<= safe tag), SEQUENCER_ONLY (above safe head),
UNGRADED (RPC lacks the tags — plain L1s or non-OP chains grade by
confirmation count alone). NOTE: on OP-stack chains L1_FINALIZED covers
sequencing derivation, not the 7-day withdrawal challenge window — bridge
withdrawals are not final at this tier.

v1.3 (colony peer spec — colonist-one/chloe thread): a leg-level VALID
verdict answers "did this transfer happen", never "does the recipient
still hold it". `--undo-scan TOKEN` enumerates the counter-legs: Transfer
events on TOKEN emitted after the leg's block going from the leg's
recipient back to its payer, chunked eth_getLogs over (leg_block, head].
The `reversal` block reports each candidate return leg, the summed
returned amount vs the leg amount, and a `correlation` field naming how
strong the match is. On plain-token rails nothing forces a refund to
reference the original tx hash, so correlation is declared honestly as
"amount-time" — a refund is identified by direction + amount + window,
which is evidence, not identity. Where a rail has no enumerable counter
-leg surface at all (e.g. Solana native lamports without an indexer, or
Lightning where a refund is a fresh payment hash) the block reports
correlation "none" instead of letting a check that cannot fail report
green. Verdict ladder for the reversal axis: NO_COUNTERLEG_FOUND /
PARTIAL_RETURN / RETURNED / OVER_RETURNED / UNSCANNABLE.

Deps: stdlib only. Public RPCs; override with --rpc / EVM_RPC / SOLANA_RPC.
"""
import json, sys, time, urllib.request

EVM_RPCS = ["https://mainnet.base.org", "https://base-rpc.publicnode.com"]
SOL_RPC = "https://api.mainnet-beta.solana.com"

TRANSFER = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
DEPOSIT = "0xe1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c"
WITHDRAW = "0x7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65"
BALANCE_OF = "0x70a08231"  # balanceOf(address)
SIG_NAMES = {TRANSFER: "Transfer", DEPOSIT: "Deposit", WITHDRAW: "Withdrawal"}


def rpc(url, method, params):
    body = json.dumps({"jsonrpc": "2.0", "id": 1, "method": method,
                       "params": params}).encode()
    req = urllib.request.Request(
        url, data=body, headers={"Content-Type": "application/json",
                                 "User-Agent": "ARION-verify/1.0"})
    with urllib.request.urlopen(req, timeout=25) as r:
        out = json.load(r)
    if "error" in out:
        raise RuntimeError(f"{method}: {out['error']}")
    return out["result"]


def evm_call(rpcs, method, params):
    last = None
    for url in rpcs:
        try:
            return rpc(url, method, params)
        except Exception as e:
            last = e
    raise last


def hx(tag):
    return "0x" + tag[-40:] if tag else None


def evm_head(rpcs):
    head = evm_call(rpcs, "eth_getBlockByNumber", ["latest", False])
    return {"number": int(head["number"], 16), "hash": head["hash"],
            "timestamp": int(head["timestamp"], 16)}


def evm_tag_height(rpcs, tag):
    """Block height for a named tag ('safe'/'finalized'), or None if the
    RPC does not support it (plain L1 nodes, non-OP chains)."""
    try:
        b = evm_call(rpcs, "eth_getBlockByNumber", [tag, False])
        return int(b["number"], 16) if b else None
    except Exception:
        return None


def evm_tier(block, safe_n, fin_n):
    if fin_n is not None and block <= fin_n:
        return "L1_FINALIZED"
    if safe_n is not None:
        return "L1_DATA_POSTED" if block <= safe_n else "SEQUENCER_ONLY"
    return "UNGRADED"


UNDO_CHUNK = 2000           # blocks per eth_getLogs call (public-RPC safe)
UNDO_MAX_BLOCKS = 200000    # ~4.6 days on Base; beyond -> truncated: true


def pad32(addr):
    return "0x" + "0" * 24 + addr.lower().replace("0x", "")


def evm_undo_scan(rpcs, token, frm, to, from_block, to_block):
    """Enumerate Transfer(token, frm->to) in (from_block, to_block].
    Chunked eth_getLogs; returns (legs, truncated)."""
    legs, truncated = [], False
    lo = from_block + 1
    while lo <= to_block:
        hi = min(lo + UNDO_CHUNK - 1, to_block)
        if lo - (from_block + 1) >= UNDO_MAX_BLOCKS:
            truncated = True
            break
        try:
            logs = evm_call(rpcs, "eth_getLogs", [{
                "address": token, "fromBlock": hex(lo), "toBlock": hex(hi),
                "topics": [TRANSFER, pad32(frm), pad32(to)]}])
        except Exception:
            truncated = True
            break
        for lg in logs:
            legs.append({"tx": lg["transactionHash"],
                         "block": int(lg["blockNumber"], 16),
                         "raw_value": int(lg["data"], 16)})
        lo = hi + 1
    return legs, truncated


def reversal_block(rpcs, token, payer, recipient, leg_value,
                   leg_block, head_n):
    legs, truncated = evm_undo_scan(rpcs, token, recipient, payer,
                                    leg_block, head_n)
    total = sum(l["raw_value"] for l in legs)
    if truncated:
        status = "UNSCANNABLE"
    elif not legs:
        status = "NO_COUNTERLEG_FOUND"
    elif total < leg_value:
        status = "PARTIAL_RETURN"
    elif total == leg_value:
        status = "RETURNED"
    else:
        status = "OVER_RETURNED"
    return {
        "token": token, "payer": payer, "recipient": recipient,
        "leg_raw_value": leg_value, "scan_window": [leg_block + 1, head_n],
        "counter_legs": legs, "returned_raw_total": total,
        "reversal_status": status, "truncated": truncated,
        "correlation": "amount-time",
        "correlation_note": (
            "enumerated all %s legs %s->%s in window; matching which leg "
            "a return undoes uses direction+amount+window — evidence, not "
            "identity. A rail with a native correlation key would grade "
            "stronger." % (token, recipient, payer)),
    }


def evm_verify(tx_hash, watch=None, rpcs=EVM_RPCS, min_conf=0,
               undo_token=None):
    rec = evm_call(rpcs, "eth_getTransactionReceipt", [tx_hash])
    if rec is None:
        return {"verdict": "UNVERIFIABLE", "reason": "tx not found", "tx": tx_hash}
    block = int(rec["blockNumber"], 16)
    head = evm_head(rpcs)
    safe_n = evm_tag_height(rpcs, "safe")
    fin_n = evm_tag_height(rpcs, "finalized")
    events = []
    for log in rec["logs"]:
        t0 = log["topics"][0] if log["topics"] else ""
        if t0 not in SIG_NAMES:
            continue
        ev = {"token": log["address"], "event": SIG_NAMES[t0]}
        if t0 == TRANSFER:
            ev["from"], ev["to"] = hx(log["topics"][1]), hx(log["topics"][2])
            ev["raw_value"] = int(log["data"], 16)
        else:  # Deposit/Withdrawal: owner in topic1, amount in data
            ev["owner"] = hx(log["topics"][1]) if len(log["topics"]) > 1 else None
            ev["raw_value"] = int(log["data"], 16)
        events.append(ev)
    confs = head["number"] - block + 1
    out = {
        "verdict": "VALID" if rec["status"] == "0x1" else "INVALID",
        "tx": tx_hash, "block": block, "status": rec["status"],
        "from": rec["from"], "to": rec["to"],
        "gas_used": int(rec["gasUsed"], 16),
        "transfer_events": events,
        "finality": {"chain_head": head, "confirmations": confs,
                     "finality_tier": evm_tier(block, safe_n, fin_n),
                     "safe_block": safe_n, "finalized_block": fin_n,
                     "min_conf_required": min_conf,
                     "verified_at": time.strftime("%Y-%m-%d %H:%M:%SZ",
                                                time.gmtime())},
    }
    if out["verdict"] == "VALID" and confs < min_conf:
        out["verdict"] = "VALID-UNCONFIRMED"
    if watch:
        acct, token = watch.split(":")
        data = BALANCE_OF + "0" * 24 + acct.lower().replace("0x", "")
        bals = {}
        for label, h in (("block-1", block - 1), ("block", block), ("latest", "latest")):
            tag = h if isinstance(h, str) else hex(h)
            r = evm_call(rpcs, "eth_call",
                         [{"to": token, "data": data}, tag])
            bals[label] = int(r, 16) if r and r != "0x" else 0
        out["balance_watch"] = {"account": acct, "token": token,
                                "raw_units": bals,
                                "delta_at_block": bals["block"] - bals["block-1"]}
    if undo_token:
        leg = next((e for e in events
                    if e["token"].lower() == undo_token.lower()
                    and e["event"] == "Transfer"), None)
        if leg is None:
            out["reversal"] = {"reversal_status": "UNSCANNABLE",
                               "reason": "no Transfer event on " + undo_token,
                               "correlation": "none"}
        else:
            out["reversal"] = reversal_block(
                rpcs, undo_token, leg["from"], leg["to"],
                leg["raw_value"], block, head["number"])
    return out


def sol_verify(sig, url=SOL_RPC, min_conf=0):
    tx = rpc(url, "getTransaction",
             [sig, {"encoding": "jsonParsed",
                    "maxSupportedTransactionVersion": 0}])
    if tx is None:
        return {"verdict": "UNVERIFIABLE", "reason": "tx not found", "sig": sig}
    status = rpc(url, "getSignatureStatuses", [[sig],
                 {"searchTransactionHistory": True}])["value"][0] or {}
    meta, msg = tx["meta"], tx["transaction"]["message"]
    keys = [k["pubkey"] if isinstance(k, dict) else k for k in msg["accountKeys"]]
    sol_deltas = {}
    for i, k in enumerate(keys):
        d = meta["postBalances"][i] - meta["preBalances"][i]
        if d:
            sol_deltas[k] = d / 1e9
    tok = {}
    for side in ("pre", "post"):
        for b in meta.get(side + "TokenBalances", []):
            key = (b.get("owner"), b["mint"])
            amt = float(b["uiTokenAmount"]["uiAmount"] or 0)
            tok.setdefault(key, {})[side] = amt
    token_deltas = [{"owner": k[0], "mint": k[1],
                     "delta": round(v.get("post", 0) - v.get("pre", 0), 9)}
                    for k, v in tok.items()
                    if abs(v.get("post", 0) - v.get("pre", 0)) > 0]
    out = {
        "verdict": "VALID" if meta["err"] is None else "INVALID",
        "sig": sig, "slot": tx["slot"],
        "blockTime": time.strftime("%Y-%m-%d %H:%M:%SZ",
                                   time.gmtime(tx["blockTime"] or 0)),
        "fee_sol": meta["fee"] / 1e9,
        "sol_deltas": sol_deltas, "token_deltas": token_deltas,
        "programs": sorted({p.get("programId") or p["program"]
                            for p in msg["instructions"]
                            if isinstance(p, dict)}),
        "finality": {"confirmation_status": status.get("confirmationStatus"),
                     "confirmations": status.get("confirmations"),
                     "min_conf_required": min_conf,
                     "verified_at": time.strftime("%Y-%m-%d %H:%M:%SZ",
                                                time.gmtime())},
    }
    conf = status.get("confirmations")
    if (out["verdict"] == "VALID"
            and status.get("confirmationStatus") != "finalized"
            and (conf is None or conf < min_conf)):
        out["verdict"] = "VALID-UNCONFIRMED"
    return out


def main():
    if len(sys.argv) < 3 or sys.argv[1] not in ("evm", "solana"):
        sys.exit(__doc__)
    mode, target = sys.argv[1], sys.argv[2]
    args = sys.argv[3:]
    watch = rpc_url = undo_token = None
    min_conf = 0
    for i, a in enumerate(args):
        if a == "--watch":
            watch = args[i + 1]
        elif a == "--rpc":
            rpc_url = args[i + 1]
        elif a == "--min-conf":
            min_conf = int(args[i + 1])
        elif a == "--undo-scan":
            undo_token = args[i + 1]
    if mode == "evm":
        out = evm_verify(target, watch, [rpc_url] if rpc_url else EVM_RPCS,
                         min_conf, undo_token)
    else:
        out = sol_verify(target, rpc_url or SOL_RPC, min_conf)
    print(json.dumps(out, indent=2))


if __name__ == "__main__":
    main()
