TEC-analysis/notebooks/01_data_collection.ipynb

377 lines
15 KiB
Plaintext

{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# 01 — TEC Data Collection\n",
"\n",
"Pull all on-chain data for the TEC token, ABC transactions, and conviction voting proposals.\n",
"\n",
"## Data targets:\n",
"1. **ABC Buy/Sell Orders** — every mint/burn through the bonding curve\n",
"2. **TEC Token Transfers** — all ERC-20 transfers\n",
"3. **Conviction Voting Proposals** — all proposals, stakes, outcomes\n",
"4. **Honeyswap Trades** — secondary market TEC/xDAI trades\n",
"5. **Reserve & Common Pool balances** — over time"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import os\n",
"import json\n",
"import time\n",
"import requests\n",
"import pandas as pd\n",
"from web3 import Web3\n",
"from datetime import datetime\n",
"\n",
"# Gnosis Chain RPC\n",
"RPC_URL = \"https://rpc.gnosischain.com\"\n",
"w3 = Web3(Web3.HTTPProvider(RPC_URL))\n",
"print(f\"Connected to Gnosis Chain: {w3.is_connected()}, block: {w3.eth.block_number}\")\n",
"\n",
"# Contract addresses\n",
"TEC_TOKEN = \"0x5dF8339c5E282ee48c0c7cE8A7d01a73D38B3B27\"\n",
"ABC_CONTRACT = \"0x74ade20c12067e2f9457c037809a73f35694f99f\"\n",
"RESERVE_POOL = \"0x4a3C145c35Fa0daa58Cb5BD93CE905B086087246\"\n",
"COMMON_POOL = \"0xb941365430a16659658bb23b88efaede1d839354\"\n",
"\n",
"ABC_START_BLOCK = 20087409\n",
"TOKEN_START_BLOCK = 20086944\n",
"\n",
"DATA_DIR = \"../data/onchain\""
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 1. ABC Buy/Sell Orders\n",
"\n",
"Event signatures from the ABC contract:\n",
"- `MakeBuyOrder(address indexed buyer, address indexed onBehalfOf, address indexed collateral, uint256 fee, uint256 purchaseAmount, uint256 returnedAmount, uint256 feePct)`\n",
"- `MakeSellOrder(address indexed seller, address indexed onBehalfOf, address indexed collateral, uint256 fee, uint256 sellAmount, uint256 returnedAmount, uint256 feePct)`"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Event topic hashes\n",
"BUY_ORDER_TOPIC = w3.keccak(text=\"MakeBuyOrder(address,address,address,uint256,uint256,uint256,uint256)\").hex()\n",
"SELL_ORDER_TOPIC = w3.keccak(text=\"MakeSellOrder(address,address,address,uint256,uint256,uint256,uint256)\").hex()\n",
"\n",
"print(f\"BuyOrder topic: {BUY_ORDER_TOPIC}\")\n",
"print(f\"SellOrder topic: {SELL_ORDER_TOPIC}\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def get_logs_chunked(address, topics, from_block, to_block=None, chunk_size=50000):\n",
" \"\"\"Fetch event logs in chunks to avoid RPC limits.\"\"\"\n",
" if to_block is None:\n",
" to_block = w3.eth.block_number\n",
" \n",
" all_logs = []\n",
" current = from_block\n",
" \n",
" while current <= to_block:\n",
" end = min(current + chunk_size - 1, to_block)\n",
" try:\n",
" logs = w3.eth.get_logs({\n",
" 'address': Web3.to_checksum_address(address),\n",
" 'topics': topics,\n",
" 'fromBlock': current,\n",
" 'toBlock': end\n",
" })\n",
" all_logs.extend(logs)\n",
" print(f\" Blocks {current}-{end}: {len(logs)} events (total: {len(all_logs)})\")\n",
" except Exception as e:\n",
" print(f\" Error at {current}-{end}: {e}\")\n",
" # Try smaller chunks on error\n",
" if chunk_size > 5000:\n",
" smaller = get_logs_chunked(address, topics, current, end, chunk_size // 5)\n",
" all_logs.extend(smaller)\n",
" current = end + 1\n",
" time.sleep(0.2) # Rate limiting\n",
" \n",
" return all_logs"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"print(\"Fetching ABC BuyOrder events...\")\n",
"buy_logs = get_logs_chunked(ABC_CONTRACT, [BUY_ORDER_TOPIC], ABC_START_BLOCK)\n",
"print(f\"\\nTotal BuyOrders: {len(buy_logs)}\")\n",
"\n",
"print(\"\\nFetching ABC SellOrder events...\")\n",
"sell_logs = get_logs_chunked(ABC_CONTRACT, [SELL_ORDER_TOPIC], ABC_START_BLOCK)\n",
"print(f\"\\nTotal SellOrders: {len(sell_logs)}\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def parse_abc_event(log, event_type):\n",
" \"\"\"Parse ABC buy/sell event log into a dict.\"\"\"\n",
" # Indexed params are in topics[1:], non-indexed in data\n",
" topics = log['topics']\n",
" data = log['data'].hex() if isinstance(log['data'], bytes) else log['data']\n",
" if data.startswith('0x'):\n",
" data = data[2:]\n",
" \n",
" # Decode non-indexed params (fee, purchaseAmount/sellAmount, returnedAmount, feePct)\n",
" chunks = [data[i:i+64] for i in range(0, len(data), 64)]\n",
" \n",
" block = w3.eth.get_block(log['blockNumber'])\n",
" \n",
" result = {\n",
" 'block_number': log['blockNumber'],\n",
" 'tx_hash': log['transactionHash'].hex() if isinstance(log['transactionHash'], bytes) else log['transactionHash'],\n",
" 'timestamp': datetime.utcfromtimestamp(block['timestamp']),\n",
" 'event_type': event_type,\n",
" 'actor': '0x' + topics[1].hex()[-40:] if isinstance(topics[1], bytes) else '0x' + topics[1][-40:],\n",
" 'on_behalf_of': '0x' + topics[2].hex()[-40:] if isinstance(topics[2], bytes) else '0x' + topics[2][-40:],\n",
" 'collateral': '0x' + topics[3].hex()[-40:] if isinstance(topics[3], bytes) else '0x' + topics[3][-40:],\n",
" 'fee_wei': int(chunks[0], 16),\n",
" 'amount_wei': int(chunks[1], 16), # purchaseAmount or sellAmount\n",
" 'returned_wei': int(chunks[2], 16),\n",
" 'fee_pct': int(chunks[3], 16),\n",
" }\n",
" # Convert to human-readable (18 decimals)\n",
" result['fee'] = result['fee_wei'] / 1e18\n",
" result['amount'] = result['amount_wei'] / 1e18\n",
" result['returned'] = result['returned_wei'] / 1e18\n",
" \n",
" return result\n",
"\n",
"# Note: This will make many RPC calls for block timestamps.\n",
"# For large datasets, consider batching or caching block timestamps.\n",
"print(\"Parsing buy orders (this may take a while due to block timestamp lookups)...\")\n",
"buy_orders = [parse_abc_event(log, 'buy') for log in buy_logs]\n",
"print(f\"Parsed {len(buy_orders)} buy orders\")\n",
"\n",
"print(\"Parsing sell orders...\")\n",
"sell_orders = [parse_abc_event(log, 'sell') for log in sell_logs]\n",
"print(f\"Parsed {len(sell_orders)} sell orders\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Combine and save\n",
"all_orders = buy_orders + sell_orders\n",
"df_abc = pd.DataFrame(all_orders).sort_values('block_number').reset_index(drop=True)\n",
"\n",
"os.makedirs(DATA_DIR, exist_ok=True)\n",
"df_abc.to_csv(f\"{DATA_DIR}/abc_orders.csv\", index=False)\n",
"print(f\"Saved {len(df_abc)} ABC orders\")\n",
"df_abc.head()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 2. TEC Token Transfers\n",
"\n",
"Standard ERC-20 `Transfer(address indexed from, address indexed to, uint256 value)` events."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"TRANSFER_TOPIC = w3.keccak(text=\"Transfer(address,address,uint256)\").hex()\n",
"\n",
"print(\"Fetching TEC Transfer events...\")\n",
"transfer_logs = get_logs_chunked(TEC_TOKEN, [TRANSFER_TOPIC], TOKEN_START_BLOCK)\n",
"print(f\"\\nTotal Transfers: {len(transfer_logs)}\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"def parse_transfer(log):\n",
" topics = log['topics']\n",
" data = log['data'].hex() if isinstance(log['data'], bytes) else log['data']\n",
" if data.startswith('0x'):\n",
" data = data[2:]\n",
" \n",
" return {\n",
" 'block_number': log['blockNumber'],\n",
" 'tx_hash': log['transactionHash'].hex() if isinstance(log['transactionHash'], bytes) else log['transactionHash'],\n",
" 'from': '0x' + (topics[1].hex() if isinstance(topics[1], bytes) else topics[1])[-40:],\n",
" 'to': '0x' + (topics[2].hex() if isinstance(topics[2], bytes) else topics[2])[-40:],\n",
" 'value_wei': int(data[:64], 16),\n",
" 'value': int(data[:64], 16) / 1e18,\n",
" }\n",
"\n",
"transfers = [parse_transfer(log) for log in transfer_logs]\n",
"df_transfers = pd.DataFrame(transfers).sort_values('block_number').reset_index(drop=True)\n",
"df_transfers.to_csv(f\"{DATA_DIR}/tec_transfers.csv\", index=False)\n",
"print(f\"Saved {len(df_transfers)} transfers\")\n",
"df_transfers.head()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 3. Conviction Voting Proposals\n",
"\n",
"The Gardens subgraph (hosted service) has been deprecated. We need to either:\n",
"1. Find the migrated Studio endpoint\n",
"2. Query Gardens contract events directly via RPC\n",
"3. Use cached/exported data from the TEC forum or Dune\n",
"\n",
"**TODO:** The Gardens conviction voting contract address for TEC needs to be identified.\n",
"It was deployed via the GardensTemplate at `0x5507A6365C47d5b201Af3c1CB18C7fD4a889321b`.\n",
"The Disputable CV app ID is `0xca60629a22f03bcad7738fee1a6f0c5863eb89463621b40566a6799b82cbe184`."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Try to find the TEC Garden/DAO address by looking at token controller or known interactions\n",
"# The MiniMe token's controller would be the fundraising/ABC app\n",
"# The Gardens DAO would interact with the common pool\n",
"\n",
"# Let's check what contracts have interacted with the common pool\n",
"print(\"Common Pool address:\", COMMON_POOL)\n",
"print(\"This is where conviction voting proposals draw funds from.\")\n",
"print(\"\\nWe need to find the CV contract by looking at outgoing transfers from the common pool.\")\n",
"print(\"These would correspond to funded proposals.\")\n",
"\n",
"# Fetch transfers FROM the common pool\n",
"ZERO_PADDED_COMMON_POOL = '0x' + COMMON_POOL[2:].lower().zfill(64)\n",
"print(f\"\\nFetching transfers FROM common pool...\")\n",
"common_pool_out_logs = get_logs_chunked(\n",
" TEC_TOKEN, \n",
" [TRANSFER_TOPIC, ZERO_PADDED_COMMON_POOL], # from = common pool\n",
" TOKEN_START_BLOCK\n",
")\n",
"print(f\"Outgoing transfers from common pool: {len(common_pool_out_logs)}\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Parse common pool outflows — these represent funded proposals\n",
"if common_pool_out_logs:\n",
" cp_outflows = [parse_transfer(log) for log in common_pool_out_logs]\n",
" df_cp_out = pd.DataFrame(cp_outflows).sort_values('block_number').reset_index(drop=True)\n",
" df_cp_out.to_csv(f\"{DATA_DIR}/common_pool_outflows.csv\", index=False)\n",
" print(f\"Common pool outflows:\")\n",
" print(df_cp_out[['block_number', 'to', 'value']].to_string())\n",
"else:\n",
" print(\"No outflows found — common pool may use a different token (xDAI/WXDAI) for disbursements\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 4. Quick Sanity Check — Current State"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Check current TEC token supply and key balances\n",
"erc20_abi = [\n",
" {\"constant\": True, \"inputs\": [], \"name\": \"totalSupply\", \"outputs\": [{\"type\": \"uint256\"}], \"type\": \"function\"},\n",
" {\"constant\": True, \"inputs\": [{\"name\": \"_owner\", \"type\": \"address\"}], \"name\": \"balanceOf\", \"outputs\": [{\"type\": \"uint256\"}], \"type\": \"function\"},\n",
" {\"constant\": True, \"inputs\": [], \"name\": \"name\", \"outputs\": [{\"type\": \"string\"}], \"type\": \"function\"},\n",
" {\"constant\": True, \"inputs\": [], \"name\": \"symbol\", \"outputs\": [{\"type\": \"string\"}], \"type\": \"function\"},\n",
"]\n",
"\n",
"tec = w3.eth.contract(address=Web3.to_checksum_address(TEC_TOKEN), abi=erc20_abi)\n",
"\n",
"total_supply = tec.functions.totalSupply().call() / 1e18\n",
"reserve_bal = tec.functions.balanceOf(Web3.to_checksum_address(RESERVE_POOL)).call() / 1e18\n",
"common_bal = tec.functions.balanceOf(Web3.to_checksum_address(COMMON_POOL)).call() / 1e18\n",
"\n",
"# Check xDAI (native) balance of reserve pool\n",
"reserve_xdai = w3.eth.get_balance(Web3.to_checksum_address(RESERVE_POOL)) / 1e18\n",
"common_xdai = w3.eth.get_balance(Web3.to_checksum_address(COMMON_POOL)) / 1e18\n",
"\n",
"print(f\"TEC Token: {tec.functions.name().call()} ({tec.functions.symbol().call()})\")\n",
"print(f\"Total Supply: {total_supply:,.2f} TEC\")\n",
"print(f\"\\nReserve Pool:\")\n",
"print(f\" TEC balance: {reserve_bal:,.2f}\")\n",
"print(f\" xDAI balance: {reserve_xdai:,.2f}\")\n",
"print(f\"\\nCommon Pool:\")\n",
"print(f\" TEC balance: {common_bal:,.2f}\")\n",
"print(f\" xDAI balance: {common_xdai:,.2f}\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Next Steps\n",
"\n",
"After running this notebook:\n",
"1. `abc_orders.csv` — All bonding curve buy/sell transactions with prices\n",
"2. `tec_transfers.csv` — All TEC token transfers\n",
"3. `common_pool_outflows.csv` — Funded proposals (if TEC denominated)\n",
"\n",
"**Still needed:**\n",
"- Honeyswap trade data (secondary market) — may need Honeyswap subgraph or Dune export\n",
"- Conviction voting proposal metadata (titles, descriptions, requesters) — forum scraping or Gardens subgraph\n",
"- Historical xDAI/USD price for USD-denominating all values\n",
"- Reserve pool balance history (xDAI over time)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python",
"version": "3.11.0"
}
},
"nbformat": 4,
"nbformat_minor": 4
}