T1-T5: Python voter fetcher (§V1-9)

- get_voters.py: scan Vexanium voters table (vexcore), retry/paginate,
  filter databisnisid single-vote + stake ≥1000 VEX, store weight verbatim,
  daily-replace SQLite snapshot (owner PK, scanned_at)
- V8/V9 + test_get_voters.py (oracle, §V1-9)
- SPEC.md, AGENTS.md, requirements.txt
This commit is contained in:
proitlab committed 2026-08-04 15:56:19 +07:00
1 parent 539b83b330
commit c5e30a4785
7 files changed
+496 -173

No files matched your search

+162
View File
@@ -0,0 +1,162 @@
"""Scan tabel voters Vexanium, simpan pemilih BP databisnisid + bobot suara ke SQLite.
Berjalan imparsial (repeatable) dan siap dijadwalkan (cron). Memakai requests.
"""
import sqlite3
import sys
import time
from datetime import datetime
import requests
# Akun Block Producer (BP) yang dicari
TARGET_BP = 'databisnisid'
# Node RPC publik Vexanium
API_NODE = 'https://v2.vexascan.com:2096'
# Kontrak & scope sistem Vexanium = vexcore (BUKAN vexio sesuai konvensi EOS)
CODE = 'vexcore'
SCOPE = 'vexcore'
TABLE = 'voters'
# Ukuran halaman saat paginasi
LIMIT = 500
# Syarat minimum stake (dalam VEX) untuk disimpan
MIN_STAKED_VEX = 1000
# Lokasi basis data SQLite
DB_PATH = 'voters.db'
# Ambang-batas nama akun "0" (kodifikasi uint64 0) pada tabel voters
ZERO_OWNER = '...........q'
def fetch_with_retry(payload, max_retries=3, timeout=30):
"""POST ke RPC dengan retry (node publik sering timeout).
V1: ulang ≥3 kali pada timeout/error HTTP, jeda 2 detik antar percobaan.
"""
url = f'{API_NODE}/v1/chain/get_table_rows'
for attempt in range(max_retries):
try:
resp = requests.post(url, json=payload, timeout=timeout)
resp.raise_for_status()
return resp.json()
except requests.RequestException:
if attempt == max_retries - 1:
raise
print(f'[Peringatan] Gagal menghubungi node, mencoba lagi... '
f'({attempt + 1}/{max_retries})')
time.sleep(2)
def scan_voters():
"""Iterasi seluruh tabel voters (paginasi), kembalikan daftar mentah.
V2: majukan lower_bound ke owner terakhir; berhenti saat more=false | baris kosong.
V3: pada halaman kelanjutan, buang baris pertama (induk pamungkas duplikat).
"""
rows_all = []
lower_bound = ''
has_more = True
while has_more:
payload = {
'json': True,
'code': CODE,
'scope': SCOPE,
'table': TABLE,
'lower_bound': lower_bound,
'limit': LIMIT,
}
data = fetch_with_retry(payload)
rows = data.get('rows') or []
if not rows:
break
if lower_bound != '':
rows = rows[1:]
rows_all.extend(rows)
has_more = data.get('more', False)
if has_more:
lower_bound = rows_all[-1]['owner']
return rows_all
def normalize(row):
"""V4/V5/V6: saring ke baris yang relevan, bidang dinormalisasi."""
owner = row['owner']
if owner == ZERO_OWNER: # V4: lewati sentinel nama akun 0
return None
if not (row.get('producers') == [TARGET_BP]): # V5
return None
staked_vex = float(row.get('staked', 0)) / 10000 # V8/V9: bisa string, ÷10000
if staked_vex < MIN_STAKED_VEX:
return None
return {
'owner': owner,
'weight': row.get('last_vote_weight', ''), # V6: bobot verbatim (string)
'staked': staked_vex,
}
def persist(rows):
"""V7: ganti snapshot — DROP+INSERT tiap skan, ⊥ history/append."""
scanned_at = datetime.now().isoformat(timespec='seconds')
conn = sqlite3.connect(DB_PATH)
try:
conn.execute('DROP TABLE IF EXISTS voters') # snapshot bersih
conn.execute(
'CREATE TABLE voters (' # I.db
'owner TEXT PRIMARY KEY, '
'weight TEXT NOT NULL, '
'staked REAL NOT NULL, '
'scanned_at TEXT NOT NULL)'
)
conn.executemany(
'INSERT INTO voters (owner, weight, staked, scanned_at) '
'VALUES (?, ?, ?, ?)',
[(r['owner'], r['weight'], r['staked'], scanned_at) for r in rows],
)
conn.commit()
finally:
conn.close()
return scanned_at
def summarize(rows, scanned_at):
"""I.cmd: ringkasan stdout dalam bahasa Indonesia, format id-ID."""
print('\n=== HASIL AKHIR ===')
print(f'Total pemilih untuk BP [{TARGET_BP}]: {len(rows)}')
print(f'Waktu pemindaian: {scanned_at}')
print('\nDaftar Pemilih:')
if not rows:
print('(Tidak ada pemilih ditemukan untuk BP ini)')
return
for index, row in enumerate(rows, start=1):
print(f"{index}. {row['owner']} - Stake: "
f"{row['staked']:,.4f} VEX - Bobot: {row['weight']}")
def main():
"""Titik masuk CLI."""
print(f'Mulai memindai voters untuk BP: "{TARGET_BP}"...')
raw = scan_voters()
filtered = []
for row in raw:
item = normalize(row)
if item is not None:
filtered.append(item)
scanned_at = persist(filtered)
summarize(filtered, scanned_at)
return 0
if __name__ == '__main__':
sys.exit(main())