185 lines
5.6 KiB
Python
185 lines
5.6 KiB
Python
"""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()) |