139 lines
4.7 KiB
Python
139 lines
4.7 KiB
Python
"""Generate a Markwhen timeline (.mw) for the TEC retrospective.
|
|
|
|
Inputs:
|
|
data/onchain/cv_proposals.csv (id, block, amount_requested, link, status, executed_block)
|
|
data/onchain/cv_proposal_categories.json (id -> category tag, optional)
|
|
|
|
Output:
|
|
output/tec_retrospective.mw
|
|
|
|
Anchoring: Gnosis block 20086948 == 2022-01-27 (CV proposal #1 per synthesis),
|
|
block time ~5.0s. Close enough for a retrospective timeline.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import csv
|
|
import json
|
|
from datetime import datetime, timedelta, timezone
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
PROPOSALS = ROOT / "data" / "onchain" / "cv_proposals.csv"
|
|
CATEGORIES = ROOT / "data" / "onchain" / "cv_proposal_categories.json"
|
|
OUT = ROOT / "output" / "tec_retrospective.mw"
|
|
|
|
ANCHOR_BLOCK = 20086948
|
|
ANCHOR_DATE = datetime(2022, 1, 27, tzinfo=timezone.utc)
|
|
GNOSIS_BLOCK_SECONDS = 5.0
|
|
|
|
|
|
def block_to_date(block: int) -> datetime:
|
|
delta = (block - ANCHOR_BLOCK) * GNOSIS_BLOCK_SECONDS
|
|
return ANCHOR_DATE + timedelta(seconds=delta)
|
|
|
|
|
|
def load_categories() -> dict[str, str]:
|
|
if not CATEGORIES.exists():
|
|
return {}
|
|
raw = json.loads(CATEGORIES.read_text())
|
|
if isinstance(raw, dict):
|
|
return {str(k): str(v) for k, v in raw.items()}
|
|
return {}
|
|
|
|
|
|
HEADER = """---
|
|
title: Token Engineering Commons — Retrospective
|
|
view: timeline
|
|
timezone: UTC
|
|
#hatch: blue
|
|
#abc: orange
|
|
#cv: green
|
|
#shock: red
|
|
#migration: purple
|
|
#shutdown: gray
|
|
---
|
|
|
|
// Generated by scripts/gen_timeline.py
|
|
// Edit macro events here; CV proposal section is auto-appended.
|
|
|
|
section Origins #hatch
|
|
2021-02-01 / 2021-08-11: Trusted Seed recruitment & Hatch prep
|
|
210 backers onboarded via Commons Stack Hatch app
|
|
2021-08-11: Hatch closed — 1,571,224 wxDAI raised from 210 backers #hatch
|
|
https://medium.com/token-engineering-commons/the-hatch-was-successful-2443cd07cbb0
|
|
2021-08-12 / 2022-01-26: Commons Configuration Dashboard — 40 parameter proposals, 2 rounds of voting #hatch
|
|
endSection
|
|
|
|
section ABC & Markets #abc
|
|
2022-01-25: 500K whale purchase — 499,803 xDAI (46.5% of all ABC buy volume, ever) #abc
|
|
wallet 0xf5dcd98a...12f35497 · entry tribute ~109,957 xDAI
|
|
2022-01-27: ABC live on Gnosis Chain; first CV proposal submitted #abc #cv
|
|
2022-05-09: Terra/Luna collapse — first exogenous shock #shock
|
|
2022-11-08: FTX collapse — second exogenous shock, crypto winter deepens #shock
|
|
2023-12-01: ABC price bottom — 0.36 xDAI (-88% from peak of 2.94) #abc
|
|
endSection
|
|
|
|
section Treasury & Migration #migration
|
|
2023-12-15: Migration to Optimism; CV replaced by multisig coordination #migration
|
|
reserve currency: xDAI → rETH
|
|
https://forum.tecommons.org/t/and-we-are-live-on-op-mainnet/1324
|
|
2024-01-01 / 2025-11-04: Post-migration draw-down era (manual allocations)
|
|
2025-04-01: Q1 2025 Progress Report — tribute revenue $3,614/qtr vs $42,244/qtr opex #shutdown
|
|
https://forum.tecommons.org/t/tec-q1-2025-progress-report/1472
|
|
2025-11-04: TEC shutdown proposal formally approved #shutdown
|
|
https://forum.tecommons.org/t/tec-shutdown-proposal/1520
|
|
endSection
|
|
|
|
"""
|
|
|
|
|
|
def build_cv_section(rows: list[dict], cats: dict[str, str]) -> str:
|
|
lines = ["section Conviction Voting Proposals #cv", ""]
|
|
for row in rows:
|
|
pid = row["id"]
|
|
try:
|
|
block = int(row["block"])
|
|
except (TypeError, ValueError):
|
|
continue
|
|
created = block_to_date(block)
|
|
executed = None
|
|
if row.get("executed_block"):
|
|
try:
|
|
executed = block_to_date(int(row["executed_block"]))
|
|
except ValueError:
|
|
pass
|
|
amount = row.get("amount_requested") or "0"
|
|
try:
|
|
amount_f = float(amount)
|
|
except ValueError:
|
|
amount_f = 0.0
|
|
status = (row.get("status") or "").lower()
|
|
title = (row.get("link") or f"Proposal #{pid}").strip() or f"Proposal #{pid}"
|
|
title = title.replace("\n", " ")[:90]
|
|
cat = cats.get(str(pid), "")
|
|
tags = [f"#{status}"] if status else []
|
|
if cat:
|
|
tags.append(f"#{cat.lower().replace(' ', '_')}")
|
|
tag_str = " " + " ".join(tags) if tags else ""
|
|
if executed and status == "executed":
|
|
rng = f"{created:%Y-%m-%d} / {executed:%Y-%m-%d}"
|
|
else:
|
|
rng = f"{created:%Y-%m-%d}"
|
|
amount_note = f" — {amount_f:,.0f} TEC" if amount_f > 0 else ""
|
|
lines.append(f"{rng}: #{pid} {title}{amount_note}{tag_str}")
|
|
lines.append("endSection")
|
|
return "\n".join(lines) + "\n"
|
|
|
|
|
|
def main() -> None:
|
|
rows = list(csv.DictReader(PROPOSALS.open()))
|
|
cats = load_categories()
|
|
body = HEADER + build_cv_section(rows, cats)
|
|
OUT.parent.mkdir(exist_ok=True)
|
|
OUT.write_text(body)
|
|
print(f"Wrote {OUT} ({len(rows)} proposals)")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|