"""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 from config import API_NODE, DB_PATH, MIN_STAKED_VEX, TARGET_BP # Kontrak & scope sistem Vexanium = vexcore (BUKAN vexio sesuai konvensi EOS) CODE = 'vexcore' SCOPE = 'vexcore' TABLE = 'voters' # Ukuran halaman saat paginasi LIMIT = 500 # 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. WAL: journal_mode=WAL → pembaca (dashboard multi-worker) tak terblokir saat DROP+INSERT harian. Mode ini persisten setelah diset. """ scanned_at = datetime.now().isoformat(timespec='seconds') conn = sqlite3.connect(DB_PATH) try: conn.execute('PRAGMA journal_mode=WAL') 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())