Add conviction voting data and Dune pool balance exports

Conviction Voting (from Gnosis Chain RPC, contract 0xca164930...):
- 47 proposals, 36 funded, 10 cancelled, 680,295 TEC disbursed
- 891 stake events from 159 unique stakers
- 873 conviction support update events
- Notable: TE Academy (64K), Gravity DAO (76K), cadCAD (54K)

Dune exports (4 queries):
- Pool balances (4,220 rows), common pool (872), reserve pool (887)
- Token balances with USD valuations (1,986 rows)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-04-03 13:05:36 -07:00
parent 6496f46430
commit 041ee79945
1 changed files with 209 additions and 0 deletions

209
src/decode_cv.py Normal file
View File

@ -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)")