near-toll-audit / pipeline/analyze_explorer.py · raw
#!/usr/bin/env python3"""Aggregates the private swap rows into the small CSVs committed under raw/explorer/.Rows stay local (hundreds of MB, and hashed addresses have no public use). Aggregates are enough to rebuild everychart offline. Anyone with a free Explorer key can regenerate the rows, because the sampling is deterministic."""import csv, gzip, hashlib, io, json, math, pathlib, re, statistics, sysfrom collections import Counter, defaultdictROOT = pathlib.Path(__file__).resolve().parent.parentSRC = ROOT / "raw_private" / "explorer"OUT = ROOT / "raw" / "explorer"sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))NEAR_WALLETS = {"buybacks.multisignature.near", "fefundsadmin.sputnik-dao.near", "1csfundsadmin.sputnik-dao.near"}MANIFEST = []def token_meta(): meta = {} for r in csv.DictReader((pathlib.Path(__file__).resolve().parent / "sql" / "token_prices.csv").open()): meta["nep141:" + r["token"]] = r return metaMETA = token_meta()STABLE = {"USDC", "USDT", "USDT0", "DAI", "USD1", "USDG", "USDC.E", "XDAI", "FRAX", "USDE"}def family(asset): sym = (META.get(asset) or {}).get("symbol", "").upper() if sym in STABLE: return "stable" if "BTC" in sym: return "btc" if sym in ("ETH", "WETH"): return "eth" if sym == "ZEC": return "zec" if sym in ("SOL", "WSOL"): return "sol" if sym in ("WNEAR", "NEAR"): return "near" return "other"def chain(asset): return (META.get(asset) or {}).get("blockchain") or "other"def rows_in(path): with gzip.open(path, "rt") as fh: for r in csv.DictReader(fh): try: r["in_usd"], r["out_usd"] = float(r["in_usd"] or 0), float(r["out_usd"] or 0) except ValueError: continue r["bps"] = [] for part in (r["fees"] or "").split(";"): if ":" in part: who, fee = part.rsplit(":", 1) try: r["bps"].append((who, float(fee))) except ValueError: pass yield rdef size_bucket(usd): if usd < 100: return "under $100" if usd < 1000: return "$100-1k" if usd < 10000: return "$1k-10k" if usd < 100000: return "$10k-100k" return "over $100k"def write(name, rows): OUT.mkdir(parents=True, exist_ok=True) if not rows: return buf = io.StringIO() wr = csv.DictWriter(buf, fieldnames=list(rows[0].keys()), lineterminator="\n") wr.writeheader() wr.writerows(rows) raw = buf.getvalue().encode() with gzip.GzipFile(OUT / f"{name}.csv.gz", "wb", mtime=0) as gz: gz.write(raw) MANIFEST.append({"file": f"{name}.csv.gz", "rows": len(rows), "sha256_csv": hashlib.sha256(raw).hexdigest()}) print(name, len(rows))def panel(): daily, partner, recipient, routes, sizes, cost = [], [], [], [], [], [] files = sorted((SRC / "panel").glob("*.csv.gz")) for path in files: day = path.name[:10] st = Counter() usd = fee = out = 0.0 p_usd, p_fee, p_n = Counter(), Counter(), Counter() p_posted = defaultdict(Counter) r_fee = Counter() fam, cha = Counter(), Counter() sz_usd, sz_n = Counter(), Counter() costs = defaultdict(list) send = Counter() blank = 0.0 for r in rows_in(path): st[r["status"]] += 1 if r["status"] != "SUCCESS" or r["in_usd"] <= 0: continue i = r["in_usd"] usd += i out += r["out_usd"] ref = re.sub(r"[^A-Za-z0-9 ._()@:-]", "_", r["referral"] or "(none)")[:60] f = sum(b for _, b in r["bps"]) / 1e4 * i fee += f p_usd[ref] += i p_fee[ref] += f p_n[ref] += 1 p_posted[ref][round(sum(b for _, b in r["bps"]), 1)] += 1 for who, b in r["bps"]: r_fee[who] += b / 1e4 * i a, b = family(r["origin"]), family(r["dest"]) fam[f"{a}>{b}"] += i cha[f"{chain(r['origin'])}>{chain(r['dest'])}"] += i sb = size_bucket(i) sz_usd[sb] += i sz_n[sb] += 1 if i >= 10 and r["out_usd"] > 0: costs[sb].append((i - r["out_usd"]) / i * 1e4) if r["sender"]: send[r["sender"]] += i else: blank += i top = send.most_common(100) daily.append({"day": day, "swaps": st["SUCCESS"], "refunded": st["REFUNDED"], "failed": st["FAILED"], "usd_in": round(usd), "usd_out": round(out), "app_fee_usd": round(fee), "near_wallet_fee_usd": round(sum(v for k, v in r_fee.items() if k in NEAR_WALLETS)), "senders": len(send), "blank_sender_usd": round(blank), "top1_sender_usd": round(top[0][1]) if top else 0, "top10_sender_usd": round(sum(v for _, v in top[:10])), "top100_sender_usd": round(sum(v for _, v in top))}) for k, v in p_usd.items(): partner.append({"day": day, "partner": k, "usd": round(v), "swaps": p_n[k], "fee_usd": round(p_fee[k], 2), "posted_bps": p_posted[k].most_common(1)[0][0]}) for k, v in r_fee.items(): recipient.append({"day": day, "recipient": k, "fee_usd": round(v, 2)}) for k, v in fam.items(): routes.append({"day": day, "kind": "family", "route": k, "usd": round(v)}) for k, v in cha.items(): routes.append({"day": day, "kind": "chain", "route": k, "usd": round(v)}) for k, v in sz_usd.items(): q = statistics.quantiles(costs[k], n=4) if len(costs[k]) >= 8 else [None, None, None] sizes.append({"day": day, "bucket": k, "n": sz_n[k], "usd": round(v), "cost_p25": q[0], "cost_p50": q[1], "cost_p75": q[2]}) keep_partner = Counter() for r in partner: keep_partner[r["partner"]] += r["usd"] big = {k for k, _ in keep_partner.most_common(60)} merged = defaultdict(lambda: [0, 0, 0.0, 0.0]) for r in partner: k = (r["day"], r["partner"] if r["partner"] in big else "(other)") merged[k][0] += r["usd"] merged[k][1] += r["swaps"] merged[k][2] += r["fee_usd"] merged[k][3] = r["posted_bps"] partner = [{"day": k[0], "partner": k[1], "usd": v[0], "swaps": v[1], "fee_usd": round(v[2], 2), "posted_bps": v[3]} for k, v in sorted(merged.items())] keep_rec = Counter() for r in recipient: keep_rec[r["recipient"]] += r["fee_usd"] # Only large, named fee collectors are published. Hex and small accounts roll into (other). big = {k for k, _ in keep_rec.most_common(15) if re.fullmatch(r"[a-z0-9_-]+(\.[a-z0-9_-]+)*\.[a-z]+", k) and not re.fullmatch(r"[0-9a-f]{64}", k)} merged = defaultdict(float) for r in recipient: merged[(r["day"], r["recipient"] if r["recipient"] in big else "(other)")] += r["fee_usd"] recipient = [{"day": k[0], "recipient": k[1], "fee_usd": round(v, 2)} for k, v in sorted(merged.items())] write("panel_daily", daily) write("panel_partner", partner) write("panel_recipient", recipient) write("panel_routes", sorted(routes, key=lambda r: (r["day"], r["kind"], -r["usd"]))) write("panel_sizes", sizes)def recent(): files = sorted((SRC / "recent").glob("*.csv.gz")) if not files: return weeks = defaultdict(lambda: {"send": Counter(), "recip": Counter(), "swaps": 0, "usd": 0.0}) first_seen = {} for path in files: day = path.name[:10] import datetime d = datetime.date.fromisoformat(day) wk = (d - datetime.timedelta(days=d.weekday())).isoformat() w = weeks[wk] for r in rows_in(path): if r["status"] != "SUCCESS" or r["in_usd"] <= 0: continue w["swaps"] += 1 w["usd"] += r["in_usd"] key = r["sender"] or r["recipient"] if key: w["send"][key] += r["in_usd"] first_seen.setdefault(key, wk) if r["recipient"]: w["recip"][r["recipient"]] += r["in_usd"] order = sorted(weeks) table = [] for i, wk in enumerate(order): w = weeks[wk] users = w["send"] ranked = users.most_common() total = sum(users.values()) or 1 ret1 = len(set(users) & set(weeks[order[i + 1]]["send"])) / len(users) * 100 if i + 1 < len(order) and users else None ret4 = len(set(users) & set(weeks[order[i + 4]]["send"])) / len(users) * 100 if i + 4 < len(order) and users else None table.append({"week": wk, "swaps": w["swaps"], "usd": round(w["usd"]), "users": len(users), "new_users": sum(1 for u in users if first_seen[u] == wk), "top1_share": round(ranked[0][1] / total * 100, 2) if ranked else 0, "top10_share": round(sum(v for _, v in ranked[:10]) / total * 100, 2), "top100_share": round(sum(v for _, v in ranked[:100]) / total * 100, 2), "top1000_share": round(sum(v for _, v in ranked[:1000]) / total * 100, 2), "median_user_usd": round(statistics.median(users.values()), 2) if users else 0, "ret_1w": round(ret1, 1) if ret1 is not None else "", "ret_4w": round(ret4, 1) if ret4 is not None else ""}) write("recent_weekly", table) all_users = Counter() for w in weeks.values(): all_users.update(w["send"]) freq = Counter(sum(1 for w in weeks.values() if u in w["send"]) for u in all_users) write("recent_frequency", [{"weeks_active": k, "users": v} for k, v in sorted(freq.items())])def whales(): files = sorted((SRC / "whales").glob("*.csv.gz")) if (SRC / "whales").exists() else [] rows = [] for path in files: usd = n = 0 senders = Counter() fee = 0.0 for r in rows_in(path): if r["status"] != "SUCCESS": continue usd += r["in_usd"] n += 1 fee += sum(b for _, b in r["bps"]) / 1e4 * r["in_usd"] senders[r["sender"] or r["recipient"]] += r["in_usd"] top = senders.most_common(10) rows.append({"month": path.name[:7], "swaps": n, "usd": round(usd), "fee_usd": round(fee), "users": len(senders), "top1_usd": round(top[0][1]) if top else 0, "top10_usd": round(sum(v for _, v in top))}) write("whales_monthly", rows)def sample_record(): """Records which windows the aggregates were built from, with the row count of each.""" out = {} for stage in ("panel", "recent", "whales"): out[stage] = {} for done in sorted((SRC / stage).glob("*.done")) if (SRC / stage).exists() else []: out[stage][done.name.split(".")[0]] = json.loads(done.read_text())["rows"] return outif __name__ == "__main__": panel() recent() whales() (OUT / "MANIFEST.json").write_text(json.dumps({"built_from": sample_record(), "files": MANIFEST}, indent=0, sort_keys=True))