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:
1 parent
539b83b330
commit
c5e30a4785
7 files changed
+496
-173
No files matched your search
+162
@@ -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())
|
||||
Reference in new issue
Block a user