"""Scan tabel voters Vexanium, simpan pemilih BP databisnisid + bobot suara ke SQLite. Berjalan imparsial (repeatable) dan siap dijadwalkan (cron). Memakai requests. """ import math import sqlite3 import sys import time from datetime import datetime, timedelta 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' # Skala stake (×10000) dan epoch bobot suara Vexanium (1 Jan 2000) VEX_SCALE = 10000 WEIGHT_EPOCH = datetime(2000, 1, 1) def derive_last_vote(weight, staked_vex): """V13: perkiraan waktu revote terakhir dari rasio bobot/stake. Vexanium: last_vote_weight = staked_raw × 2^(tahun sejak 2000). Maka t = log2(last_vote_weight / staked_raw) → tanggal revote. Kembalikan None bila tak bisa dihitung (bobot/stake nol atau tak valid). """ staked_raw = staked_vex * VEX_SCALE if staked_raw <= 0: return None try: ratio = float(weight) / staked_raw except (TypeError, ValueError): return None if ratio <= 0: return None t_years = math.log(ratio) / math.log(2) return (WEIGHT_EPOCH + timedelta(days=t_years * 365.25)).isoformat(timespec='seconds') 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, 'last_vote': derive_last_vote(row.get('last_vote_weight', ''), 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, ' 'last_vote TEXT)' ) conn.executemany( 'INSERT INTO voters (owner, weight, staked, scanned_at, last_vote) ' 'VALUES (?, ?, ?, ?, ?)', [(r['owner'], r['weight'], r['staked'], scanned_at, r.get('last_vote')) 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())