diff --git a/src/decode_cv.py b/src/decode_cv.py new file mode 100644 index 0000000..ef12e05 --- /dev/null +++ b/src/decode_cv.py @@ -0,0 +1,209 @@ +"""Decode conviction voting events from raw RPC log data.""" +import json +import csv +import sys +from collections import defaultdict + +DATA_DIR = sys.argv[1] if len(sys.argv) > 1 else "data/onchain" + +with open("/tmp/cv_events.json") as f: + d = json.load(f) +logs = d.get("result", []) + +PROPOSAL_ADDED = "0xe180363919da754b2737a8f10869b7d2df0be7ef0e81339d3b5dabba166060ed" +STAKE_CHANGED = "0x28d9b583e0c477691a08f6c1e00fedc0895ed4221487c627fa96a7024119f499" +SUPPORT_CHANGED = "0x16f23283da3097bc9027dcdf31f24863b1520556f04818d406f0e6ecd08580f5" +PROPOSAL_EXECUTED = "0xf758fc91e01b00ea6b4a6138756f7f28e021f9bf21db6dbf8c36c88eb737257a" + +by_topic = defaultdict(list) +for l in logs: + by_topic[l["topics"][0]].append(l) + +# --- Decode Proposals --- +proposals = {} +for l in by_topic[PROPOSAL_ADDED]: + topics = l["topics"] + prop_id = int(topics[2], 16) + creator_int = int(topics[1], 16) + creator = "0x" + hex(creator_int)[2:].zfill(40) if creator_int > 0 else "0x0" + action_id = int(topics[3], 16) + block = int(l["blockNumber"], 16) + + data = l["data"][2:] + chunks = [data[i : i + 64] for i in range(0, len(data), 64)] + + amount = int(chunks[2], 16) / 1e18 if len(chunks) > 2 else 0 + stable = bool(int(chunks[3], 16)) if len(chunks) > 3 else False + beneficiary_raw = chunks[4] if len(chunks) > 4 else "" + beneficiary = "0x" + beneficiary_raw[24:] if beneficiary_raw else "" + + # Extract link/metadata + link = "" + if len(chunks) > 6: + link_len = int(chunks[5], 16) + if 0 < link_len < 200: + link_hex = "".join(chunks[6 : 6 + (link_len + 31) // 32]) + try: + link = bytes.fromhex(link_hex[: link_len * 2]).decode( + "utf-8", errors="replace" + ) + except Exception: + link = "" + + proposals[prop_id] = { + "id": prop_id, + "block": block, + "tx_hash": l["transactionHash"], + "creator": creator, + "amount_requested": amount, + "stable": stable, + "beneficiary": beneficiary, + "action_id": action_id, + "link": link.strip("\x00"), + "status": "open", + "executed_block": "", + } + +# --- Executed proposals --- +for l in by_topic.get(PROPOSAL_EXECUTED, []): + prop_id = int(l["topics"][1], 16) + block = int(l["blockNumber"], 16) + if prop_id in proposals: + proposals[prop_id]["status"] = "executed" + proposals[prop_id]["executed_block"] = block + +# --- Check other event types for cancellation --- +other_topics = set(by_topic.keys()) - { + PROPOSAL_ADDED, + STAKE_CHANGED, + SUPPORT_CHANGED, + PROPOSAL_EXECUTED, +} +for t in other_topics: + for l in by_topic[t]: + if len(l["topics"]) > 1: + try: + prop_id = int(l["topics"][1], 16) + if prop_id in proposals and proposals[prop_id]["status"] == "open": + proposals[prop_id]["status"] = "cancelled" + except (ValueError, IndexError): + pass + +# --- Stakes --- +stakes = [] +for l in by_topic[STAKE_CHANGED]: + staker = "0x" + l["topics"][1][26:] + prop_id = int(l["topics"][2], 16) if len(l["topics"]) > 2 else -1 + block = int(l["blockNumber"], 16) + data = l["data"][2:] + chunks = [data[i : i + 64] for i in range(0, min(len(data), 320), 64)] + + tokens_staked = int(chunks[0], 16) / 1e18 if chunks else 0 + total_staked = int(chunks[1], 16) / 1e18 if len(chunks) > 1 else 0 + conviction = int(chunks[2], 16) / 1e18 if len(chunks) > 2 else 0 + + stakes.append( + { + "block": block, + "tx_hash": l["transactionHash"], + "staker": staker, + "proposal_id": prop_id, + "tokens_staked": tokens_staked, + "total_tokens_staked": total_staked, + "conviction": conviction, + } + ) + +# --- Support updates --- +supports = [] +for l in by_topic[SUPPORT_CHANGED]: + prop_id = int(l["topics"][1], 16) + block = int(l["blockNumber"], 16) + supports.append( + { + "block": block, + "proposal_id": prop_id, + "tx_hash": l["transactionHash"], + } + ) + +# Print summary +props_list = sorted(proposals.values(), key=lambda x: x["id"]) +print("=== CONVICTION VOTING SUMMARY ===") +print(f"Total proposals: {len(props_list)}") +print( + f" Executed: {sum(1 for p in props_list if p['status']=='executed')}" +) +print( + f" Cancelled: {sum(1 for p in props_list if p['status']=='cancelled')}" +) +print(f" Open: {sum(1 for p in props_list if p['status']=='open')}") +print(f"Stake events: {len(stakes)}") +print(f"Support updates: {len(supports)}") +print(f"Unique stakers: {len(set(s['staker'] for s in stakes))}") +total_requested = sum(p["amount_requested"] for p in props_list) +total_funded = sum( + p["amount_requested"] for p in props_list if p["status"] == "executed" +) +print(f"Total requested: {total_requested:,.0f} tokens") +print(f"Total funded (executed): {total_funded:,.0f} tokens") + +print(f"\n=== ALL PROPOSALS ===") +for p in props_list: + if p["status"] == "executed": + marker = "FUNDED" + elif p["status"] == "cancelled": + marker = "CANCEL" + else: + marker = "OPEN " + print( + f' {marker} #{p["id"]:2d} | {p["amount_requested"]:>10,.0f} tokens' + f' | beneficiary: {p["beneficiary"][:16]}...' + f' | link: {p["link"][:60]}' + ) + +# Save CSVs +fields_p = [ + "id", + "block", + "tx_hash", + "creator", + "amount_requested", + "stable", + "beneficiary", + "action_id", + "link", + "status", + "executed_block", +] +with open(f"{DATA_DIR}/cv_proposals.csv", "w", newline="") as f: + w = csv.DictWriter(f, fieldnames=fields_p, extrasaction="ignore") + w.writeheader() + for p in props_list: + w.writerow(p) + +fields_s = [ + "block", + "tx_hash", + "staker", + "proposal_id", + "tokens_staked", + "total_tokens_staked", + "conviction", +] +with open(f"{DATA_DIR}/cv_stakes.csv", "w", newline="") as f: + w = csv.DictWriter(f, fieldnames=fields_s) + w.writeheader() + for s in stakes: + w.writerow(s) + +fields_u = ["block", "proposal_id", "tx_hash"] +with open(f"{DATA_DIR}/cv_support_updates.csv", "w", newline="") as f: + w = csv.DictWriter(f, fieldnames=fields_u) + w.writeheader() + for s in supports: + w.writerow(s) + +print(f"\nSaved: cv_proposals.csv ({len(props_list)} rows)") +print(f"Saved: cv_stakes.csv ({len(stakes)} rows)") +print(f"Saved: cv_support_updates.csv ({len(supports)} rows)")